美文网首页Android网络请求开源库Retrofit 2.0Android开发
Retrofit2的再封装实战—多线程下载与断点续传(三)

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

作者: Leinyo | 来源:发表于2016-12-01 18:02 被阅读6983次
    终结篇

    前面两篇文章我们讲了项目整体的设计结构、入口类DownloadManager、下载类DownloadTask,这篇文章我们讲最重要的类DownLoadRequest。
    由于离前两篇文章时间比较长了,感觉陌生的同学可以先回顾一下:
    Retrofit2的再封装实战—多线程下载与断点续传(一)
    Retrofit2的再封装实战—多线程下载与断点续传(二)

    流程图


    回忆之前文章提到的,我们将需要下载的任务构造成一个List传入DownLoadManager中,DownLoadManager调用方法downLoad生成DownLoadRequest对象,同时将List参数代入,最后调用downLoadRequest.start()方法。

    一、Start

    start

    我们将下载的部分操作封装成DownLoadHandle对象,59行我们调用queryDownLoadData方法,对应上面结构图的查询下载总长度步骤,这是一个耗时操作,不用担心,我们在之前的DownLoadManager中已经创建线程了,这里面的所有操作都是在子线程中进行的,UI线程是不会被阻塞的。
    queryDownLoadData:

    //汇总所有下载信息
    List<DownLoadEntity> queryDownLoadData(List<DownLoadEntity> list) {
        final Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            DownLoadEntity downLoadEntity = (DownLoadEntity) iterator.next();
            downLoadEntity.downed = 0;
            Call<ResponseBody> mResponseCall = null;        
            List<DownLoadEntity> dataList = mDownLoadDatabase.query(downLoadEntity.url);
            if (dataList.size() > 0) {
                downLoadEntity.multiList = dataList;
                if (!TextUtils.isEmpty(dataList.get(0).lastModify)) {
                    mResponseCall = NetWorkRequest.getInstance().getDownLoadService().getHttpHeaderWithIfRange(downLoadEntity.url, dataList.get(0).lastModify, "bytes=" + 0 + "-" + 0);
                }
            } else {
                mResponseCall = NetWorkRequest.getInstance().getDownLoadService().getHttpHeader(downLoadEntity.url, "bytes=" + 0 + "-" + 0);
            }
            executeGetFileWork(mResponseCall, new GetFileCount(downLoadEntity, mResponseCall));
        }
        while (!mGetFileService.isShutdown() && getCount() != list.size()) {
        }
        return list;
    }
    

    迭代List,先在数据库中查询当前任务的url,如果查询结果大于0,说明我们曾经下载过此url,将dataList赋值给multList,下面介绍一个概念。如果我们下载过一个文件,但是服务器将这个文件的内容置换掉了,客户端如何判断下载文件的时效性?


    request

    http请求头中有个If-Range属性,下面摘自网络上解释:

    If-Range是另一个起条件判断的请求头(我们之前讲过If-Match/If-None-Match,If-Modified-Since/If-Unmodified-Since).If-Range头用来避免客户端在下载了某资源(比如图片)的一部分后,下次重新下载又从头开始下载。使用If-Range之后,客户端每次可以从上次下载的部分之后继续开始下载。
    If-Range的使用格式为:If-Range: Etag|Http-Date也就是说If-Range后面可以使用Etag或者Last-Modified返回的值:
    If-Range: "df6b0-b4a-3be1b5e1"
    If-Range: Tue, 8 Jul 2008 05:05:56 GMT
    逻辑上来讲,上面2种方式分别和If-Match,If-Unmodified-Since的工作原理一样,他们的值正是服务器返回的Etag和Last-Modified值。

    初次接触你可能是蒙圈的,没关系,这里举例来说明一下,我下载过一个文件A,这是http的response头信息:


    response

    Last-Modified,直观上很清晰他是一个关于时间戳的属性。他代表着文件最后修改时间,我们需要做的就是保持这个字段到本地,下次请求时候赋值给If-Range头信息,服务器会告诉你这文件是否更新过。怎么判断?

    如果请求报文中的Last-Modified与服务器目标内容的Last-Modified相等,即没有发生变化,那么应答报文的状态码为206。如果服务器目标内容发生了变化,那么应答报文的状态码为200。

    好了,理论具备,只欠代码了。继续看queryDownLoadData方法,如果我们下载过此url,并且Modified不为空,调用接口来看看他是否更新过
    @Streaming @GET Call<ResponseBody> getHttpHeaderWithIfRange(@Url String fileUrl, @Header("If-Range") String lastModify, @Header("Range") String range);
    和我们之前的downloadFile方法差不多,这里不多解释。继续看,如果没下载过,直接调用getHttpHeader方法,不需要If-Range头。
    executeGetFileWork方法很简单只有两行代码:

    private void executeGetFileWork(Call<ResponseBody> call, GetFileCountListener listener) {
        GetFileCountTask getFileCountTask = new GetFileCountTask(call, listener);    
        mGetFileService.submit(getFileCountTask);
    }
    

    GetFileCountTask,看名字就知道了,创建获取文件长度的任务,然后加入线程池。
    GetFileCountListener查询结果回调:

    public interface GetFileCountListener {
        void success(boolean isSupportMulti, boolean isNew, String modified, Long fileSize);
        void failed()
    }
    

    很简单两个方法,成功和失败。GetFileCountTask中通过response的返回报文,判断是否支持多线程下载,是否更新过,modified值,下载长度,代码很简单这里就不贴了,感兴趣的同学自己撸代码看吧。下面看GetFileCountListener回调:


    GetFileCountListener回调

    先看失败 如果重试次数小于0,停止所有任务,如果未到0,则重新尝试获取长度,重复次数默认为3次。


    成功后赋值mDownLoadEntity相关属性,93-108行,如果未更换文件,判断下载文件还是否存在,存在说明只要下载剩余任务就可以了,不存在,当新任务对待。
    setCount方法结合queryDownLoadData最后的while循环看,有个全局变量记录任务的完成数,每个url任务完成或者失败后count +1,如果未完成任务,或者线程池未被关闭则一直循环等待。
    这里提醒下:尤其每个task都是一个线程,所以这里的计数,必须要考虑线程同步问题!
    整个queryDownLoadData就结束了,再回到start方法继续看,60-86行遍历所有下载任务,获得总下载值,如果总下载值=已经下载值,直接回调UI线程,已经下载结束了。87生成下载总回调,我们知道一个url是一个线程,一个线程对应一个自己的回调,那么每个线程的回调,统一汇聚到下载总回调,只有这个回调负责和UI接口通信。
    一张图可能更能说明:

    回调结构图

    从下向上看,UI回调和总回调1对1关系,总回调里有UI回调引用,总回调和每个Task的回调,1对多关系,每个Listener中有总回调引用。
    现在从上向下看,Listener下载了1MB,告诉总回调:“你可以给UI回调了”,UI回调就老老实实告诉UI我下载了1MB了。简单的说,总回调就是一个代理类。

    二、AddDownLoadTask

    我们还差什么?入口类完成了,真正的下载类完成了,下载之前的巴拉巴拉已经完成了,那就只差下载任务了对不对?下面就真的easy了。

    private void addDownLoadTask(DownLoadEntity downLoadEntity) {
        Map<Integer, Future> downLoadTaskMap = new ConcurrentHashMap<>();
        MultiDownLoaderListener multiDownLoaderListener = new MultiDownLoaderListener(mDownCallBackListener);
        if (downLoadEntity.multiList != null && downLoadEntity.multiList.size() != 0) {
            for (int i = 0; i < downLoadEntity.multiList.size(); i++) {
                DownLoadEntity entity = downLoadEntity.multiList.get(i);            
                //当前分支是否下载完成
                if (entity.downed + entity.start > entity.end) {                continue;
                }
                DownLoadTask downLoadTask = new DownLoadTask.Builder().downLoadModel(entity).downLoadTaskListener(multiDownLoaderListener).build();
                executeNetWork(entity, downLoadTask, downLoadTaskMap);
            }
        } else {
            //文件不存在 直接下载        
            createDownLoadTask(downLoadEntity, NEW_DOWN_BEGIN, downLoadTaskMap, multiDownLoaderListener);
        }
    }
    

    map是内存缓存,之前就提过了,我们用
    //URL下载Taskprivate Map<String, Map<Integer, Future>> mUrlTaskMap = new ConcurrentHashMap<>();
    保存缓存信息,String是url,Map<Integer, Future>是当前url下的任务,为啥又用个Map?因为可能是多线程啊!Integer,下载任务的唯一ID,这里是数据库主键,Future不了解的同学请自行百度,这就是下载任务。
    如果有下载记录,就找未完成的生成DownLoadTask, executeNetWork就是加入线程池。如果没有下载记录,就是新文件,createDownLoadTask创建下载任务。

    createDownLoadTask
    127-141 如果下载任务大于多线程下载的分割值,切成多段进行下载。else 单线程下载。
    好了 大概的流程到这里就结束了,还差什么?Task任务回调,主线程回调,这些代码没有贴出来,大家自己去发现吧。这里用了代理模式,还有很多的多线程数据安全方面的代码。下载Error重置下载机制,判断下载是否真正结束机制。对缓存的操作,map套map的增删改查。

    总结

    到这所有的多线程下载和断点续传就结束了,其实写作过程是痛苦的,但是到结束还是很欣慰的,相信您从开始看到这篇结束,整个项目的流程您是了解的,怎么定制,怎么修改bug应该也没有问题了,毕竟思路有了,就差不停的实践了,对吗?
    我希望这篇文章再思路上可以帮助到您,那也是我的初衷啊!
    下篇文章我会整理封装的支持上拉,下拉,可以添加Head的RecycleView。
    最后,感谢私信过我,鼓励过我,打赏过我的朋友,谢谢你们的支持。
    GitHub地址
    我希望大家可以积极fork,一起修改,如发现问题,欢迎反馈。
    微信:hly1501

    相关文章

      网友评论

      • 164cbab4696b:我看不懂,是不是太菜了 为什么调用传的是一个集合, 能具体写个RecyclerView的demo吗
        164cbab4696b:你github里RetrofitDownload这个项目的easyretrofit文件夹是空的,把lib和demo分开出来好别扭。
        164cbab4696b:@Leinyo 好的,谢谢
        Leinyo:@夜光寒风 加了单实体的下载 你更新一下
      • ZapFive69:好东西
      • BKQ_SYC:看了几天,发现了一个问题。首先设定一个情景:
        文件下载到一半(已开始下载,但未下完状态),此时取消下载。回到手机文件删除下载未完成的文件。然后再次点击下载,这个时候应该是重新创建下载,但是数据库delete方法执行不完全,downloadentity的multiList没清除,然后这个时候走的时继续下载那条路,而不是重新下载。问题在delete方法或者DownLoadRequest的addDownLoadTask方法的第一个if判断
        Leinyo:@frag 谢谢 已修复
        BKQ_SYC:@深刻的猴子 恩,好的
        Leinyo:@frag 先谢谢你 我现在没有时间 等有时间会看一下
      • BKQ_SYC:代码呢?关键代码没看到啊,,,好想看看 :dizzy_face:
        Leinyo:@frag 在另一个项目里呢
        BKQ_SYC:@深刻的猴子 我看你git上面的项目关键代码在那个module里面,但是那个里面是空的额,整个项目就是一个Activity和一个Application,难道是我没找着么
        Leinyo:@frag 啥关键代码?

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

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