多线程下载

在日常开发中,我们不可避免的会接到类似这样的需求,下载一个比较大的素材文件或者安装包文件,以此实现APP的自动更新,APP内的素材替换等。由于一般此类文件都比较大,一般会在50M以上,如果我们不对下载的进度进行记录的话,那么对于用户的流量的损耗和体验,都是比较糟糕的。所以我们自然而然的就会想到断点续传,同时为了充分压榨用户的带宽,使一些文件能够尽快的下载完成,那么我们也可以使用多线程同时下载的技术加快文件的下载速度。

举个例子,我们要从一个水缸中用抽水机通过水管抽水,由于管子的直径等等的限制,我们单条管子无法完全利用我们的抽水机的抽水动力。因此我们就将这些抽水的任务分成了多份,分摊到多个管子上,这样就可以更充分的利用我们的抽水机动力,从而提高抽水的速度。

因此,我们使用多线程下载的主要意义就是——提高下载速度。

多线程下载的原理

简单来讲,多线程下载原理其实就是讲一个文件逻辑区分了几块,每个线程分别独立地下载自己负责的区块。

所以我们可以简单地讲一个文件的大小平均分成几份即可。

既然要分配文件的区块,那么我们就要知道文件的总大小,文件的总大小我们可以通过网络请求进行获取,在 Response Headers 中的 Content-Length 字段。也就是该文件的大小,单位是字节。

简单抽象出来

    /*** 获取需要下载链接的文件长度* @param url 链接*/@WorkerThreadfun obtainTotalSize(url: String): Long

获取文件指定区域的内容

任务分配我们已经了解了,看起来很理想,但有一个问题,我们如何实现向服务器只请求这个文件的某一段而不是全部呢?

我们可以通过在请求头中加入 Range 字段来指定请求的范围,从而实现指定某一段的数据。如:RANGE bytes=10000-19999 就指定了 10000-19999 这段字节的数据所以我们的核心思想就是通过它拿到文件对应字节段的 InputStream,然后对它读取并写入文件。

抽象出来即:

    /*** 获取文件分段内的内容* @param url 链接* @param start 开始位置* @param end 结束位置* @return 输入流*/@WorkerThreadfun obtainStreamByRange(url: String, start: Long, end: Long): InputStream?

文件的指定位置的写入

获取到对应的内容,那么我们就要在文件的指定区域去写入,由于我们是多线程下载,因此文件并不是每次都是从前往后一个个字节写入的,随时可能在文件的任何一个地方写入数据。因此我们需要能够在文件的指定位置写入数据。这里我们用到了RandomAccessFile 来实现这个功能。

RandomAccessFile 是一个随机访问文件类,同时整合了 FileOutputStream 和 FileInputStream,支持从文件的任何字节处读写数据。通过它我们就可以在文件的任何字节处写入数据。

接下来简单讲讲我们这里是如何使用 RandomAccessFile 的。我们对于每个子任务来说都有一个开始和结束的位置。每个任务都可以通过 RandomAccessFile::seek 跳转到文件的对应字节位置,然后从该位置开始读取 InputStream 并写入。这样,就实现了不同线程对文件的随机写入。

断点续传

那么文件的写入搞定了,那么就剩下最后一个文件,如何实现断点续传。这里其实我们可以记录每一次写入文件的进度,当下载任务被暂停的时候,我们就将对应的任务记录下载,记录相应的url,存储文件,当前下载的进度等基本信息,当用户再次出发的时候我们就可以从这些信息恢复进度,继续下载。

简单来讲,只需要我们将对应的任务进行持久化即可。

代码实现

首先我们需要定义任务的下载的各个阶段的状态。

/*** Author: huangtao* Date: 2022/12/27* Desc: 下载状态枚举*/
enum class DownloadStatus {/*** 空闲,默认状态*/IDLE,/*** 完成*/COMPLETED,/*** 下载中*/DOWNLOADING,/*** 暂停*/PAUSE,/*** 出错*/ERROR
}

