RandomAccessFile 文件写入

下面再讲讲文件写入问题,由于我们是多线程下载,因此文件并不是每次都是从前往后一个个字节写入的,随时可能在文件的任何一个地方写入数据。因此我们需要能够在文件的指定位置写入数据。这里我们用到了RandomAccessFile 来实现这个功能。

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

接下来简单讲讲我们这里是如何使用 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;
}
});
}

可以看到,我们获取了其从 currentPosendPos 端的字节流,通过其 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多线程断点续传下载原理及实现,移动开发工程师简历相关推荐

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

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

  2. android 多线程断点续传下载 三

    android 多线程断点续传下载 三 转载于:https://www.cnblogs.com/zhujiabin/p/5660093.html

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

    csdn从老编辑器更换成新的编辑器,居然是重新发表一篇博客,好吧 http://blog.csdn.net/self_study/article/details/50505865

  4. android 多文件多线程断点续传下载

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

  5. 撸了个多线程断点续传下载器,我从中学习到了这些知识(附开源地址)

    2020年6月20日 上海张江 感谢看客老爷点进来了,周末闲来无事,想起同事强哥的那句话:"你有没有玩过断点续传?" 当时转念一想,断点续传下载用的确实不少,具体细节嘛,真的没有去 ...

  6. OkHttp实现多线程断点续传下载,单例模式下多任务下载管理器,一起抛掉sp,sqlite的辅助吧

    丨版权说明 :<OkHttp实现多线程断点续传下载,单例模式下多任务下载管理器,一起抛掉sp,sqlite的辅助吧>于当前CSDN博客和乘月网属同一原创,转载请说明出处,谢谢. 最近项目需 ...

  7. 即拿即用-Android多线程断点下载

    线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为3条线程,因为线程过多会占用系统资源,而且线程间的相互竞争也会导致下载变慢. 其次下载的时候将文件分割为三份(假设用3条线程下载)下载,在 ...

  8. android 多线程断点下载,listview 模式 开始 暂停等功能

    android 多线程断点下载,listview 模式 代码依次如下: 布局: <?xml version="1.0" encoding="utf-8"? ...

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

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

最新文章

  1. as3绘制抛物线(二)
  2. 满屋研选获1亿元B轮融资,华创资本领投,五岳资本、金地集团、治平资本等跟投...
  3. mybatis mysql Dao_Dao模式创建mybatis项目过程
  4. 【Java每日一题】20161018
  5. fileinput 加 ftp 加 nginx 加 SpringBoot上传文件
  6. 确认! Python夺冠,Java“被迫”退出竞争舞台,网友:崩溃!
  7. 熵值法确定权重(matlab附代码)
  8. 蒙特卡洛模拟(Monte Carlo simulation)
  9. tesseract 提升识别质量
  10. HDU2111 Saving HDU 【贪心】
  11. OpenCV-人像—酷感冷艳滤镜
  12. c++中向任意目录下写文件
  13. Quartus II13.0的破解过程
  14. 高铁订票系统设计C语言,数学建模 高铁订票系统建模
  15. 服务器上使用nvcc编译多个cu文件,在cmake中使用nvcc编译。cu
  16. pthread_cond_wait和pthread_cond_signal函数详解
  17. 外卖骑手的收入怎么样?
  18. 在ubuntu中运行sudo apt-get update报错The following signatures couldn‘t be verified because the public key
  19. 青海行--(7月28日)凯旋归程
  20. VUE3 使用 Ant Design Vue 图标库的图标

热门文章

  1. C语言基础-部分基础理论知识汇总
  2. 乐谱学习软件:iReal Pro for Mac
  3. 简易画图工具(Python)
  4. 如何使用 DiskGenius 合并分区教程
  5. 三节点大数据环境安装详细教程
  6. Kibana 7.13.2 启动时报错 TaskManager is unable to start as there the Kibana UUID is invalid
  7. 如何让60岁老人学会使用智能手机
  8. 星星之火-5:数字无线通信相对模拟无线通信的优点
  9. 无线华为能连苹果不能连接到服务器,华为网络正常app连不上网络
  10. 软件的基本是要处理好”算法“及其基础(一)流-字-字符(包括某个数字、字母、符号和某个汉字等)-字符串-字节动态数组-字节-整数之间的转化关系和算法