造轮子之 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可以暂停,取消,恢复一个任务的执行
  • 任何情况下退出都应该能保存任务的下载状态

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

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

推荐阅读更多精彩内容