Android断点续传下载 OkDownload 使用误区

6,266 阅读5分钟

为什么选择 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并行执行,有多种方式:
  1. 使用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();
  1. 使用DownloadTask.enqueue(tasks, listener)

  2. 写一个单例类,使用UnifiedListenerManager类对task做管理,它能实现tasklistener的多对对管理,也可以动态的添加和删除某一个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社区,解决碰到的问题,最终在微信上线了下面这款微信小游戏《成语锦衣卫》,欢迎大家扫码体验,并作为参考项目模版,开发出属于自己的小游戏 欢迎大家扫码体验