您的位置:首页 > 理论基础 > 计算机网络

优雅设计封装基于Okhttp3的网络框架(二):多线程下载功能原理设计 及 简单实现

2017-07-09 13:24 1091 查看
通过上篇博客的学习,相信大家已经对Http协议及OKhttp3框架使用有一定的理解,切身感受到OKhttp3请求网络方法简单易懂,是否意味着可以直接通过它去解决开发中的相关网络需求?

并非如此,它的使用方式确实简单,但是在实现某一模块或功能时,不仅要考虑OKhttp3框架使用,还涉及到其余网络编程相关知识的混合,例如线程的调度和管理、本地缓存操作处理。当多个模块融合在一起时,会发现实现基本功能不仅仅是调用OKhttp3框架那么简单,接下来通过实现一个简单的下载例子来感受在学习过程中如何去将相关知识实践到代码中,此篇文章需要完成的功能是多线程下载。

一. 多线程下载剖析

1 . 涉及到多线程下载的Http字段

多线程下载并不是一个新颖的功能,但是很考验一个开发者除Android之外的综合知识,首先从涉及到一些Http相关字段开始:

Transfer-Encoding:chunked

Transfer-Encoding是客户端接收的编码传输方式。在下载文件的过程中会遇到Transfer-Encoding为chunked的编码方式。以一个实际场景的例子来解释:客户端在与服务器进行交互时有时候耗时过长,为了解决此问题采用chunked编码方式,即传输数据给客户端是分成一部分一部分去发送,利于客户端可以先去展示一部分数据给用户,提升体验。但是仅限于下载呈现资源的情况,若是下载文件则会导致无法获取Http协议请求头的文件总长度!

对于将要实现的多线程下载功能来说,有必要判断Transfer-Encoding方式,避免影响文件下载

content-length

它是作用在Http响应头的数据,标识当前下载或访问的文件总长度。

对于将要实现的多线程下载功能来说,获取当前文件总长度,再去判断每个线程需要下载的文件大小做处理。

Range

它可以去访问服务器中指定长度的内容,即提供了跨段式的访问。举个例子,服务器中有一个文本文件,长度为100Kb,那么可以通过指定Range去访问该文本50Kb~60Kb的内容。

对于将要实现的多线程下载功能来说,在实现断点续传时会把当前数据保存到本地中,避免下次重复请求,可通过Range字段指定需要下载的部分。

2 . 多线程下载原理

在实现多线程下载功能之前开发者必须对其源码熟透于心,因为我们需要控制线程之间的调度、开启和关闭,取消机制等等,所以在此之前还要介绍熟悉线程池的实现、原理,很考验掌控多线程的能力。下面首先举个例子来了解多线程原理:



上图所示,例如下载一个100字节的文件,为了减少用户等待时间,采用两个线程同时下载该文件的不同部分,用Range字段来指定线程的下载部分。这个简单的例子是我们在多线程下载时需要考虑和实现的,但是在一个成熟的框架中,需要考虑的远多于这个简单的例子,以下是实现功能时需要考虑的问题:

3 .多线程下载需要解决问题

文件存储的位置

你也许会认为文件存储肯定是在手机的SD卡,但是不可忽视有的手机并无内置SD卡,会出现异常。通常的解决方式是首先判断有无SD卡,若有则存储在SD卡的某个文件夹下,若无则可以把数据存储到应用程序的Cache目录中。

文件是否受损

在下载文件的过程中可能出现文件受损的情况,其中一个字节的编码方式会导致文件无法显示。为了保证文件格式和编码方式正确,需要对文件进行校验,方式是:通过全文件的MD5值进行校验,也就是说当服务器去请求文件时,将文件的所有内容进行MD5加密生成一个值,在客户端请求时获取该标识值,和在客户端下载完毕后本地对文件内容进行MD5加密生成的值进行比较,即可判断文件下载过程中是否受损。

文件空间大小是否足够

很常见的一个细节问题,下载之前需要去检查当前可存储空间大小是否适于下载该文件。

进度条的更新

进度条的更新是为了提高用户的体验,实现方式有多种:Handler发消息、check文件大小等。

