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

在这里插入图片描述

多线程下载

在日常开发中,我们不可避免的会接到类似这样的需求,下载一个比较大的素材文件或者安装包文件,以此实现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 学习交流群

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/39125.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

断点续传的实现

文章目录 从上次断开位置继续下载实现原理&#xff08;客户端&#xff09;&#xff1a;HTTP实现&#xff08;流程&#xff09;&#xff1a;图1&#xff1a; 客户端 Range头部信息图2&#xff1a;服务端Content-Range信息 客户端与服务端的请求与响应图解&#xff1a;获取文件 E…

断点续传的原理

断点续传是大型文件数据传输的核心。本文将以多线程技术和Socket技术为依托&#xff0c;介绍大型文件断点续传的实现方法。  基本实现思想  多线程断点续传实现的基本思想就是在发送端&#xff08;也称客户端&#xff09;将要传输的文件分割为大小相当的多块&#xff0c;用…

wget 断点续传

wget是Linux系统中的一个下载工具&#xff0c;最近因为要下载一个比较大的数据集&#xff0c;直接使用浏览器下载每次断开后都会从头开始下载&#xff0c;而wget支持的断点续传功能则可以在已经下载的部分数据的基础上继续下载剩余数据。 &#xff08;1&#xff09;wget安装 …

如何实现文件断点续传功能

本文章首发于公众号攻城狮客栈&#xff0c;有需要的朋友可文末扫描二维码。 相信大家都使用过迅雷、电驴、百度云网盘等一类的工具&#xff0c;所有这些支持上传或下载的工具都有一个功能&#xff0c;那就是断点续传&#xff0c;也就是在你网络不佳传输断开时&#xff0c;传输会…

python 断点续传

python 断点续传 1.前序2.技术原理2.1 Content-Range2.2 Range 3. 代码实现 1.前序 当下载突然断开后&#xff0c;断点续传就需要了&#xff0c;继续前面下载的内容下载。解决了不需要重复下载 2.技术原理 HTTP/1.1 开始支持断点续传&#xff0c;一般断点下载会用到 Range 和…

web前端Tips:断点续传如何实现?

在Web前端中实现断点续传功能的一种常见方式是使用HTTP Range请求和文件分片上传。 以下是一个简单的断点续传实现的步骤&#xff1a; 前端将要上传的文件分成多个固定大小的片段&#xff08;chunk&#xff09;&#xff0c;例如每个片段的大小为1MB。当用户选择上传文件时&am…

前段实现文件的断点续传

早就听说过断点续传这种东西&#xff0c;前端也可以实现一下 断点续传在前端的实现主要依赖着HTML5的新特性&#xff0c;所以一般来说在老旧浏览器上支持度是不高的 本文通过断点续传的简单例子&#xff08;前端文件提交后端PHP文件接收&#xff09;&#xff0c;理解其大致的实…

Android开发——断点续传原理以及实现

0. 前言 在Android开发中&#xff0c;断点续传听起来挺容易&#xff0c;在下载一个文件时点击暂停任务暂停&#xff0c;点击开始会继续下载文件。但是真正实现起来知识点还是蛮多的&#xff0c;因此今天有时间实现了一下&#xff0c;并进行记录。本文原创&#xff0c;转载请注…

如何实现断点续传

断点续传是指在网络传输中&#xff0c;当传输过程中出现异常或者用户主动停止传输时&#xff0c;能够恢复传输过程&#xff0c;避免重新传输已经传输过的数据&#xff0c;提高传输效率。实现断点续传可以通过以下方式&#xff1a; HTTP协议支持的断点续传 在HTTP协议中&#xf…

【MQ学习笔记】RocketMQ知识分析与总结

RocketMQ 为什么使用mq&#xff1f;mq的作用&#xff1f;mq对你项目带来了什么&#xff1f;不选mq行不行&#xff1f; 异步&#xff0c;MQ能够以异步的方式对消息进行处理&#xff0c;能够大大提高了系统的响应以及吞吐量解耦&#xff0c;MQ双方只需要负责生产或消费信息即可…

销售数据分析方法、如何写好一个专题分析报告、Hive大数据知识体系教程、大数据分析平台总体架构方案……| 本周精华...