对应的实体类

data class SubDownloadModel(//下载路径val url: String,//子任务的大小val taskSize: Long,//开始位置val startPos: Long,//结束位置val endPos: Long,//当前位置var currentPos: Long = startPos,//保存的文件路径val saveFile: String
){/*** 当前任务的状态*/@Volatilevar status: DownloadStatus = DownloadStatus.IDLE/*** 已经下载的大小*/@Volatilevar completeSize = 0L
}data class DownloadModel(//链接val url: String,//保存路径val savePath: String,
) {/*** 完成大小*/@Volatilevar completeSize: Long = 0internal set/*** 文件总大小*/var totalSize: Long = 0internal set/*** 状态*/var status: DownloadStatus = DownloadStatus.IDLEinternal set
}

相应的监听回调

/*** Author: huangtao* Date: 2022/12/27* Desc: 下载的事件监听*/
interface DownloadListener {/*** 开始下载*/fun onStart() {}/*** 下载中* @param progress 进度 字节* @param total 总数 字节*/fun onDownloading(progress: Long, total: Long) {}/*** 暂停*/fun onPause() {}/*** 取消下载*/fun onCancel() {}/*** 下载完成*/fun onComplete() {}/*** 出错* @param msg 错误信息*/fun onError(msg: String) {}
}

Http的抽象辅助类


/*** Author: huangtao* Date: 2022/12/27* Desc: 下载的网络请求接口定义*/
interface DownloadHttpHelper {/*** 获取需要下载链接的文件长度* @param url 链接*/@WorkerThreadfun obtainTotalSize(url: String): Long/*** 获取文件分段内的内容* @param url 链接* @param start 开始位置* @param end 结束位置* @return 输入流*/@WorkerThreadfun obtainStreamByRange(url: String, start: Long, end: Long): InputStream?
}

持久化的辅助类

/*** Author: huangtao* Date: 2022/12/27* Desc: 下载的db存储接口定义*/
interface DownloadDbHelper {/*** 删除一个任务* @param model 下载子任务*/fun delete(model: SubDownloadModel)/*** 添加一个子任务* @param model 子任务*/fun insert(model: SubDownloadModel)/*** 更新一个任务* @param task 子任务*/fun update(model: SubDownloadModel)/*** 根据url查询相关任务* @param url 下载链接* @return 相关子任务 无:返回空列表*/fun queryByTaskTag(url: String, saveFile: String): List<SubDownloadModel>
}

一些通用的配置

/*** Author: huangtao* Date: 2022/12/27* Desc: 下载配置接口*/
object DownloadConfig {const val TAG = "DownloadManager"/*** 上下文*/lateinit var context: Applicationprivate set/*** db实现*/var dbHelper: DownloadDbHelperprivate set/*** http下载实现*/var httpHelper: DownloadHttpHelperprivate set/*** 线程数*/var threadNum: Intprivate set/*** 线程池*/var mExecutorService: Executorprivate setinit {threadNum = 4dbHelper = DownloadDbImpl()httpHelper = DownloadHttpImpl()mExecutorService = Dispatchers.IO.asExecutor()}/*** 必须要设置* 设置上下文*/fun setContext(app: Application): DownloadConfig {context = appreturn this}/*** 设置自定义的DownloadDbHelper* 默认使用sqlite*/fun setDbHelper(impl: DownloadDbHelper): DownloadConfig {dbHelper = implreturn this}/*** 设置自定义的DownloadHttpHelper* 默认使用HttpURLConnection*/fun setHttpHelper(impl: DownloadHttpHelper): DownloadConfig {httpHelper = implreturn this}/*** 设置线程数* 默认 4*/fun setThreadNum(num: Int): DownloadConfig {threadNum = numreturn this}/*** 设置线程池* 默认采用 协程IO线程池*/fun setExecutor(executor: Executor): DownloadConfig {mExecutorService = executorreturn this}
}

