Retrofit2的再封装实战—多线程下载与断点续传(二)

Retrofit2的再封装实战—多线程下载与断点续传(二)_第1张图片
抱歉时间太长,最近实在是太忙了。

上篇文章Retrofit2的再封装实战—多线程下载与断点续传(一)中,介绍了项目的结构图,这次我们从程序入口DownLoadManager和实际下载类DownLoadTask开始。
我知道你们要的是代码

DownLoadManager

在开始DownLoadManager之前,我们要先明确一下下载回调和下载任务的数据结构。

一、下载任务数据结构

用什么样的数据结构来表达我们的下载任务?这里我选择使用List集合存储所有下载任务,每个任务是一个DownLoadEntity,url是下载地址,saveName是保存地址,目前你只需要关心这两个属性。

    public int dataId; 
    public String url;
    public long end;
    public long start;
    public long downed;
    public long total;
    public String saveName;
    public List multiList;
}```
####二、下载回调
在下载过程中,你会关心哪些下载状态?
1.开始下载:   用户触发下载条件后,在完成一系列任务(判断已下载数据是否完整,获取所有任务总长度,计算已下载百分比,创建下载任务)后回调百分比。简单说,在万事俱备那一刻回调。
2.取消下载:用户触发取消条件后回调(只回调一次)。
3.下载中:   触发条件并非每次I/O后,都会回调,为了节省资源,这里每下载1MB回调一次百分比(这个当然你可以自己设置)。
4.完成下载:一个下载请求的所有任务完成后回调。
5.下载出错:下载过程出现异常状态回调。
```public interface DownLoadBackListener {
    void onStart(double percent);
    void onCancel();
    void onDownLoading(double percent);
    void onCompleted();
    void onError(DownLoadEntity downLoadEntity,Throwable throwable);
}```
onError方法要拿出来单讲一下,一个下载请求可能会有几十个url地址,如果某个任务失败了,你会怎么做呢?我想你会单独拿出失败的url再单独请求一次下载,然后限定一个重复次数,比如10次,超过10次后仍然失败,你可能会提示用户下载失败。在这次封装中,你不必再考虑这些因素,因为已经帮你处理了失败情况,每个失败的url是会重新下载的,十次尝试机会,如果都失败了,才会进行onError回调。最后失败的下载实体和失败原因已经回调给你了,至于怎么处理,你自己来决定。

####三、下载入口
DownLoadManager做为下载的总入口,结合上面说的下载结构和回调,我们提供下载方法:
```public void downLoad(final List list, final String tag, final DownLoadBackListener downLoadTaskListener, final long multiLine) {    
    mExecutorService.submit(new Runnable() {
        @Override
        public void run() {
DownLoadRequest downLoadRequest = new              DownLoadRequest(mDownLoadDatabase,downLoadTaskLister, list, multiLine); 
         downLoadRequest.start();
         mDownLoadRequestMap.put(tag, downLoadRequest);
        }
    });
}```
List:整个请求的下载数据。
tag:因为我们要缓存每个请求的下载数据,使用tag来区别不同次请求,如果还不了解请浏览我的另一篇文章[《[Retrofit2的再封装实战—同步与异步请求》](http://www.jianshu.com/p/21fd4e468343)](http://www.jianshu.com/p/21fd4e468343),与文章中的Tag相同含义。
DownLoadBackListener:上面说的下载回调。
multiLine:多线程下载分割线,单位字节,程序默认使用多线程下载,分割线默认值是10 ✖ 1024 ✖ 1024字节,也就是10mb。比如一个url的大小是50mb,那么程序会自动把50mb分成5个10mb一起下载。如果你不想使用多线程下载,直接传0就好了;
当然,如果你想简单的使用默认值,程序还提供了对应的多态方法:

//默认支持多线程下载
public void downLoad(final List list, final String tag, final DownLoadBackListener downLoadTaskListener) {
downLoad(list, tag, downLoadTaskListener, MULTI_LINE);
}

上篇文章说过了,DownLoadManager有实现缓存的功能,我们使用
`private Map mDownLoadRequestMap = new ConcurrentHashMap<>();`
来记录下载任务,key就是tag,value是DownLoadRequest。同时提供cancel()方法,实现取消任务。具体实现和[《[Retrofit2的再封装实战—同步与异步请求》](http://www.jianshu.com/p/21fd4e468343)](http://www.jianshu.com/p/21fd4e468343)这篇文章思路一样,这里不多说。
细心的朋友可能已经发现在downLoad()方法中使用了mExecutorService线程池,在这里解释一下为什么要另开一个线程,其实就是为了处理上面所说的onStart回调之前那一系列操作所造成的主线程阻塞情况,在真正开始下面之前,我们要先拿到当前任务所有url的总长度(不然我怎么回调百分比呢?),大概思路是这样的,首先会迭代所有url,每个url先查询本地数据库,查看是否有当前url的任务记录,如果有,取出数据。如果没有,进行异步网络请求,获取下载长度。我们有个轮循机制,要等待所有url都查询到长度后,再开始下载。所以上面这一部分,一定是同步的!一定是同步的!一定是同步的!(当然所有获取url的网络请求是异步执行的)也就是说,我要等到所有的url都结束才能真正开始下载任务。如果你的下载请求,有近百个url,这一部分大概会耗时2~3秒,这短短的2~3秒对ui线程来说就是致命的,有洁癖的同学当然不能容忍啦!但是这里会出现个问题,downLoadTaskListener的所有回调现在都是在异步线程中的,至于怎么在异步线程中回调更新ui,这里不需要使用者再处理,程序中已经处理过了,怎么实现?使用一个获取主线程Looper的Handler就可以了,如果你看过Retrofit源码这点不会陌生,具体代码在DownLoadRequest里再给出。

说了这么多,下面放个使用的简单demo,DownLoadManager入口类的所有功能就介绍完毕了.
![demo](http://upload-images.jianshu.io/upload_images/3376157-aa0c19feacbcc400.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
##DownLoadTask
上文说过真正的下载任务是在DownLoadTask进行的,我们已经创建好了DownLoadService,只需要在Task中调用DownLoadService中的Api进行I/O操作就可以了,这里特别强调一下,下载地址URL和Task可以是一对一也可以是一对多的关系。
这里我们使用Builder模式创建Task实例:
```public static final class Builder {
    private DownLoadEntity mDownModel;
    private DownLoadTaskListener mDownLoadTaskListener;    
    public Builder downLoadModel(DownLoadEntity downLoadEntity) {
        mDownModel = downLoadEntity;
        return this;
    }
    public Builder downLoadTaskListener(DownLoadTaskListener downLoadTaskListener) {
        mDownLoadTaskListener = downLoadTaskListener;       
        return this;
    }
    public DownLoadTask build() {
        if (mDownModel.url.isEmpty()) {
            throw new IllegalStateException("DownLoad URL required.");
        }
        if (mDownLoadTaskListener == null) {
       throw new IllegalStateException("DownLoadTaskListener required.");
        }
        if (mDownModel.end == 0) {
            throw new IllegalStateException("End required.");
        }
     return new DownLoadTask(mTaskId, mDownModel, mDownLoadTaskListener);
    }
}```
(代码排版了半天,不知道中间为啥还是空这么大间隔。。。)
DownLoadTaskListener:这是每个Task的回调,不同于我们上面说的DownLoadBackListener,DownLoadBackListener是处理UI的回调,从某种意义上讲更像是总回调,而DownLoadTaskListener更多关注的是细节,是每个下载任务的回调,所以他更多关心的下载任务的本身:

public interface DownLoadTaskListener {
void onStart();
void onCancel(DownLoadEntity downLoadEntity);
void onDownLoading(long downSize);
void onCompleted(DownLoadEntity downLoadEntity);
void onError(DownLoadEntity downLoadEntity, Throwable throwable);
}```
回调方法和DownLoadBackListener基本一致,只是个别方法参数不同,onCancel onCompleted方法返回了DownLoadEntity实体,这些回调在DownLoadRequest中进行处理,最后再统一回调给DownLoadTaskListener。
DownLoadEntity:每个DownLoadEntity都是缓存在DB中的,结合上面给出的对象属性来看,url和saveName上面已经说过了,本处不再解释。
dataID:数据库主键,每个实体的id是唯一。属性的目的是缓存本地Map的DownLoadTask(这里比较绕,在DownLoadRequest里会解释)。
start:本次下载的开始位置
end:本次下载的技术位置
downed:已经下载字节数
total、multiList:举个例子来说 比较好理解 比如我们一个url有50mb,多线程下载会拆成5个DownLoadEntity,这五个实体就保存在multiList中,total值就是50mb,而不是10mb。这两个属性和下载是没有关系的。具体在DownLoadRequest中解释。

