Android下载器开发
2015-07-18
Android下载器开发
最近因为项目需要,我开发了一个Android下载管理器,我觉得很有必要记录一下,谈谈我是怎么开发的,遇到了什么问题以及如何解决的。算是一个总结吧。
需求分析
我们项目包含了一个功能:将文件下载手机上,并且要支持断点下载,因为我们下载的文件主要是图片和视频,其中视频基本上都是大文件,然后下载可能随时会被中断,所以首要一点就是要支持断点续传,然后要支持任务管理,例如暂停下载和继续下载,再然后就是要下载速度尽量快。不过这最后一点估计所有人都会这样要求。
为什么要自己开发
需求已经较清楚了,然后再看怎么实现。一般来说,像下载这么通用的功能,因为很多开源库可用才对。不过在开始之前我要说明一下,这个功能一开始不是我做的,是另外一个同事负责的,但是他离职了,我来接手。问题在于他离职之前这个功能算是实现了,但是很多问题,例如断点续传有问题,任务管理有问题,下载速度很慢。我接手之后,我特意去网上查找了有没有可用的较好的开源库。经过一轮搜索之后,我发现,很多博客上介绍的下载都是同一下载,就是黎活明老师的下载,估计这个下载是他讲课的案例,主要特点是多线程断点下载。而我们之前那个同事的实现就是把网上的例子弄来的。他这个下载确实还可以,支持断点下载,还支持多线程分片下载来提高下载速度。不足之处就是不支持任务管理,还有数据库很容易出现多线程问题。也就是说核心的东西它有了。如果你要在一个项目中用它,然后还有通过测试同事的各种测试,那是不行的。还有很多工作要做。
再说说我找库的问题,下载库到是找到了几个,但是发现都有点小问题。例如有一个貌似是阿里巴巴的技术人员写的,支持断点下载和任务管理,但是不支持多线程分块下载。然后我发现很少外国人有写下载库,我的想法是 是不是因为他们的网速比较好,下载文件都比较快,像断点续传和多线程分块下载,任务管理需求也比较少(也比较少下大文件,因为他们不能下载视频)。所以我最终决定自己来实现一个下载器。
实现
1断点续传
第一个要点是断点续传。这个不难,因为这是HTTP协议支持,通过range这个header字段。这个字段可以指定要从文件的哪个地方开始传,到哪里结束。下载的部分要写入文件,这里用到了一个Java类:RandomAccessFile。这个类你可以指定创建一个指定大小的文件,然后随机读写任何部分。断点下载一个要点就是,在下载之前,我们要先创建一个指定大小的RandomAccessFile文件,然后再把下载下来的部分写入这个文件中。已下载的部分要记录到配置文件或者数据库中,下载要从哪里下载就从配置文件中读取。这里要注意的就是每次将下载部分写入文件的时候,同时也要保存已下载到哪里的信息。这里的额外的保存操作会拖慢一点下载速度,但是为了避免程序被强杀或者突然崩溃导致已下载部分丢失,也只能这么做。如果为了追求速度,可以容易这种异常情况的丢失,这里是可以改进的。
多线程分块下载
为了加快速度,我们可以将一个大文件分成几块,每块分配一个线程来下载,这样速度就翻倍了。这里就有一个要点:我们将文件分成N块,然后这每一块都按照断点续传来实现。所以基本的下载流程是这样的:首先获取文件的大小,然后在本地创建一个该大小RandomAccessFile文件,然后分成N块来下载,设置好每一块的下载起始位置和结束位置,分配一个线程来下载,而且每一块的下载都要几好下载到哪里,以便以后支持继续下载。要判断文件是不是下载完成了,只要检查每块是不是已经正常下载完成或者已下载的部分就是等于文件大小,就可以了。
任务管理
任务管理就是你可以随时中断下载,可以创建多个任务,同时可能只支持X个文件进行下载,其他的任务等待。然后失败的任务可以重启,也可以对任务进行删除。这个倒不是很难,但是如果要加入任务优先级,会复杂很多。现在我的实现是没有优先级的,先进先出机制。不过我已预览这个设计,要扩展起来也很容易。任务管理实现起来不难,就是有一个线程池来下载,下载中和等待中的任务加入线程池,当任务被暂停时,如果是正在下载,就cancel掉线程,如果是等待中,就设置cancel标记。删除和暂停差不多,只是要对数据库进行删除操作。这里对正在下载的任务停止有一个要点。有很多人对停止线程掌握的不好。大部分人都知道设置一个布尔标记,当这个标志为true时就不做任务了,这样线程的run方法可以结束了。问题是很多人不知道这个布尔变量是有要求的。我之前的那个同事就犯了个错误。他发现把那个标记设置为true时,线程不能立即结束。他就在线程里加了个sleep方法,然后进入判断这个标示的逻辑。这样做可以起到一点的作用,但是还是不能解决根本问题,而且会导致无需的程序睡眠时间,导致下载速度变慢。解方法很简单,对这个布尔值变量声明时用volatile修饰就可以了,而且不用在线程里sleep。
遇到的问题
前面说了前同事的实现有问题,断点下载有问题,我检查代码时发现是因为它的数据库有问题,下载的时候没有把已下载的进度写到数据库。他采用的一个数据库框架叫litepal。这是一个对象型数据库,我并不反对用框架,就沿用了他的做法,修改了他的bug。然后修改了他停止线程的bug,基本上就能实现断点续传的功能,并且因为线程少了sleep方法,速度有了明显提升。
对速度提升的优化
经我的研究,下载速度主要受限于网络,还有下载你设置的缓冲区buffer的大小。当然网络是前提,网络不好,你设置的下载buffer再大也没有用。但是我们的应用因为是局域网内,网络不是问题。所以这个时候buffer的大小就起作用了。当初我看很多的下载例子他们设置的buffer是8KB,我跟其他讨论之后,设置512KB的大小,速度果然提升不小。然后我们配置了每个文件分两块进行下载,总速度可达10MB每秒。如果还有提升的话,可以再加大这个buffer,还可以从优化每个线程下载时写文件之后更新数据库那个操作。
HTTP协议content-length问题
测试同事发现下载大文件有问题。我一看那个失败的文件是超过2GB。第一反应是是不是整数溢出。因为Java中int类型是不超过2GB的(Java的都是有符号数)。我一看在获取文件大小的时候,协议返回的竟然是0,然后发现Android的HTTP协议getContentLength返回的是一个int类型!。我通过断点调试发现服务器返回的协议的文件大小是正确的,只是在经过getContentLength获取的值就变了,因为溢出了。所以我改成自己去从header里解析这个contentLength。这下获取的文件大小就正常了。
Android4.4系统写大文件bug
改正了getContentLength的问题后,下载还是失败,我就纳闷了。然后跟踪发现异常发生在RandomAccessFile的setLength方法。我就到网上搜这个异常,找了很多终于在Google groud发现有人也遇到这个问题,说事Android4.4系统的一个bug。Google的人也回应了,是在SD卡相关的类中的一个bug,没有用的是32位的方法,他们已修复这个bug。搞了半天,原来是系统的bug,然后我然测试同事测试了替他系统,都没有问题,确实只有4.4才有。而且发现了更奇怪的问题,下载4GB的文件竟然可以!我至今还没搞清楚这是啥原因。知道是系统问题后,将这个问题先是放了一段时间,后来我想不能下载对用户来说确实不好,有没有什么办法可以绕过这个bug。所以我针对这一种特殊的情况,采用另外一种写文件的方式,用FileChannel来建立一个指定大小的文件,这种方法比RandomAccessFile的setContentLen方法效率低很多,但是总比不能用好吧。至此这个bug告一段落。
其他问题
我们在下载文件之前是先要在磁盘创建一个同样大小的文件,然后下载时就是写入到这个文件中。如果你要下载A.MOV文件,网上的很多下载例子都是直接建立一个A.MOV文件。这样是不好的,因为如果用户用别的文件管理应用去查看文件的时候,看到这个文件就想消费,可实际上这个文件可能根本没有下载完,是消费不了的。所以通常的做法就是开始创建的是A.MOV.TMP文件,等真正下载完之后再重命名为A.MOV。
还有另外一个问题就是,测试同学很喜欢做压力测试,他们会疯狂下载文件,最终磁盘满了,就出现问题了。虽然我一早就做了错误提示,也定义各种可能出现的错误提示。但是有些系统在SD卡满了之后会卡死,特别是小米的系统。我一开始以为是我的程序问题造成系统卡死,经过研究之后我发现是系统问题,而且不同的系统不同反应。我测试发现一般留足20MB空间就不会有太大的问题。所以在每次问下下载之前,我都检查一下是否有足够的空间可以下载,这里预留了额外的20MB空间。
总结
这一次写下载器给我的收获很多。从前同事的一堆烂摊子开始,bug无数,到我接手之后功能完善,各种bug解决,我自己还是比较满意的。我觉得你做事的态度决定了你能把事情做成怎样。不过这个库也还有其他问题,例如还没有任务优先级,速度应该还可以提升。现在暂时够用,就告一段落了。我已经将这块独立成一个库,放到了github上面。希望有需要的人可以用它,并且改善它。
以下是github地址,欢迎使用。
Category: Android Tagged: Android Download