数据保存

此点较为重要,需要去保证文件下载格式和存储路径、文件受损等问题是否完全正确,在编码过程中需要做诸如此类的校验。

4 . 简介Android的下载功能

一个功能的实现重来都不是简单的,需要考虑很多问题,来看一下Android系统自带的下载功能实现:



以上是Android系统中下载模块的核心类,系统是采用Provider形式实现的,因为应用程序可能会跨进程调用,如果在你项目中需要使用到系统的下载模块,需要唤醒Provider实例,获取实例后开启DownloadldleService,在Service中处理当前下载数据,首先在本地数据库检查有哪些数据需要下载,通过DownloadThread进行下载,以上是一个基本的流程。

简单概括就是:通过ContentProvider进进行跨进程间的调用,然后把需要下载的数据存储到对应的数据库中来明确需要下载的数据,并且提供Notification界面向用户展示下载进度。

二. 设计多线程下载的HTTP字段实际应用

在稍作了解涉及下载的Http字段和多线程下载的原理后,现在由代码重现实现该功能可能遇到的问题。

1 . content-length 和 Transfer-Encoding

先从涉及到下载功能的HTTP字段开始:如果想要实现文件下载功能,首先要获取 content-length 字段,因为想要多线程下载文件时,需要根据文件总长度来分配线程下载任务。需要注意的是并不是每个HTTP响应头那个都有 content-length 字段。以下来实例证实:打开Chrome开发者模式,访问腾讯网,查看首页资源的Headers



查看以上响应头发现并无content-length 字段,倒是发现了Transfer-Encoding:chunked,由此可证明腾讯新闻网页为了呈现良好的用户体验, 传输给客户端的数据是分成一部分一部分,并非一次性获取所有数据,所以无法对腾讯网资源获取实现多线程下载!但是对于一些静态的文件,例如图片、js文件等,可以获取content-length 字段,来查看一个png图片:



发现可获取到该图片的content-length字段,所以可得结论

只有获取一些静态文件,例如图片、js文件、纯文本才可以获取到响应头的content-length字段。是否可以获取到content-length字段还是取决于服务器,若要实现多线程下载的功能,对应请求的接口必须有content-length字段。需要重点判断Transfer-Encoding字段,因为它决定了content-length字段的存在与否。

2 . Range

Range字段通过HTTP请求服务器不容易查看,实现一个小demo来查看Range字段使用情况,已得知腾讯网上一直图片的content-length为4009,在此使用Range字段请求一部分数据,代码如下:

