美文网首页Retrofit 2.0Android开发Android-Rxjava&retrofit&dagger
Retrofit2的再封装实战—多线程下载与断点续传(二)

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

作者: Leinyo | 来源:发表于2016-11-12 13:59 被阅读9271次
    抱歉时间太长,最近实在是太忙了。

    上篇文章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:https://img.haomeiwen.com/i3376157/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

    相关文章

      网友评论

      • 641cc18205d7:那个,大牛,如果我想将retrofit和rxjava连用怎么提高性能呢?您说的request里面的优化就办法做了。而且跟现在的代码改动也挺大的,框架设计不出来了,发愁。。。。
        Leinyo:@杨玉安 你只要把call换成rx就可以了 其他的其实没必要改动
      • weifucheng:多线程下载提高网速,只有在源带宽小于下载带宽的时候才有用吧,这种情况并不多
      • Cyandev:多线程下载中线程优先级没有卵用吧,纯 I/O 操作把 QOS 设多高网速也不能提升啊……
      • Clendy:学习了~~

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

      本文链接:https://www.haomeiwen.com/subject/fqhwuttx.html