android 多线程异步下载文件,造轮子之 Android 多线程多任务断点续传下载器(设计篇)...

前段时间面试,被问到 app 的自动更新是怎么做的,文件下载怎么实现的?用了多线程吗?是否支持断点续传?一下蒙逼,因为直接用第三方框架实现的文件下载,这些问题完全没想过。

回来后觉得这里面其实涉及很多知识点,就打算自己动手封装一个支持多线程多任务断点续传的库,用了一个星期的业余时间,目前主要功能基本完成,所以记录一下这个过程中遇到的问题和收获

1. 涉及知识点

听起来并不复杂的一个功能,但是实际动手做起来发现还是涉及了很多知识点的,概括一下主要涉及以下几个部分:

HTTP 请求:为了尽量的减少对第三方框架的依赖,这个库里的 HTTP 请求部分就直接用 HttpURLConnection 来实现

断点续传:主要借助 RandomAccessFile 这个类来实现,这个类可以从文件的任意指定位置开始进行读写

多线程下载:涉及到多个子线程的同步,中断异常的处理,线程池的使用等

多任务下载:这里涉及到任务的调度和同步,比如限制同时下载的任务数,达到这个上限以后,再添加任务要等待,如何处理。暂停或者取消一个任务后如何自动启动等待的任务等。这里使用了阻塞队列和信号量来实现任务的调度

事件的发布:可以用广播, EventBus,Handler,回调

数据的持久化:退出程序后,保存下载任务的状态,再次打开后加载所有的任务,并能够继续下载。这里可以用数据库,临时文件 或者 SharedPreference 实现。

2. 整体设计

1. 下载请求的封装

用一个 JavaBean 类封装一个下载请求,包括了下载的 url 地址,文件保存的目录以及文件名,基本的参数其实就这三个,还可以根据需要添加更多的配置参数,比如指定并发的线程数等。如果参数比较多的情况,可以使用 Builder 模式

2. 任务的调度

回想我们使用迅雷下载时,填入下载链接,选好保存路径之后点击开始,任务就自动开始下载了。如果此时任务数已经达到上限,那么就会等待,直到有任务结束,再自动开始等待的任务。仔细思考之后,有以下几个要点:

因为添加任务是在主线程进行,所以应该是非阻塞的,任何时候执行添加一个任务都应该立即返回。所以这里考虑使用一个无上限的阻塞队列 LinkedBlockingQueue

任务一旦添加就自动开始,这里参考了 Volley 的 NetworkDispatcher 的设计,开启一个专门的任务调度线程,用一个死循环不断的从阻塞队列取出任务来执行,当队列为空时就阻塞,非空时就被唤醒并执行任务。其实就是一个典型的生产-消费模型

达到最大任务数后要等待有任务停止(包括成功,失败,暂停,取消几种情况)才能开始,这里很自然的想到用 Semaphore 来实现,当从阻塞队列取出一个任务后,还需要先成功获取一个信号量,才能继续开始执行,否则就阻塞。当任务停止的时候,释放一个信号量,之前等待的任务就可以自动开始执行了。

当然这里也可以不用信号量,通过一个计数器加一个等待队列实现调度。一个任务结束后,需要检查当前正在下载的任务数,以及是否有任务在等待队列,如果有并且计数器值小于上限值,就从等待队列取出一个任务执行。个人感觉使用信号量在概念上更加清晰。

3. 下载任务的执行

一个支持多线程断点续传的任务开始后,其实是分成了串行的两步执行的:

(1) 发起一次 HTTP 请求,获取下载文件的长度信息

(2) 根据文件的长度以及设置的线程数N,把下载任务分成N个子任务,每个子任务再分别发起HTTP请求,负责下载自己那一部分的数据并写入同一个文件中(RandomAccessFile 已经处理了同步问题)。

所以这里我的设计是先使用一个 AsyncTask 获取文件长度,再异步的回调里,开启N个子任务线程进行下载。

这里当然是使用线程池来执行子任务了,子任务都实现 Runnable 丢到线程池里。另外由于 AsyncTask 默认的实现是串行的,也可以让 AsyncTask 在默认的线程池上执行,这样就可以实现多个任务同时开始下载了。

4. 下载任务的封装和管理

首先要用一个类来描述一个下载任务,这个类的设计要考虑以下几点:

每一个下载任务和一个下载请求一一对应,所以下载任务中应该包含一个下载请求的字段

每个任务需要一个唯一的ID,这里考虑使用url+保存路径+文件名的字符串进行MD5运算,来作为一个任务的ID

需要记录下载文件的大小

需要一个字段标示当前任务所处的状态,比如正在下载,暂停,失败等,操作该字段需要同步

需要一个字段标示当前任务已经下载的字节数,操作该字段也需要同步

需要一个List字段保存已经开始下载的任务的子任务的信息,每个子任务中保存当前写入文件的位置以及结束写入的位置

然后就是需要一个集合来保存所有的已添加的任务,因为各种对任务的操作,比如暂停,取消,删除等都是要根据ID来找到一个对应任务,所以使用Map来保存可以保证查找的效率。

5. 事件发布设计

所有的事件都通过 LocalBroadcastManager 发布,然后使用者可以有两种方法实现对事件的监听,一种是定义自己的 Receiver 接收处理各种广播事件。还有一种是注册 Listener,然后我们在框架内部实现一个 BroadcastReceiver,根据不同的事件调用 Listener 的不同的方法,这样封装的更好,不过某些场景自己注册Receiver还是更灵活一些,可以在 switch 里面对多个 case 合并处理

6. 任务状态的切换

最主要的部分就是如何暂停或者取消一个正在进行的任务。在下载的子任务线程里,会有一个循环从InputStream读取数据并写入文件的操作,我们就在这个循环这里加入对任务状态的判断,当状态是Downloading时,就继续下载,当状态被设为 Paused 时,就跳出循环,这样就实现了任务的暂停。

当然也可以用 FutureTask.cancel(),在循环里判断 isInterrupted() 来实现,不过因为我们已经有了一个表示任务状态的字段,直接使用这个字段可以达到同样的效果。

当恢复一个暂停的任务时,不能让它直接开始,要把重新加到任务队列里面去,然后等待调度。因为可能已经达到任务上限,所以还是要重新拿到信号量才可以开始。

7. 任务的持久化

不考虑大量任务管理的场景的话,可以直接用 SharedPreference 配合 Gson 的序列化和反序列化,实现任务的持久化。用数据库的话就麻烦一点,要自己读写各个字段,当然也可以用 GreenDao,Realm 等orm框架,不过作为一个实验性项目,这块暂时先不做那么复杂吧。

3. 总结

初步的分析结束,整体的思路已经清楚了,主要的难点应该是在任务的调度,多线程的协作和同步。最后从用户的角度总结一下最终要实现的功能:

定义一个下载请求并加入下载队列,获得任务的ID以便后续的操作。任务自动开始下载,如果达到上限就等待

通过任务ID可以暂停,取消,恢复一个任务的执行

任何情况下退出都应该能保存任务的下载状态

下一篇就写具体的代码实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值