前面两篇文章我们讲了项目整体的设计结构、入口类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。

这里需要注意的是:If-Range首部行必须与Range首部行配套使用。如果请求报文中没有Range首部行,那么If-Range首部行就会被忽略。如果服务器不支持If-Range,那么Range首部行也会被忽略。
好了,理论具备,只欠代码了。继续看queryDownLoadData方法,如果我们下载过此url,并且Modified不为空,调用接口来看看他是否更新过

@GET
Call 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都是一个线程,所以这里的计数,必须要考虑线程同步问题!这里我们选择使用synchronized。

整个queryDownLoadData就结束了,再回到start方法继续看,60-86行遍历所有下载任务,如果其中有total未获取到的任务(对应前面获取长度失败),那么直接返回错误,终止下载任务。如果都正常,叠加获得总下载值,如果总下载值=已经下载值,直接回调UI线程,已经下载结束了。88行,onStart()这时就已经回调给主线程下载百分比了,细心的朋友可能发现了,这是使用mMainThread回调UI线程,mMainThread是什么?看过Retrofit源码的朋友肯定不陌生,他的实现原理其实就是运用了拥有MainLooper的hander,因为我们的操作都是在异步线程中进行的,所以需要mMainThread是什么回调主线程(这个在之前已经讲过了),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是内存缓存,之前就提过了,我们用
Taskprivate Map<String, Map<Integer, Future>> mUrlTaskMap = new ConcurrentHashMap<>();
保存缓存信息,String是url,Map是当前url下的任务,为啥又用个Map?因为可能是多线程啊!Integer,下载任务的唯一ID,这里是数据库主键,Future不了解的同学请自行百度,这就是下载任务。
如果有下载记录,就找未完成的生成DownLoadTask, executeNetWork就是加入线程池。如果没有下载记录,就是新文件,createDownLoadTask创建下载任务。

createDownLoadTask

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

总结

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

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

  1. android 多线程下载,断点续传,线程池

    android 多线程下载,断点续传,线程池 你可以在这里看到这个demo的源码: https://github.com/onlynight/MultiThreadDownloader 效果图 这张效 ...

  2. 【Android】多线程下载加断点续传

    http://blog.csdn.net/smbroe/article/details/42270573 文件下载在App应用中也用到很多,一般版本更新时多要用的文件下载来进行处理,以前也有看过很多大 ...

  3. android多线程下载程序卡死,android 多线程下载与断点续传

    多线程下载: 下载速度更快,服务器对每个线程平分资源,故线程越多,得到的资源越多,下载速度越快. 断点续传: 下载中断,再次下载时从上一次下载结束的位置开始下载,防止重复下载 下载结束后 代码: pa ...

  4. android线程池断点续传,Android之多线程下载及断点续传

    今天我们来接触一下多线程下载,当然也包括断点续传,我们可以看到 很多下载器,当开通会员的时候下载东西的速度就变得快了许多,这是为什么呢?这就是跟今天讲的多线程有关系了,其实就是多开了几个线程一起下载罢 ...

  5. Android 多线程下载以及断点续传

    多线程下载 在日常开发中,我们不可避免的会接到类似这样的需求,下载一个比较大的素材文件或者安装包文件,以此实现APP的自动更新,APP内的素材替换等.由于一般此类文件都比较大,一般会在50M以上,如果 ...

  6. java httpclient 断点续传_【幻化万千戏红尘】qianfengDay27-HttpURLConnection,OkHttpClient,,多线程下载且断点续传基础学习:...

    课程回顾: Servlet:java语言开发的运行在服务器上的 开发步骤: 1.创建Servlet类 2.重写doGet或doPost方法 3.运行在服务器 生命周期: 1.初始化 2.服务 3.销毁 ...

  7. java多线程下载_Java实现多线程下载,支持断点续传

    多线程下载及断点续传的实现是使用 HTTP/1.1 引入的 Range 请求参数,可以访问Web资源的指定区间的内容.虽然实现了多线程及断点续传,但还有很多不完善的地方. 包含四个类: Downloa ...

  8. 【软件分享】免费多线程下载神器,可完全替代IDM(支持MacWindows)

    引言 提到下载软件,大家最常用的可能就是迅雷或者IDM了.笔者一直以来也都是用的IDM,IDM最核心的功能包括:多线程下载.断点续传以及网页资源嗅探等.但IDM是需要收费的,而且不支持Mac.所以今天 ...

  9. Linux运行指令(axel多线程下载工具)

    补充说明 axel 是Linux下一个不错的HTTP/ftp高速下载工具.支持多线程下载.断点续传,且可以从多个地址或者从一个地址的多个连接来下载同一个文件.适合网速不给力时多线程下载提高下载速度.比 ...

  10. Linux axel多线程下载工具

    wget 单线程下载工具 axel 能把我们的下载速度提到最高,对方服务器能承载的上线,或者你自己带宽的上线 Axel 是 Linux 下一个不错的轻量级高速下载工具,支持HTTP/FTP/HTTPS ...

最新文章

  1. 茅台App首发就登顶!单日下载量43万,甚至还没开始试运行
  2. Java第四次作业——面向对象高级特性(继承和多态)
  3. python语言属于哪一种语言_Python与Java:你应该学习哪种语言,他们有什么区别?...
  4. 机器学习算法总结--K近邻
  5. 什么是死锁及死锁的必要条件和解决方法【转】
  6. oracle11g trc 文件,Oracle11g11.2.0.1设置HuagePage导致TRC变大变多
  7. idea插件JRebel激活
  8. 利用AOP+Swagger注解实现日志记录功能
  9. 【web素材】04—40款个人主页简历网页模板及企业单页
  10. 八字起大运php代码,八字起大运方法有几种
  11. vue3 web项目引入高拍仪
  12. CN域名海外注册商体系(7)2010年4月2日资料
  13. 【密码学一】密码学概念、密码学初体验
  14. Day13 多态、Object、抽象、接口
  15. android 手机作为手写输入板
  16. 营业执照办理流程是怎样的?
  17. 安卓应用测试MonkeyMonkeyScriptMonkeyRunnerpyhton性能工具脚本
  18. 计算机怎么在表格里打勾,excel中怎么输入打勾符号在哪里,excel表格中怎么输入打勾符号...
  19. 如何用python在mysql上创建1亿条数据
  20. python编程输入文字并读出来_Python如何像scratch一样朗读文字?

热门文章

  1. Access 操作必须使用可更新查询
  2. Gartner发布2011年SIEM市场分析报告(幻方图)
  3. Android API 中文 (54) —— Filterable
  4. IT人士易犯4大职业病 鼠标手居第一位
  5. 几个非常实用性的在线学习Ethical Hacking的网站
  6. JM代码阅读之一SODB RBSP EBSP NALU
  7. 数据结构一些自己没搞懂的点
  8. 2010-2020年全国poi兴趣点
  9. VScode设置中文界面
  10. android把代码打包成sdk,基于Library去开发android SDK——sdk打包(示例代码)