/**
- 获取所有指定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();
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
最后
都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。
技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;
我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
Java语言与原理;
大厂,小厂。Android面试先看你熟不熟悉Java语言
高级UI与自定义view;
自定义view,Android开发的基本功。
性能调优;
数据结构算法,设计模式。都是这里面的关键基础和重点需要熟练的。
NDK开发;
未来的方向,高薪必会。
前沿技术;
组件化,热升级,热修复,框架设计
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多,GitHub可见;《Android架构视频+学习笔记》
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!
习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
我在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多,GitHub可见;《Android架构视频+学习笔记》
当然,想要深入学习并掌握这些能力,并不简单。关于如何学习,做程序员这一行什么工作强度大家都懂,但是不管工作多忙,每周也要雷打不动的抽出 2 小时用来学习。
不出半年,你就能看出变化!