▲点击上方卡片关注我&#xff0c;回复“8”&#xff0c;加入数据分析领地&#xff0c;一起学习数据分析&#xff0c;持续更新数据分析学习路径相关资料~&#xff08;精彩数据观点、学习资料、数据课程分享、读书会、分享会等你一起来乘风破浪~&#xff09;回复“小飞象”&…

老杨刷完了23个跨年演讲,这6场最适合网工

晚上好&#xff0c;我是老杨。 放假放了一阵子&#xff0c;老杨闲着没事儿&#xff0c;一共刷了23个跨年演讲。 好的演讲&#xff0c;和一本好书一样&#xff0c;都是可以震荡灵魂的。 也知道最近大家都忙得很&#xff0c;这里推荐7个最值得一听的跨年演讲&#xff0c;贴心吧…

朱广权李佳琦直播掉线,1.2 亿人在线等

作者 | 胡巍巍 出品 | 程序人生&#xff08;ID&#xff1a;coder_life&#xff09; 原来央视爸爸也有掉线的时候。 4月6日晚间&#xff0c;“国民段子手”朱广权连麦李佳琦&#xff0c;给湖北做公益带货直播。 “小朱配琦”的神仙组合&#xff0c;让人大呼过瘾&#xff0c;该直…

“我让 AI 来处理我 24 小时的音频信息——这会是未来吗?”

整理 | 郑丽媛 出品 | CSDN&#xff08;ID&#xff1a;CSDNnews&#xff09; 这几年 AI 发展和进化的速度&#xff0c;几乎可以用三个字来形容&#xff1a;杀疯了。 AI 下棋、AI 编程、AI 作画、AI 写小说、AI 预测蛋白质结构……当 AI 逐渐融入我们的生活&#xff0c;你是否设…

抗住百万人直播、被联合国推荐,起底飞书技术演进之路!

你去公司上班了吗&#xff1f; 随着近期接连不断传来的好消息&#xff0c;上班族开始关心起这问题来。但许多企业在相关政策的号召下&#xff0c;仍采取远程办公的方式。据艾媒咨询的数据显示&#xff0c;今年春节期间中国远程办公企业规模超过 1800 万家&#xff0c;远程办公人…

听8位淘宝工程师聊聊他们眼中的元宇宙 | 1024特辑

我们是技术工作者&#xff0c;致力于思考和创新&#xff0c;用代码去解决生活中的问题&#xff0c;为消费者的快乐和幸福而努力。 今天是1024程序员节&#xff0c;我们邀请了8位不同岗位的淘宝工程师&#xff0c;聊了聊他们眼中的未来消费生活&#xff0c;他们眼中的元宇宙&…

数字人的新革命,BAT的“冲高”战场

配图来自Canva可画 ChatGPT横空出世&#xff0c;让人们看到了数字人的另一种可能&#xff0c;将ChatGPT与虚拟数字人融合&#xff0c;研发出更加智能化、拟人化的虚拟数字人成为数字人厂商的新命题、新方向。 2月份&#xff0c;岭南股份、风语筑、开普云等10多家公司&#xf…

数字化直播沟通

目录 前言 一、介绍数字化&#xff08;5-10分钟&#xff09; 主播提问&#xff08;王&#xff09;&#xff1a; 回答&#xff08;向&#xff09;&#xff1a; 什么是数字化&#xff1f; 好的&#xff0c;我们来看下一个问题&#xff1a;什么是数字化转型&#xff1f; 好的…

部署React项目到云服务器(步骤清晰)

部署React项目到云服务器 前言(下载相关软件)开始部署服务器以及域名购买实例远程连接 或 XShell 7 远程连接服务器XShell 7 连接云服务器操作使用FileZilla将 打包后的 build(可能你们那里打包是dist)文件塞到服务器文件夹感谢看到这里&#xff01;点个关注再走啊&#xff01;…

云服务安装配置git

搭建网站&#xff0c;我们有时候是直接从github拉取代码&#xff0c;这就要配置git。 云服务器搭建网站还需要一些其他的基础配置&#xff0c;前面我们已经讲到&#xff0c;未进行配置的可以参考之前的文章。 云服务基本配置&#xff1a;腾讯云服务器小白保姆级教程_大鹏bmfm…