子任务的实现


/*** Author: huangtao* Date: 2022/12/27* Desc: 子任务下载类*/
class SubDownloadTask(internal val subDownload: SubDownloadModel,//回调监听var listener: DownloadListener? = null
) : Runnable {companion object {const val BUFFER_SIZE: Long = 1024 * 1024}/*** 暂停任务*/fun pause() {subDownload.status = DownloadStatus.PAUSE}override fun run() {try {subDownload.status = DownloadStatus.DOWNLOADINGlistener?.onStart()val input = DownloadConfig.httpHelper.obtainStreamByRange(subDownload.url,subDownload.currentPos,subDownload.endPos)?: throw java.lang.NullPointerException("obtainStreamByRange InputStream is null")writeFile(input)} catch (e: Exception) {Log.e(DownloadConfig.TAG, e.message ?: "", e)subDownload.status = DownloadStatus.ERRORlistener?.onError(e.message ?: "")}}@Throws(IOException::class)private fun writeFile(input: InputStream) {Log.i(DownloadConfig.TAG,"${DownloadConfig.TAG}{${hashCode()}},写入开始 线程名:${Thread.currentThread().name} " +"文件路径:${subDownload.saveFile}")val file = RandomAccessFile(subDownload.saveFile, "rwd")val bufferSize = BUFFER_SIZE.coerceAtMost(subDownload.taskSize).toInt()val buffer = ByteArray(bufferSize)file.seek(subDownload.currentPos)while (true) {if (subDownload.status != DownloadStatus.DOWNLOADING) {break}val offset = input.read(buffer, 0, bufferSize)if (offset == -1) {break}file.write(buffer, 0, offset)subDownload.currentPos += offsetsubDownload.completeSize += offsetlistener?.onDownloading(offset.toLong(), subDownload.taskSize)}//更新状态if (subDownload.status == DownloadStatus.DOWNLOADING) {subDownload.status = DownloadStatus.COMPLETED}DownloadConfig.dbHelper.update(subDownload)//处理回调if (subDownload.status == DownloadStatus.COMPLETED) {listener?.onComplete()} else if (subDownload.status == DownloadStatus.PAUSE) {listener?.onPause()}//关闭资源file.close()input.close()Log.i(DownloadConfig.TAG,"${DownloadConfig.TAG}{${hashCode()}},\n 写入状态:${subDownload.status.name} " +"总大小=${subDownload.taskSize} 开始位置${subDownload.startPos} " +"结束位置${subDownload.endPos} 完成大小${subDownload.completeSize}")}
}

总任务的实现

/*** Author: huangtao* Date: 2022/12/27* Desc:*/
class DownloadTask(private val download: DownloadModel,//回调监听private val listener: DownloadListener
) : DownloadListener {/*** 线程数*/private val threadNum = DownloadConfig.threadNum/*** 子任务列表*/private val subTasks = mutableListOf<SubDownloadTask>()/*** 线程池*/private val mExecutorService: Executor by lazy {DownloadConfig.mExecutorService}private val mHandle = Handler(Looper.getMainLooper())/*** 开始下载* 如果是暂停的则从上次的位置继续下载*/fun download() {mExecutorService.execute {if (download.status == DownloadStatus.DOWNLOADING) {return@execute}download.status = DownloadStatus.DOWNLOADINGval list = DownloadConfig.dbHelper.queryByTaskTag(download.url, download.savePath)subTasks.clear()download.totalSize = 0download.completeSize = 0for (model in list) {val subTask = SubDownloadTask(model, this)download.totalSize += model.taskSizedownload.completeSize += model.completeSizesubTasks.add(subTask)}if (subTasks.isEmpty()) {downloadNewTask()} else if (subTasks.size == threadNum) {existDownloadTask()} else {resetDownloadTask()}}}/*** 暂停下载任务*/fun pauseDownload() {if (download.status != DownloadStatus.DOWNLOADING) {return}for (task in subTasks) {task.pause()}download.status = DownloadStatus.PAUSElistener.onPause()}/***重置下载任务*/fun resetDownloadTask() {mExecutorService.execute {for (task in subTasks) {DownloadConfig.dbHelper.delete(task.subDownload)}subTasks.clear()downloadNewTask()}}private fun existDownloadTask() {startAsyncDownload()}private fun downloadNewTask() {mExecutorService.execute {listener.onStart()download.completeSize = 0val targetFile = File(download.savePath)val destinationFolder = File(targetFile.parent ?: "")if (!destinationFolder.exists()) {destinationFolder.mkdirs()}targetFile.createNewFile()val size = DownloadConfig.httpHelper.obtainTotalSize(download.url)download.totalSize = sizeval averageSize = size / threadNumfor (i in 0 until threadNum) {var taskSize = averageSizeif (i == (threadNum - 1)) {taskSize += download.totalSize % threadNum}var start = 0Lvar index = iwhile (index > 0) {start += subTasks[i - 1].subDownload.taskSizeindex--}val end = start + taskSize - 1val subModel = SubDownloadModel(download.url, taskSize, start,end, start, targetFile.absolutePath)val subTask =SubDownloadTask(subModel, this)subTasks.add(subTask)DownloadConfig.dbHelper.insert(subTask.subDownload)}val file = RandomAccessFile(targetFile.absolutePath, "rwd")file.setLength(size)file.close()startAsyncDownload()}}private fun startAsyncDownload() {download.status = DownloadStatus.DOWNLOADINGfor (task in subTasks) {if (task.subDownload.completeSize < task.subDownload.taskSize) {mExecutorService.execute(task)}}}/*** 下载进度*/override fun onDownloading(progress: Long, total: Long) {synchronized(this) {mHandle.post {download.completeSize += progresslistener.onDownloading(download.completeSize, download.totalSize)}}}/*** 子任务完成回调* 此方法被将被调用threadNum次*/override fun onComplete() {for (task in subTasks) {if (task.subDownload.status != DownloadStatus.COMPLETED) {return}}Log.i(DownloadConfig.TAG,"${DownloadConfig.TAG}{${hashCode()}},下载完成 当前的线程名:${Thread.currentThread().name} ")for (task in subTasks) {DownloadConfig.dbHelper.delete(task.subDownload)}download.status = DownloadStatus.COMPLETED}/*** 子任务出现异常的回调*/override fun onError(msg: String) {//出现异常 暂停,清除任务重新下载pauseDownload()for (task in subTasks) {DownloadConfig.dbHelper.delete(task.subDownload)}subTasks.clear()listener.onError(msg)listener.onCancel()}/*** 任务是否完成*/fun isComplete(): Boolean {return download.status == DownloadStatus.COMPLETED}
}

使用方法

//引入依赖 gradle 7.0以下 项目根目录 build.gradle 文件
allprojects {repositories {...maven { url 'https://jitpack.io' }}}
//引入依赖 gradle 7.0以上 项目根目录 setting.gradle 文件
dependencyResolutionManagement {...repositories {...maven { url 'https://jitpack.io' }}
}
//模块module build.gradle
dependencies {...implementation 'com.github.huangtaoOO.TaoComponent:lib-download:0.0.7'
}
//初始化,必须
DownloadConfig//设置上下文 必须.setContext(application)//设置线程数 默认4 非必选.setThreadNum(2)//设置线程池 默认公用协程线程池 非必选.setExecutor(Dispatchers.Default.asExecutor())//设置下载实现 默认HttpURLConnection实现 非必选.setHttpHelper(object : DownloadHttpHelper{//...})//设置序列化实现 默认sqlite实现 非必须.setDbHelper(object : DownloadDbHelper{//...})//构建下载任务
val url = "https://dldir1.qq.com/qqfile/qq/TIM3.4.3/TIM3.4.3.22064.exe"
val downloadTask = createDownloadTask(url, createDownloadFile(context = this, url)) { progress, total ->//进度回调,不要处理耗时操作
}//下载任务
downloadTask.download()//重新下载
downloadTask.resetDownloadTask()//暂停下载
downloadTask.pauseDownload()//判断任务是否完成
downloadTask.isComplete()

源码传送门:github

  • 使用过程中如有BUG,请提issue
  • 使用过程中如有疑问或者更好的想法,欢迎进群讨论Android 学习交流群

Android 多线程下载以及断点续传相关推荐

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

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

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

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

  3. Android多线程下载断点续传

    先上图看卡结果: GITHUB:Android多线程下载断点续传 下载杵这儿 如图所示点击下载就开始下载,点击停止就会停止再次点击下载就会接着下载了. 设计思路是这样的: 首先通过广播将下载信息传递给 ...

  4. 更好的Android多线程下载框架

    /*** 作者:Pich* 原文链接:http://me.woblog.cn/* QQ群:129961195* Github:https://github.com/lifengsofts*/ 概述 为 ...

  5. android多线程下载原理,安卓多线程断点续传下载功能(靠谱第三方组件,原理demo)...

    一,原生的DownloadManager 从Android 2.3(API level 9)开始,Android以Service的方式提供了全局的DownloadManager来系统级地优化处理长时间 ...

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

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

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

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

  8. android多线程下载3

    今天跟大家一起分享下android开发中比较难的一个环节,可能很多人看到这个标题就会感觉头很大,的确如果没有良好的编码能力和逻辑思维,这块是很难搞明白的,前面2次总结中已经为大家分享过有关技术的一些基 ...

  9. Android -- 多线程下载

    因为Android应用程序是java写的,基本上很多java写的程序都可以直接照搬到Android上面,移植性非常Good.这里讲一下多线程下载,就是每个线程都下载自己的那部分,那么就需要平均分配分割 ...

最新文章

  1. ab flash player 8_ROM、RAM、DRAM、SRAM和FLASH的区别是什么?
  2. 总结C语言中的数组知识点
  3. 安全之心:一文读懂可信计算
  4. vs code vue 语法提示不全_Vue造轮子必备*.vue文件源码读取并高亮展示
  5. mysql5.7.9 zip achive
  6. Ignite 架构全面解析
  7. 宇枫资本女性如何理财致富
  8. C/C++银行账户管理系统
  9. 你还在找音乐网站吗?试试这几个吧
  10. Bable的基本使用
  11. ArcGIS 地图切图系列之(一)切片原理解析
  12. 数据标注案例分析-足球比赛时间轴打点标注项目
  13. LaTeX入门学习9(tikz基础-01)
  14. 【云原生之Docker实战】使用Docker部署phpMyAdmin数据库管理工具
  15. 0019_畸变矫正(单相机标定)
  16. 华为的面试题 要求8分钟写出代码
  17. Anaconda 安装python第三方库的各类方法
  18. TMC-城市智慧消防云平台
  19. 购买SOLIDWORKS正版软件需要注意哪些问题
  20. iOS简易蓝牙对战五子棋游戏设计思路之二——核心棋盘逻辑与胜负判定算法

热门文章

  1. 编写一个简单的考试程序,在控制台完成出题、答题的交互。试题(Question)分为单选(SingleChoice)和多选( MultiChoice)两种。
  2. 来吧,是时候升级您的领英技术档案了
  3. 【Flutter从入门到入坑】Flutter 知识体系
  4. 51单片机FM调频收音机可存台音量可调TEA5767 STM32
  5. 1、高德地图JS API开发背景知识
  6. 电机震动噪声(NVH)入门笔记
  7. response.setHeader参数、用法的介绍
  8. 如何由密度函数求分布函数
  9. MySQL: 容器化方式启动
  10. linux开启h2客户端