class RangHttp {
public static void main(String args[]) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://img1.gtimg.com/news/pics/hv1/17/107/2222/144512852.jpg").
addHeader("Accept-Encoding","identity").
//使用Range字段指定下载部分☆☆☆☆☆
addHeader("Range", "bytes=0-5").
build();
try {
Response response = client.newCall(request).execute();
System.out.println("content-length : "+response.body().contentLength());
if (response.isSuccessful()) {
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i++) {
System.out.println(headers.name(i) + " : " + headers.value(i));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}


输出结果:



由上图实践可知,我们在代码中指定下载图片的0~5字节,所以最终下载的文件总长度content-length字段为6字节。注意这里会多出一个字段Content-Range: bytes 0-5/4009,显示下载的部分和总长度。(如果在代码中写
addHeader("Range", "bytes=0-").
,后续不指定则会下载全部)。

这里我在请求的时候多加了一个字段
addHeader("Accept-Encoding","identity")
,作用是若请求服务器的资源支持content-length,会优先返回。若该资源为Transfer-Encoding:chunked,则无法改变。

三. 多线程下载功能准备工作

在测试完Http字段的demo后,我们对多线程下载的认识更加清晰了一些,但在实现编码之前,需要做一些准备工作,具体如下:



1 . Log工具类

首先需要编写整个项目的测试Log工具类,此操作各位应该并不陌生,为了便于后期调试,对Log类进行简单封装,代码如下:

public class Logger {
public static final boolean DEBUG = true;

public static void debug(String tag, String message) {
if (DEBUG) {
Log.d(tag, message);
}
}

public static void debug(String tag, String message, Object... args) {
if (DEBUG) {
Log.d(tag, String.format(Locale.getDefault(), message, args));
}
}

public static void error(String tag, String message) {
if (DEBUG) {
Log.e(tag, message);
}
}

public static void info(String tag, String message) {
if (DEBUG) {
Log.i(tag, message);
}
}

public static void warn(String tag, String message) {
if (DEBUG) {
Log.w(tag, message);
}
}
}


2 . 文件管理

在实现多线程下载时会涉及到文件管理,所以创建一个文件管理类,通过它管理所有文件存储的操作,代码如下:

public class FileStorageManager {
private static final FileStorageManager sManager = new FileStorageManager();

private Context mContext;

public static FileStorageManager getInstance() {
return sManager;
}

private FileStorageManager() {
}

public void init(Context context) {
this.mContext = context;
}

public File getFileByName(String url) {
File parent;

if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
parent = mContext.getExternalCacheDir();
} else {
parent = mContext.getCacheDir();
}

String fileName = Md5Uills.generateCode(url);

File file = new File(parent, fileName);
if (!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
}


查看以上代码,FileStorageManager 类提供的是单例模式,重点放在
getFileByName
方法,即根据URL获取文件,在此篇文章讲解文件存储位置时已经提过,此方法的逻辑就是如此:首先判断有无SD卡,若有则存储在SD卡的某个文件夹下,若无则可以把数据存储到应用程序的Cache目录(系统默认的Cache目录)中;确定好存储位置后,根据Md5加密url获取文件名,而后创建文件。

3 . MD5加密URL

一个MD5工具类,其中含有一个方法,即对url进行加密,很常见的加密方法,代码如下:

public class Md5Uills {

public static String generateCode(String url) {
if (TextUtils.isEmpty(url)) {
return null;
}

StringBuffer buffer = new StringBuffer();
try {
MessageDigest digest = MessageDigest.getInstance("md5");
digest.update(url.getBytes());
byte[] cipher = digest.digest();
for (byte b : cipher) {
String hexStr = Integer.toHexString(b & 0xff);
buffer.append(hexStr.length() == 1 ? "0" + hexStr : hexStr);
}

} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}

return buffer.toString();
}
}


4 . 创建Application

项目中需要创建一个Application ,文件管理类FileStorageManager需在
onCreate()
方法中初始化,并传入Context,代码如下:

public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
FileStorageManager.getInstance().init(this);
}
}


四. 多线程下载编码简单实现

在完成以上准备工作后,现在开始正式编码实现多线程下载功能,从偶想吃下载网络请求开始:

1 . 网络请求接口

首先需要定义网络请求接口,它主要用来处理接口的回调:

public interface DownloadCallback {

void success(File file);

void fail(int errorCode, String errorMessage);

void progress(int progress);
}


以上所示,最基本的
success
fail
progress
三个方法。需要注意,接口的定义直接影响到整个程序的拓展性。以上三个接口中的参数都具有含义:

success(File file)

如果下载成功,在回调方法中传入File文件,这个File文件时当初通过url加密后创建的,若有需求可在此回调中获取文件具体存储位置等相关信息。

fail(int errorCode, String errorMessage)

这里的参数有errorCodeerrorMessage,对于开发者来说,错误码足以说明问题所在,但是补充的错误信息可以更好的将错误呈现到用户面前,也帮组了开发者错误理解。

progress(int progress)

也是一个常见的接口定义,为了呈现更好的用户体验,在此可进行显示下载进度等相关操作。

2 . 网络请求类HttpManager

接口实现后,定义一个简单的网络请求类HttpManager,首先还是以单利模式来提供HttpManager实例对象,与文件管理类FileStorageManager 相同,提供初始化方法
init
,需要在Application中进行初始化。接下来需要定义一些与网络请求相关的接口,同步请求和异步请求封装,实现如下:

public class HttpManager {

public static final int NETWORK_ERROR_CODE = 1;
public static final int CONTENT_LENGTH_ERROR_CODE = 2;
public static final int TASK_RUNNING_ERROR_CODE = 3;

private static final HttpManager sManager = new HttpManager();

private Context mContext;

private OkHttpClient mClient;

public static HttpManager getInstance() {
return sManager;
}

private HttpManager() {
mClient = new OkHttpClient();
}

public void init(Context context) {
this.mContext = context;
}

/**
* 同步请求
*
* @param url
* @return
*/
public Response syncRequest(String url) {
Request request = new Request.Builder().url(url).build();
try {
return mClient.newCall(request).execute();
} catch (IOException e) {
e.printStackTrace();
}
return null;

}

/**
* 异步调用
*
* @param url
* @param callback
*/
public void asyncRequest(final String url, Callback callback) {
Request request = new Request.Builder().url(url).build();
mClient.newCall(request).enqueue(callback);
}

public void asyncRequest(final String url, final DownloadCallback callback) {
Request request = new Request.Builder().url(url).build();
mClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, Response response) throws IOException {

if (!response.isSuccessful() && callback != null) {
callback.fail(NETWORK_ERROR_CODE, "请求失败");
}

File file = FileStorageManager.getInstance().getFileByName(url);

byte[] buffer = new byte[1024 * 500];
int len;
FileOutputStream fileOut = new FileOutputStream(file);
InputStream inStream = response.body().byteStream();
while ((len = inStream.read(buffer, 0, buffer.length)) != -1) {
fileOut.write(buffer, 0, len);
fileOut.flush();
}
callback.success(file);
}
});
}
}


(1)同步请求 syncRequest(String url)

同步请求由于没有回调封装处理很简单,提供参数URL,根据URL创建请求Request,执行请求返回请求结果Response

(2)异步回调 asyncRequest(final String url, final DownloadCallback callback)

首先也是根据URL创建请求Request,执行请求在回调中处理结果。重点是未出现异常的回调方法
onResponse
中,步骤如下:

a. 先判断响应response是否成功且callback不等于null,若不满足条件则是网络请求异常,调用之前自定义的接口DownloadCallback
fail
方法,传入错误码及信息。

b. 走到这里代表请求下载成功。通过URL生成文件对象,接下来进行文件写入操作:创建FileOutputStream对象准备写入,通过
response.body().byteStream()
获取到下载文件的输入流InputStream对象,利用while循环读取输入流的信息写入到文件中,最后读写完毕调用自定义的接口DownloadCallback
success
方法,传入写好的文件File。

3 . 测试 —— 异步下载图片

这里通过异步回调的网络请求来下载腾讯网上的一张照片显示到手机上,代码如下:

HttpManager.getInstance().asyncRequest("http://img1.gtimg.com/kid/pics/hv1/4/175/2222/144530179.jpg", new DownloadCallback() {
@Override
public void success(File file) {
final Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());
runOnUiThread(new Runnable() {
@Override
public void run() {
//在主线程中进行UI操作
mImageView.setImageBitmap(bitmap);
}
});
Logger.debug("MainActivity", "success" + file.getAbsolutePath());

}

@Override
public void fail(int errorCode, String errorMessage) {
Logger.debug("MainActivity", "fail" + errorMessage);
}

@Override
public void progress(int progress) {
}
});


结果:



通过以上结果图片可知,之前编写的请求接口DownloadCallback 及网络请求类HttpManager正确,搭配使用无误。

五. 小结

1 . 此篇文章总结

此篇文章首先介绍了设计多线程下载的三个HTTP字段,在第一篇文章中也着重学习了HTTP相关知识,可见完成一个功能不止需要单方面的知识,涉及到多方面只是的融合。再介绍了多线程下载功能的实际原理、需要注意的问题和编码前的必做的一些准备工具类。最后开始正式编码,主要实现了一个简单的网络请求下载功能,涉及到网络请求接口DownloadCallback 和请求类HttpManager的配合使用。

2 . 下篇内容预告

纵观而言,这前两篇的文章难度不大,重点在基础的讲解学习,而第二篇的编码工作只是一个开始。在第三篇将学习多线程下载功能的核心实现、线程池原理和队列机制,期待下篇文章出炉~(源码正在整理中,后续会提供出来)

若有错误,欢迎指教~

希望对你们有帮助 :)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