DownLoadTask 实现Runnable接口,我们主要来看run方法:

Retrofit2的再封装实战—多线程下载与断点续传(二)_第2张图片
run()

49行设置线程优先级为最高。
50-55行 调用我们上篇文章定义过的DownLoadService接口方法生成Retrofit Call,判断downed是否为0,如果是,则直接从start开始,不是不为0,开始位置就是downed+start。
下面的代码很好理解,拿到Call的Response取出响应体。63行执行I/O操作,66-75是对失败情况进行处理,并释放资源。来看关键的writeToFile方法:

Retrofit2的再封装实战—多线程下载与断点续传(二)_第3张图片
writeToFile-part1

这里的逻辑很简单,先判断文件是否存在,然后创建文件,88标记开始写文件位置,93-95设置文件读取缓冲区,相信对I/O操作熟悉的人,这里不会陌生,不熟悉的朋友请大家自行查询资料,这里不做解释。继续往下看:

Retrofit2的再封装实战—多线程下载与断点续传(二)_第4张图片
writeToFile-part2

这里我们做了优化,如果我们每写4096的字节,就回调一次,那未免太奢侈了,所以我们设定一个常量
private final long CALL_BACK_LENGTH = 1024 * 1024;
每1mb回调一次,为了统计每次回调前的下载量,我们定义属性
private long mFileSizeDownloaded;
105行 每次写完mFileSizeDownloaded+read;107-110行,如果当前mFileSizeDownloaded大于CALL_BACK_LENGTH,也就是说到达回调临界值回调onDowLoading方法,同时mFileSizeDownloaded置为0,mNeedDownSize属性是统计本次下载剩余字节,112-115行,如果剩余字节不足回调临界点,那么等下载完最后一字节,再回调。
Retrofit2的再封装实战—多线程下载与断点续传(二)_第5张图片
writeToFile-part3

122-130行 关闭资源 132 到结束是你需要处理的IO异常,这里需要根本个人的业务进行异常处理,也就是你需要定制的地方。取消线程时,会触发InterruptedIOException异常(不要问我为什么,线程的基础知识)。网络断开,触发SocketTimeoutException异常,这里我们的业务逻辑是只要不是用户取消,都认为是Error。下面给出不同状态回调代码;
Retrofit2的再封装实战—多线程下载与断点续传(二)_第6张图片
多状态回调

Tips

还有最后一篇文章就完结了,这篇文章陆陆续续写了将近两周了,质量我不是太满意。先把代码贴出来吧。本是想最后再给出来的,大家看不懂的对着代码撸一下吧。。。
希望喜欢的朋友帮我顶一下,如果使用中有bug欢迎反馈给我。
微信:hly1501
邮箱:[email protected]

你可能感兴趣的:(Retrofit2的再封装实战—多线程下载与断点续传(二))