为什么选择 OkDownload
明确自己要找的下载框架需求:支持并发下载、文件多点分块下载、断点接续下载、动态设置 callback、使用 okHttp、retrofit、稳定性高(star 数)。网上找了一圈,有很多分析多线程断点续传下载原理的文章,核心内容说的很明白,核心代码也有,但是一个实用的下载框架,要满足和兼容尽可能多的业务场景需求,才是最耗时的。在 github 上流利说团队出品的 OkDownload、印度团队 MindOrks开源的 PRDownloader,两个框架的star数都很多,PRDownloader 是 HttpClient 发起的网络请求,还需要在Application中启动,这些导致它都不是T0的选择。
一开始用OkDownload
这个库还是有点犹豫的,因为看到它已经一年多没有更新了,提的issue也没人回应,基本处于无人维护的状态,但是没有其他更好的选择了,只有硬着头皮用了,然后开始踩坑了。
1. OkDownload
基础
先看一下基础用法:
// 核心代码库
implementation 'com.liulishuo.okdownload:okdownload:{latest_version}'
// sqlite数据库缓存下载信息(断点下载时使用),不引入则用内存缓存
implementation 'com.liulishuo.okdownload:sqlite:{latest_version}'
// 使用okhttp请求网络必须引入
implementation 'com.liulishuo.okdownload:okhttp:{latest_version}'
// 这个是文件下载管理 filedownloader,可以不引入
implementation 'com.liulishuo.okdownload:filedownloader:{latest_version}'
// 提供kotlin extension,可以不引入
implementation 'com.liulishuo.okdownload:ktx{latest_version}'
// 必须引入
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
添加了依赖库,build后就可以使用OkDownload
,基础的用法可以从github下载demo学习,这里就不重复说了。
我结合几个业务场景讲解一下问题。
2. 静默下载
静默下载,其实是指task不挂靠到主线程,下载后callback也在子线程。
private DownloadTask createTask() {
String filePath = '...';
return new DownloadTask.Builder(url, filePath, fileName)
.setPriority(0)
//callback progress的最小间隔(毫秒)
.setMinIntervalMillisCallbackProcess(300)
//task下载完,是否callback到主线程
.setAutoCallbackToUIThread(false)
.build();
}
上面是 task 创建的方式,它还可以设置任务执行优先级等属性。
-
单独task执行
// 异步执行task
task.enqueue(listener);
// 取消tasks
task.cancel();
-
多个task并行执行,有多种方式:
- 使用
DownloadContext
类,可以判断是否所有task都下载完成
DownloadContext.Builder builder = new DownloadContext.QueueSet()
.setParentPathFile(parentFile)
.setMinIntervalMillisCallbackProcess(150)
.commit();
builder.bindSetTask(task1);
//task.addTag(key, value)是用于传递任务下载时需要的参数
builder.bindSetTask(task2.addTag(key, value));
builder.bindSetTask(task3)
//contextListener:在单个 task 结束和所有 task 结束时 callback
builder.setListener(contextListener);
DownloadContext context = builder.build();
context.startOnParallel(listener);
...
// stop
context.stop();
-
使用
DownloadTask.enqueue(tasks, listener)
-
写一个单例类,使用
UnifiedListenerManager
类对task做管理,它能实现task
和listener
的多对对管理,也可以动态的添加和删除某一个task
上的listener
public class GlobalTaskManager {
private UnifiedListenerManager2 manager;
private DownloadListener mListener;
private GlobalTaskManager() {
manager = new UnifiedListenerManager2();
}
private static class ClassHolder {
private static final GlobalTaskManager INSTANCE = new GlobalTaskManager();
}
public static GlobalTaskManager getInstance() {
return ClassHolder.INSTANCE;
}
public void addAutoRemoveListenersWhenTaskEnd(int id) {
manager.addAutoRemoveListenersWhenTaskEnd(id);
}
public void addAutoRemoveListenersWhenTaskEnd(int[] ids) {
for (int id : ids) {
manager.addAutoRemoveListenersWhenTaskEnd(id);
}
}
public void attachListener(@NonNull DownloadTask task, @NonNull DownloadListener listener) {
manager.attachListener(task, listener);
}
public void attachAndEnqueueIfNotRun(@NonNull DownloadTask task, @NonNull DownloadListener listener) {
manager.detachListener(task.getId());
manager.attachAndEnqueueIfNotRun(task, listener);
}
public void enqueueTask(@NonNull DownloadTask task, @NonNull DownloadListener listener) {
manager.enqueueTaskWithUnifiedListener(task, listener);
}
public void enqueueTasks(@NonNull ArrayList<DownloadTask> tasks, @NonNull DownloadListener listener) {
mListener = listener;
manager.enqueueTasks(tasks, listener);
}
public DownloadListener getDownloadListener() {
return mListener;
}
static class UnifiedListenerManager2 extends UnifiedListenerManager {
public void enqueueTasks(@NonNull ArrayList<DownloadTask> tasks, @NonNull DownloadListener listener) {
for (DownloadTask task : tasks) {
attachListener(task, listener);
if (StatusUtil.isSameTaskPendingOrRunning(task)) {
tasks.remove(task);
}
}
DownloadTask[] _tasks = new DownloadTask[tasks.size()];
DownloadTask.enqueue(tasks.toArray(_tasks), getHostListener());
}
}
}
3. 插件化下载业务场景
首页开始多任务并行静默下载,点击首页list的item进入单个任务下载页面,这时需要获取当前task的下载状态,并实时同步下载进度。
这个feature有很多种方式来实现,请耐心往下看。
使用上面提到的UnifiedListenerManager
来实现
//LoadingActivity.java
...
mDownLoadTask = createTask().addTag(TASK_BEAN, data);
DownloadTask lastTask = OkDownload.with().downloadDispatcher().findSameTask(mDownLoadTask);
//判断在 OkDownload 中 task 是否已经存在
if (lastTask != null) {
mDownLoadTask = lastTask;
}
BreakpointInfo info = StatusUtil.getCurrentInfo(mDownLoadTask);
if (info != null) {
//如果存在task的信息,则先把task的进度同步
calcProgressToView(info.getTotalOffset(), info.getTotalLength(), true);
}
StatusUtil.Status status = StatusUtil.getStatus(mDownLoadTask);
if (status == StatusUtil.Status.COMPLETED
|| ((status == StatusUtil.Status.RUNNING || status == StatusUtil.Status.PENDING)
&& PrefUtil.getKeyBoolean(PrefKeys.DOWNLOAD_TASK_HAS_END + mDownLoadTask.getFilename(), false))) {
//1. 静默下载的task已经走到taskEnd回调中,不需要在替换listener了(极端情况)
//2. 静默下载的task已经完成(极端情况)
calcProgressToView(1, 1, true);
downloadFinish();
} else {
//task未下载 或 下载中
GlobalTaskManager.getInstance().attachAndEnqueueIfNotRun(mDownLoadTask, getListener());
GlobalTaskManager.getInstance().addAutoRemoveListenersWhenTaskEnd(mDownLoadTask.getId());
}
...
private DownloadListener4 getListener() {
return new DownloadListener4() {
...
}
}
使用DownloadTask.replaceListener方法
StatusUtil.Status status = StatusUtil.getStatus(mDownLoadTask);
if (status == StatusUtil.Status.COMPLETED
|| ((status == StatusUtil.Status.RUNNING || status == StatusUtil.Status.PENDING)
&& PrefUtil.getKeyBoolean(PrefKeys.DOWNLOAD_TASK_HAS_END + mDownLoadTask.getFilename(), false))) {
...
} else if (status == StatusUtil.Status.RUNNING || status == StatusUtil.Status.PENDING) {
DownloadListener4 listener = getListener();
listener.setAlwaysRecoverAssistModelIfNotSet(true);
mDownLoadTask.replaceListener(listener);
} else {
mDownLoadTask.enqueue(getListener());
}
以上两种方法都可以,但是会有一个大问题,就是 createTask()
时,如果后台静默下载设置不回调主线程,而在 LoadingActivity
中设置回调主线程时,会导致task下载的文件不完整,而且不会报错。这是我花了近两天时间才发现并验证的问题。
静默下载监听listener记录下载进度,保存到Map中,在 LoadingActivity
中定时获取进度
if (status == StatusUtil.Status.PENDING || status == StatusUtil.Status.RUNNING) {
//开始倒计时
mSubscription = Observable.interval(0, 1, TimeUnit.SECONDS)//延迟0,间隔1s,单位秒
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Long>() {
@Override
public void call(Long time) {
DownloadListener listener = GlobalTaskManager.getInstance().getDownloadListener();
if (listener instanceof DownLoadListener) {
DownLoadListener listener = (DownLoadListener) listener;
long currentOffset = 0;
if (listener.getCurrentOffset(task.getFileName()) != null) {
currentOffset = listener.getCurrentOffset(task.getFileName());
}
long totalLength = 0;
if (listener.getTotalLength(task.getFileName()) != null) {
totalLength = listener.getTotalLength(task.getFileName());
}
calcProgressToView(currentOffset, totalLength, false);
String endStatus = listener.getTaskEndStatus(task.getFileName());
if (TextUtils.isEmpty(endStatus)) {
return;
}
if ("success".equals(endStatus)) {
downloadFinish();
}
}
}
}, new Action1<Throwable>() {
@Override
public void call(Throwable throwable) {
}
});
}
java.io.IOException: The current offset on block-info isn't update correct, 13439740 != 14072131 on 2
以上就是OkDownload
的基础用法。
结尾
既然您看到这了,说明文章对你还有吸引力,帮忙点个赞再走吧,谢谢!
关注我的公众号「掉队程序员」,持续输出更多内容!
自己动手写,分解项目中的各个模块需求,通过查文档和搜索Cocos社区,解决碰到的问题,最终在微信上线了下面这款微信小游戏《成语锦衣卫》,欢迎大家扫码体验,并作为参考项目模版,开发出属于自己的小游戏