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

抱歉时间太长,最近实在是太忙了。

上篇文章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<DownLoadEntity> 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<DownLoadEntity> 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<DownLoadEntity>:整个请求的下载数据。
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<DownLoadEntity> list, final String tag, final DownLoadBackListener downLoadTaskListener) {
downLoad(list, tag, downLoadTaskListener, MULTI_LINE);
}

上篇文章说过了,DownLoadManager有实现缓存的功能,我们使用
`private Map<String, DownLoadRequest> 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方法:

run()

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

writeToFile-part1

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

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行,如果剩余字节不足回调临界点,那么等下载完最后一字节,再回调。
writeToFile-part3

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

Tips

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容