Android多线程断点续传下载原理及实现,移动开发工程师简历
RandomAccessFile 文件写入
下面再讲讲文件写入问题,由于我们是多线程下载,因此文件并不是每次都是从前往后一个个字节写入的,随时可能在文件的任何一个地方写入数据。因此我们需要能够在文件的指定位置写入数据。这里我们用到了RandomAccessFile
来实现这个功能。
RandomAccessFile
是一个随机访问文件类,同时整合了 FileOutputStream
和 FileInputStream
,支持从文件的任何字节处读写数据。通过它我们就可以在文件的任何字节处写入数据。
接下来简单讲讲我们这里是如何使用 RandomAccessFile
的。我们对于每个子任务来说都有一个开始和结束的位置。每个任务都可以通过 RandomAccessFile::seek
跳转到文件的对应字节位置,然后从该位置开始读取 InputStream
并写入。
这样,就实现了不同线程对文件的随机写入。
文件大小的获取
由于我们在真正开始下载之前,我们需要先将任务分配到各个线程,因此我们需要先了解到文件的大小。
为了获取到文件的大小,我们用到 Response Headers
中的 Content-Length
字段。
如下图所示,可以看到,打开该下载请求的链接后,Response Headers
中包含了我们需要的 Content-Length
,也就是该文件的大小,单位是字节。
断点续传原理
对于多个子任务,我们如何实现它们的断点续传呢?
其实原理很简单,只需要保证每个子任务的下载进度能够被
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
即时地记录即可。这样继续下载时只需要读取这些下载记录,从上次下载结束的位置开始下载即可。
它的实现有很多方式,只要能做到数据持久化即可。这里我使用的是数据库来实现。
这样,我们的子任务需要拥有一些必要的信息
completedSize
:当前下载完成大小taskSize
:子任务总大小startPos
:子任务开始位置currentPos
:子任务进行到的位置endPos
:子任务结束位置
通过这些信息,我们就能够记录子任务的下载进度从而恢复我们之前的下载,实现断点续传。
代码实现
下面我们用代码来实现这样一个多线程下载功能。
下载状态
首先,我们定义一下下载中的各个状态:
public class DownloadStatus {
public static final int IDLE = 233; // 空闲,默认状态
public static final int COMPLETED = 234; // 完成
public static final int DOWNLOADING = 235; // 下载中
public static final int PAUSE = 236; // 暂停
public static final int ERROR = 237; // 出错
}
可以看到,这里定义了如上的五种状态。
基本辅助类的抽象
这里需要用到如数据库及 HTTP 请求的功能,我们这里定义其接口如下,具体实现各位可以根据需要自己实现:
数据库辅助类
public interface DownloadDbHelper {
/**
- 从数据库中删除子任务记录
- @param task 子任务记录
*/
void delete(SubDownloadTask task);
/**
- 向数据库中插入子任务记录
- @param task 子任务记录
*/
void insert(SubDownloadTask task);
/**
- 在数据库中更新子任务记录
- @param task 子任务记录
*/
void update(SubDownloadTask task);
/**
- 获取所有指定Task下的子任务记录
- @param taskTag Task的Tag
- @return 子任务记录
*/
List queryByTaskTag(String taskTag);
}
Http 辅助类
public interface DownloadHttpHelper {
/**
- 获取文件总长度
- @param url 下载url
- @param callback 获取文件长度CallBack
*/
void getTotalSize(String url, NetCallback callback);
/**
- 获取InputStream
- @param url 下载url
- @param start 开始位置
- @param end 结束位置
- @param callback 获取字节流的CallBack
*/
void getStreamByRange(String url, long start, long end, NetCallback callback);
}
子任务实现
成员变量及解释
我们先从上到下,从子任务开始实现。在我的设计中,它具有如下的成员变量:
@Entity
public class SubDownloadTask implements Runnable {
public static final int BUFFER_SIZE = 1024 * 1024;
private static final String TAG = SubDownloadTask.class.getSimpleName();
@Id
private Long id;
private String url; // 文件下载的 url
private String taskTag; // 父任务的 Tag
private long taskSize; // 子任务大小
private long completedSize; // 子任务完成大小
private long startPos; // 开始位置
private long currentPos; // 当前位置
private long endPos; // 结束位置
private volatile int status; // 当前下载状态
@Transient
private SubDownloadListener listener; // 子任务下载监听,主要用于提示父任务
@Transient
private File saveFile; // 要保存到的文件
…
}
由于这里的数据库的操作是用 GreenDao
实现,因此这里有一些相关注解,各位可以忽略。
InputStream
获取
可以看到,子任务是一个 Runnable,我们可以通过其 run 方法开始下载,这样就可以通过如 ExecutorService 来开启多个线程执行子任务。
我们看到其 run 方法:
@Override
public void run() {
status = DownloadStatus.DOWNLOADING;
DownloadManager.getInstance()
.getHttpHelper()
.getStreamByRange(url, currentPos, endPos, new NetCallback() {
@Override
public void onResult(InputStream inputStream) {
listener.onSubStart();
writeFile(inputStream);
}
@Override
public void onError(String message) {
listener.onSubError(“文件流获取失败”);
status = DownloadStatus.ERROR;
}
});
}
可以看到,我们获取了其从 currentPos
到 endPos
端的字节流,通过其 Response Body 拿到了它的 InputStream
,然后调用了 writeFile(InputStream)
方法进行文件的写入。
文件写入
接下来看到 writeFile
方法:
private void writeFile(InputStream in) {
try {
RandomAccessFile file = new RandomAccessFile(saveFile, “rwd”); // 通过 saveFile 建立RandomAccessFile
file.seek(currentPos); // 跳转到对应位置
byte[] buffer = new byte[BUFFER_SIZE];
while (true) {
// 循环读取 InputStream,直到暂停或读取结束
if (status != DownloadStatus.DOWNLOADING) {
// 状态不为 DOWNLOADING,停止下载
break;
}
int offset = in.read(buffer, 0, BUFFER_SIZE);
if (offset == -1) {
// 读取不到数据,说明读取结束
break;
}
// 将读取到的数据写入文件
file.write(buffer, 0, offset);
// 下载数据并在数据库中更新
currentPos += offset;
completedSize += offset;
DownloadManager.getInstance()
.getDbHelper()
.update(this);
// 通知父任务下载进度
listener.onSubDownloading(offset);
}
if(status == DownloadStatus.DOWNLOADING) {
// 下载完成
status = DownloadStatus.COMPLETED;
// 通知父任务下载完成
listener.onSubComplete(completedSize);
}
file.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
listener.onSubError(“文件下载失败”);
status = DownloadStatus.ERROR;
resetTask();
}
}
具体流程可以看代码中的注释。可以看到,子任务实际上就是循环读取 InputStream
,并写入文件,同时将下载进度同步到数据库。
父任务实现
父任务也就是我们具体的下载任务,我们同样先看到成员变量:
public class DownloadTask implements SubDownloadListener {
private static final String TAG = DownloadTask.class.getSimpleName();
private String tag; // 下载任务的 Tag,用于区分不同下载任务
private String url; // 下载 url
private String savePath; // 保存路径
private String fileName; // 保存文件名
private DownloadListener listener; // 下载监听
private long completeSize; // 下载完成大小
private long totalSize; // 下载任务总大小
private int status; // 当前下载进度
private int threadNum; // 线程数(由外部设置的每个任务的下载线程数)
private File file; // 保存文件
private List subTasks; // 子任务列表
private ExecutorService mExecutorService; // 线程池,用于执行子任务
…
}
下载功能
对于一个下载任务,可以通过 download 方法开始执行:
public void download() {
listener.onStart();
subTasks = querySubTasks();
status = DownloadStatus.DOWNLOADING;
if (subTasks.isEmpty()) {
// 是新任务
downloadNewTask();
} else if (subTasks.size() == threadNum) {
// 不是新任务
downloadExistTask();
} else {
// 不是新任务,但下载线程数有误
listener.onError(“断点数据有误”);
resetTask();
}
}
可以看到,我们先将子任务列表从数据库中读取出来。
- 如果子任务列表为空,则说明还没有下载记录,也就是说是一个新任务,调用
downloadNewTask
方法。 - 如果子任务列表大小等于线程数,则说明其不是新任务,调用
downloadExistTask
方法。 - 如果子任务列表大小不等于线程数,说明当前的下载记录已不可用,于是重置下载任务,从新下载。
下载新任务
我们先看到 downloadNewTask
方法:
DownloadManager.getInstance()
.getHttpHelper()
.getTotalSize(url, new NetCallback() {
@Override
public void onResult(Long total) {
completeSize = 0L;
totalSize = total;
initSubTasks();
startAsyncDownload();
}
@Override
public void onError(String message) {
error(“获取文件长度失败”);
}
});
可以看到,获取到总长度后,通过调用 initSubTasks
方法,对子任务列表进行了初始化(计算子任务长度等),然后调用了 startAsyncDownload
方法后通过 ExecutorService
运行子任务进入子任务进行下载。
我们看到 initSubTasks
方法:
private void initSubTasks() {
long averageSize = totalSize / threadNum;
for (int taskIndex = 0; taskIndex < threadNum; taskIndex++) {
long taskSize = averageSize;
if (taskIndex == threadNum - 1) {
// 最后一个任务,则 size 还需要加入剩余量
taskSize += totalSize % threadNum;
}
long start = 0L;
int index = taskIndex;
while (index > 0) {
start += subTasks.get(index - 1).getTaskSize();
index–;
}
long end = start + taskSize - 1; // 注意这里
SubDownloadTask subTask = new SubDownloadTask();
subTask.setUrl(url);
subTask.setStatus(DownloadStatus.IDLE);
subTask.setTaskTag(tag);
subTask.setCompletedSize(0);
subTask.setTaskSize(taskSize);
subTask.setStartPos(start);
subTask.setCurrentPos(start);
subTask.setEndPos(end);
subTask.setSaveFile(file);
subTask.setListener(this);
DownloadManager.getInstance()
.getDbHelper()
.insert(subTask);
subTasks.add(subTask);
}
}
可以看到就是计算每个任务的大小及开始及结束点的位置,这里要注意的是 endPos 需要 -1,否则各个任务的下载位置会重叠,并且最后一个任务会多下载一个字节导致如文件损坏等影响。具体原因就是比如一个大小为 500 的文件,则应当是 0-499 而不是 0-500。
恢复旧任务
接下来我们看看 downloadExistTask
方法:
private void downloadExistTask() {
// 不是新任务,且下载线程数无误,计算已下载大小
completeSize = countCompleteSize();
totalSize = countTotalSize();
startAsyncDownload();
}
这里其实很简单,遍历子任务列表计算已下载量及总任务量,并调用 startAsyncDownload 开始多线程下载。
执行子任务
具体执行子任务我们可以看到 startAsyncDownload
方法:
private void startAsyncDownload() {
for (SubDownloadTask subTask : subTasks) {
if (subTask.getCompletedSize() < subTask.getTaskSize()) {
// 只下载没有下载结束的子任务
mExecutorService.execute(subTask);
}
}
}
可以看到,这里其实只是通过 ExecutorService 执行对应子任务(Runnable)而已。
####暂停功能
我们接下来看到 pause 方法:
public void pause() {
stopAsyncDownload();
status = DownloadStatus.PAUSE;
listener.onPause();
}
可以看到,这里只是调用了 stopAsyncDownload
方法停止子任务。
看到 stopAsyncDownload
方法:
() < subTask.getTaskSize()) {
// 只下载没有下载结束的子任务
mExecutorService.execute(subTask);
}
}
}
可以看到,这里其实只是通过 ExecutorService 执行对应子任务(Runnable)而已。
####暂停功能
我们接下来看到 pause 方法:
public void pause() {
stopAsyncDownload();
status = DownloadStatus.PAUSE;
listener.onPause();
}
可以看到,这里只是调用了 stopAsyncDownload
方法停止子任务。
看到 stopAsyncDownload
方法:
Android多线程断点续传下载原理及实现,移动开发工程师简历相关推荐
- android 多线程断点续传下载
今天跟大家一起分享下android开发中比较难的一个环节,可能很多人看到这个标题就会感觉头很大,的确如果没有良好的编码能力和逻辑思维,这块是很难搞明白的,前面2次总结中已经为大家分享过有关技术的一些基 ...
- android 多线程断点续传下载 三
android 多线程断点续传下载 三 转载于:https://www.cnblogs.com/zhujiabin/p/5660093.html
- android多线程断点续传下载
csdn从老编辑器更换成新的编辑器,居然是重新发表一篇博客,好吧 http://blog.csdn.net/self_study/article/details/50505865
- android 多文件多线程断点续传下载
今天跟大家一起分享下android开发中比较难的一个环节,可能很多人看到这个标题就会感觉头很大,的确如果没有良好的编码能力和逻辑思维,这块是很难搞明白的,前面2次总结中已经为大家分享过有关技术的一些基 ...
- 撸了个多线程断点续传下载器,我从中学习到了这些知识(附开源地址)
2020年6月20日 上海张江 感谢看客老爷点进来了,周末闲来无事,想起同事强哥的那句话:"你有没有玩过断点续传?" 当时转念一想,断点续传下载用的确实不少,具体细节嘛,真的没有去 ...
- OkHttp实现多线程断点续传下载,单例模式下多任务下载管理器,一起抛掉sp,sqlite的辅助吧
丨版权说明 :<OkHttp实现多线程断点续传下载,单例模式下多任务下载管理器,一起抛掉sp,sqlite的辅助吧>于当前CSDN博客和乘月网属同一原创,转载请说明出处,谢谢. 最近项目需 ...
- 即拿即用-Android多线程断点下载
线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为3条线程,因为线程过多会占用系统资源,而且线程间的相互竞争也会导致下载变慢. 其次下载的时候将文件分割为三份(假设用3条线程下载)下载,在 ...
- android 多线程断点下载,listview 模式 开始 暂停等功能
android 多线程断点下载,listview 模式 代码依次如下: 布局: <?xml version="1.0" encoding="utf-8"? ...
- android多线程下载原理,安卓多线程断点续传下载功能(靠谱第三方组件,原理demo)...
一,原生的DownloadManager 从Android 2.3(API level 9)开始,Android以Service的方式提供了全局的DownloadManager来系统级地优化处理长时间 ...
最新文章
- as3绘制抛物线(二)
- 满屋研选获1亿元B轮融资,华创资本领投,五岳资本、金地集团、治平资本等跟投...
- mybatis mysql Dao_Dao模式创建mybatis项目过程
- 【Java每日一题】20161018
- fileinput 加 ftp 加 nginx 加 SpringBoot上传文件
- 确认! Python夺冠,Java“被迫”退出竞争舞台,网友:崩溃!
- 熵值法确定权重(matlab附代码)
- 蒙特卡洛模拟(Monte Carlo simulation)
- tesseract 提升识别质量
- HDU2111 Saving HDU 【贪心】
- OpenCV-人像—酷感冷艳滤镜
- c++中向任意目录下写文件
- Quartus II13.0的破解过程
- 高铁订票系统设计C语言,数学建模 高铁订票系统建模
- 服务器上使用nvcc编译多个cu文件,在cmake中使用nvcc编译。cu
- pthread_cond_wait和pthread_cond_signal函数详解
- 外卖骑手的收入怎么样?
- 在ubuntu中运行sudo apt-get update报错The following signatures couldn‘t be verified because the public key
- 青海行--(7月28日)凯旋归程
- VUE3 使用 Ant Design Vue 图标库的图标
热门文章
- C语言基础-部分基础理论知识汇总
- 乐谱学习软件:iReal Pro for Mac
- 简易画图工具(Python)
- 如何使用 DiskGenius 合并分区教程
- 三节点大数据环境安装详细教程
- Kibana 7.13.2 启动时报错 TaskManager is unable to start as there the Kibana UUID is invalid
- 如何让60岁老人学会使用智能手机
- 星星之火-5:数字无线通信相对模拟无线通信的优点
- 无线华为能连苹果不能连接到服务器,华为网络正常app连不上网络
- 软件的基本是要处理好”算法“及其基础(一)流-字-字符(包括某个数字、字母、符号和某个汉字等)-字符串-字节动态数组-字节-整数之间的转化关系和算法