Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

目录

目录
简介
基本使用
开始一个任务
配置 DownloadTask
任务队列的构建、开始和停止
获取任务状态
获取断点信息
设置任务监听
设置多个监听
动态更改任务的监听
全局控制
组件注入
动态串行队列
源码结构
使用案例
Activity
辅助工具类
辅助Bean
DownloadListener4WithSpeed
源码分析
OkDownload
DownloadTask
DownloadCall
DownloadChain
总体流程

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

简介

项目地址

A Reliable, Flexible, Fast and Powerful download engine.

引入

implementation 'com.liulishuo.okdownload:okdownload:1.0.5' //核心库
implementation 'com.liulishuo.okdownload:sqlite:1.0.5' //存储断点信息的数据库
implementation 'com.liulishuo.okdownload:okhttp:1.0.5' //提供okhttp连接,如果使用的话,需要引入okhttp网络请求库
implementation "com.squareup.okhttp3:okhttp:3.10.0"

OkDownload是一个android下载框架,是FileDownloader的升级版本,也称FileDownloader2;是一个支持多线程,多任务,断点续传,可靠,灵活,高性能以及强大的下载引擎。

对比FileDownloader的优势

  • 单元测试覆盖率很高,从而保证框架的可靠性。
  • 简单的接口设计。
  • 支持任务优先级。
  • Uri文件转存储输出流。
  • 核心类库更加单一和轻量级。
  • 更灵活的回调机制和侦听器。
  • 更灵活地扩展OkDownload的每个部分。
  • 在不降低性能的情况下,更少的线程可以执行相同的操作。
  • 文件IO线程池和网络IO线程池分开。
  • 如果无法从响应头中找到,从URL中获取自动文件名。
  • 取消和开始是非常有效的,特别是对于大量的任务,有大量的优化。

基本使用

具体详见官方文档 Simple-Use-GuidelineAdvanced-Use-Guideline

请通过Util.enableConsoleLog()在控制台打印上启用日志,也可以通过Util.setLogger(Logger)设置自己的日志记录器

开始一个任务

DownloadTask task = new DownloadTask.Builder(url, parentFile)
         .setFilename(filename) 
         .setMinIntervalMillisCallbackProcess(30) // 下载进度回调的间隔时间(毫秒)
         .setPassIfAlreadyCompleted(false)// 任务过去已完成是否要重新下载
         .setPriority(10)
         .build();
task.enqueue(listener);//异步执行任务
task.cancel();// 取消任务
task.execute(listener);// 同步执行任务
DownloadTask.enqueue(tasks, listener); //同时异步执行多个任务

配置 DownloadTask

  • setPreAllocateLength(boolean preAllocateLength) //在获取资源长度后,设置是否需要为文件预分配长度
  • setConnectionCount(@IntRange(from = 1) int connectionCount) //需要用几个线程来下载文件
  • setFilenameFromResponse(@Nullable Boolean filenameFromResponse)//如果没有提供文件名,是否使用服务器地址作为的文件名
  • setAutoCallbackToUIThread(boolean autoCallbackToUIThread) //是否在主线程通知调用者
  • setMinIntervalMillisCallbackProcess(int minIntervalMillisCallbackProcess) //通知调用者的频率,避免anr
  • setHeaderMapFields(Map<String, List > headerMapFields)//设置请求头
  • addHeader(String key, String value)//追加请求头
  • setPriority(int priority)//设置优先级,默认值是0,值越大下载优先级越高
  • setReadBufferSize(int readBufferSize)//设置读取缓存区大小,默认4096
  • setFlushBufferSize(int flushBufferSize)//设置写入缓存区大小,默认16384
  • setSyncBufferSize(int syncBufferSize)//写入到文件的缓冲区大小,默认65536
  • setSyncBufferIntervalMillis(int syncBufferIntervalMillis)//写入文件的最小时间间隔
  • setFilename(String filename)//设置下载文件名
  • setPassIfAlreadyCompleted(boolean passIfAlreadyCompleted)//如果文件已经下载完成,再次发起下载请求时,是否忽略下载,还是从头开始下载
  • setWifiRequired(boolean wifiRequired)//只允许wifi下载

案例

private DownloadTask createDownloadTask(ItemInfo itemInfo) {
    return new DownloadTask.Builder(itemInfo.url, new File(Utils.PARENT_PATH)) //设置下载地址和下载目录,这两个是必须的参数
        .setFilename(itemInfo.pkgName)//设置下载文件名,没提供的话先看 response header ,再看 url path(即启用下面那项配置)
        .setFilenameFromResponse(false)//是否使用 response header or url path 作为文件名,此时会忽略指定的文件名,默认false
        .setPassIfAlreadyCompleted(true)//如果文件已经下载完成,再次下载时,是否忽略下载,默认为true(忽略),设为false会从头下载
        .setConnectionCount(1)  //需要用几个线程来下载文件,默认根据文件大小确定;如果文件已经 split block,则设置后无效
        .setPreAllocateLength(false) //在获取资源长度后,设置是否需要为文件预分配长度,默认false
        .setMinIntervalMillisCallbackProcess(100) //通知调用者的频率,避免anr,默认3000
        .setWifiRequired(false)//是否只允许wifi下载,默认为false
        .setAutoCallbackToUIThread(true) //是否在主线程通知调用者,默认为true
        //.setHeaderMapFields(new HashMap<String, List<String>>())//设置请求头
        //.addHeader(String key, String value)//追加请求头
        .setPriority(0)//设置优先级,默认值是0,值越大下载优先级越高
        .setReadBufferSize(4096)//设置读取缓存区大小,默认4096
        .setFlushBufferSize(16384)//设置写入缓存区大小,默认16384
        .setSyncBufferSize(65536)//写入到文件的缓冲区大小,默认65536
        .setSyncBufferIntervalMillis(2000) //写入文件的最小时间间隔,默认2000
        .build();
}

任务队列的构建、开始和停止

DownloadContext.Builder builder = new DownloadContext.QueueSet()
        .setParentPathFile(parentFile)
        .setMinIntervalMillisCallbackProcess(150)
        .commit();
builder.bind(url1);
builder.bind(url2).addTag(key, value);
builder.bind(url3).setTag(tag);
builder.setListener(contextListener);

DownloadTask task = new DownloadTask.Builder(url4, parentFile).build();
builder.bindSetTask(task);

DownloadContext context = builder.build();
context.startOnParallel(listener);
context.stop();

获取任务状态

Status status = StatusUtil.getStatus(task);
Status status = StatusUtil.getStatus(url, parentPath, null);
Status status = StatusUtil.getStatus(url, parentPath, filename);

boolean isCompleted = StatusUtil.isCompleted(task);
boolean isCompleted = StatusUtil.isCompleted(url, parentPath, null);
boolean isCompleted = StatusUtil.isCompleted(url, parentPath, filename);

Status completedOrUnknown = StatusUtil.isCompletedOrUnknown(task);

获取断点信息

// 注意:任务完成后,断点信息将会被删除
BreakpointInfo info = OkDownload.with().breakpointStore().get(id);
BreakpointInfo info = StatusUtil.getCurrentInfo(url, parentPath, null);
BreakpointInfo info = StatusUtil.getCurrentInfo(url, parentPath, filename);
BreakpointInfo info = task.getInfo(); //断点信息将被缓存在任务对象中,即使任务已经完成了

设置任务监听

可以为任务设置五种不同类型的监听器,同时,也可以给任务和监听器建立1对1、1对多、多对1、多对多的关联。

项目提供了六种监听供选择:DownloadListener、DownloadListener1、DownloadListener3、DownloadListener3、DownloadListener4、DownloadListener4WithSpeed
具体流程详见 官方文档

设置多个监听

Combine Several DownloadListeners

DownloadListener listener1 = new DownloadListener1();
DownloadListener listener2 = new DownloadListener2();

DownloadListener combinedListener = new DownloadListenerBunch.Builder()
                   .append(listener1)
                   .append(listener2)
                   .build();

DownloadTask task = new DownloadTask.build(url, file).build();
task.enqueue(combinedListener);

动态更改任务的监听

Dynamic Change Listener For tasks

UnifiedListenerManager manager = new UnifiedListenerManager();
DownloadTask task = new DownloadTask.build(url, file).build();

DownloadListener listener1 = new DownloadListener1();
DownloadListener listener2 = new DownloadListener2();
DownloadListener listener3 = new DownloadListener3();
DownloadListener listener4 = new DownloadListener4();

manager.attachListener(task, listener1);
manager.attachListener(task, listener2);
manager.detachListener(task, listener2);
manager.addAutoRemoveListenersWhenTaskEnd(task.getId());// 当一个任务结束时,这个任务的所有监听器都被移除
manager.enqueueTaskWithUnifiedListener(task, listener3);// enqueue task to start.
manager.attachListener(task, listener4);

全局控制

Global Control

OkDownload.with().setMonitor(monitor);
DownloadDispatcher.setMaxParallelRunningCount(3); //最大并行下载数
RemitStoreOnSQLite.setRemitToDBDelayMillis(3000);
OkDownload.with().downloadDispatcher().cancelAll();
OkDownload.with().breakpointStore().remove(taskId);

组件注入

Injection Component

If you want to inject your components, please invoke following method before you using OkDownload:

OkDownload.Builder builder = new OkDownload.Builder(context)
    .downloadStore(downloadStore)
    .callbackDispatcher(callbackDispatcher)
    .downloadDispatcher(downloadDispatcher)
    .connectionFactory(connectionFactory)
    .outputStreamFactory(outputStreamFactory)
    .downloadStrategy(downloadStrategy)
    .processFileStrategy(processFileStrategy)
    .monitor(monitor);

OkDownload.setSingletonInstance(builder.build());

动态串行队列

Dynamic Serial Queue

DownloadSerialQueue serialQueue = new DownloadSerialQueue(commonListener);
serialQueue.enqueue(task1);
serialQueue.enqueue(task2);

serialQueue.pause();
serialQueue.resume();

int workingTaskId = serialQueue.getWorkingTaskId();
int waitingTaskCount = serialQueue.getWaitingTaskCount();

DownloadTask[] discardTasks = serialQueue.shutdown();

源码结构

OKDownload 下载框架 断点续传 MD 随笔 第1张

├── DownloadContext  //多个下载任务串/并行下载,使用QueueSet来做设置
├── DownloadContextListener
├── DownloadListener  //下载状态回调接口定义
├── DownloadMonitor
├── DownloadSerialQueue
├── DownloadTask  //单个下载任务
├── IRedirectHandler
├── OkDownload //入口类,负责下载任务装配
├── OkDownloadProvider //单纯为了获取上下文Context
├── RedirectUtil
├── SpeedCalculator //下载速度计算
├── StatusUtil //获取DownloadTask下载状态,检查下载文件是否已经下载完成等
├── UnifiedListenerManager //多个listener管理
├── core
│   ├── IdentifiedTask
│   ├── NamedRunnable  //可命名的线程实现
│   └── Util  //工具类
├── breakpoint
│   ├── BlockInfo //下载分块信息,记录当前块的下载进度,第0个记录整个下载任务的进度
│   ├── BreakpointInfo // BlockInfo聚合类,包含文件名、URL等信息
│   ├── BreakpointStore //下载过程中断点信息存储接口定义
│   └── BreakpointStoreOnCache //断点信息存储在缓存中的实现
│   ├── DownloadStore
│   └── KeyToIdMap
├── cause
│   ├── EndCause //结束状态
│   └── ResumeFailedCause //下载异常原因
├── connection
│   ├── DownloadConnection // 下载链接接口定义
│   └── DownloadUrlConnection //下载链接UrlConnection实现
├── dispatcher
│   ├── CallbackDispatcher //DownloadListener分发代理(是否回调到UI线程,默认为true)
│   └── DownloadDispatcher //下载任务线程分配
├── download
│   ├── BreakpointLocalCheck
│   ├── BreakpointRemoteCheck
│   ├── ConnectTrial
│   ├── DownloadCache //MultiPointOutputStream包裹类
│   ├── DownloadCall //下载任务线程,包含DownloadTask、DownloadChain的list以及DownloadCache
│   ├── DownloadChain //持有DownloadTask等对象,链式调用各connect及fetch的Interceptor,开启下载任务
│   └── DownloadStrategy //下载策略,包括分包策略、下载文件命名策略以及response是否可用
├── exception //各种异常
│   ├── DownloadSecurityException
│   ├── FileBusyAfterRunException
│   ├── InterruptException
│   ├── NetworkPolicyException
│   ├── PreAllocateException
│   ├── ResumeFailedException
│   ├── RetryException
│   └── ServerCancelledException
├── file
│   ├── DownloadOutputStream //输出流接口定义
│   ├── DownloadUriOutputStream //Uri输出流实现
│   ├── FileLock
│   ├── MultiPointOutputStream //多block输出流管理
│   └── ProcessFileStrategy //下载过程中文件处理逻辑
├── interceptor
│   ├── BreakpointInterceptor //connect时分块,fetch时循环调用FetchDataInterceptor获取数据
│   ├── FetchDataInterceptor //fetch时读写流数据,记录增加bytes长度
│   ├── Interceptor
│   ├── RetryInterceptor //错误处理、connect时重试机制,fetch结束时同步输出流,确保写入数据完整
│   └── connect
│       ├── CallServerInterceptor //启动DownloadConnection
│       └── HeaderInterceptor //添加头信息,调用connectStart、connectEnd
└── listener //多种回调及辅助接口
│   ├── DownloadListener1
│   ├── DownloadListener2
│   ├── DownloadListener3
│   ├── DownloadListener4
│   ├── DownloadListener4WithSpeed
│   ├── DownloadListenerBunch
│   └── assist
│       ├── Listener1Assist
│       ├── Listener4Assist
│       ├── Listener4SpeedAssistExtend
│       ├── ListenerAssist
│       └── ListenerModelHandler

使用案例

必要的配置

  • 1、申请两个权限:WRITE_EXTERNAL_STORAGE 和 REQUEST_INSTALL_PACKAGES
  • 2、配置 FileProvider
  • 3、添加依赖

Activity

public class DownloadActivity extends ListActivity {
    static final String URL1 = "https://oatest.dgcb.com.cn:62443/mstep/installpkg/yidongyingxiao/90.0/DGMmarket_rtx.apk";
    static final String URL2 = "https://cdn.llscdn.com/yy/files/xs8qmxn8-lls-LLS-5.8-800-20171207-111607.apk";
    static final String URL3 = "https://downapp.baidu.com/appsearch/AndroidPhone/1.0.78.155/1/1012271b/20190404124002/appsearch_AndroidPhone_1-0-78-155_1012271b.apk";

    ProgressBar progressBar;
    List<ItemInfo> list;
    HashMap<String, DownloadTask> map = new HashMap<>();

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        String[] array = {"使用DownloadListener4WithSpeed",
            "使用DownloadListener3",
            "使用DownloadListener2",
            "使用DownloadListener3",
            "使用DownloadListener",
            "=====删除下载的文件,并重新启动Activity=====",
            "查看任务1的状态",
            "查看任务2的状态",
            "查看任务3的状态",
            "查看任务4的状态",
            "查看任务5的状态",};
        setListAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, array));
        list = Arrays.asList(new ItemInfo(URL1, "com.yitong.mmarket.dg"),
            new ItemInfo(URL1, "哎"),
            new ItemInfo(URL2, "英语流利说"),
            new ItemInfo(URL2, "百度手机助手"),
            new ItemInfo(URL3, "哎哎哎"));
        progressBar = new ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal);
        progressBar.setIndeterminate(false);
        getListView().addFooterView(progressBar);

        new File(Utils.PARENT_PATH).mkdirs();
        //OkDownload.setSingletonInstance(Utils.buildOkDownload(getApplicationContext()));//注意只能执行一次,否则报错
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        //OkDownload.with().downloadDispatcher().cancelAll();
        for (String key : map.keySet()) {
            DownloadTask task = map.get(key);
            if (task != null) {
                task.cancel();
            }
        }
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        switch (position) {
            case 0:
            case 1:
            case 2:
            case 3:
            case 4:
                download(position);
                break;
            case 5:
                Utils.deleateFiles(new File(Utils.PARENT_PATH), null, false);
                recreate();
                break;
            default:
                ItemInfo itemInfo = list.get(position - 6);
                DownloadTask task = map.get(itemInfo.pkgName);
                if (task != null) {
                    Toast.makeText(this, "状态为:" + StatusUtil.getStatus(task).name(), Toast.LENGTH_SHORT).show();
                }

                BreakpointInfo info = StatusUtil.getCurrentInfo(itemInfo.url, Utils.PARENT_PATH, itemInfo.pkgName);
                //BreakpointInfo info = StatusUtil.getCurrentInfo(task);
                if (info != null) {
                    float percent = (float) info.getTotalOffset() / info.getTotalLength() * 100;
                    Log.i("bqt", "【当前进度】" + percent + "%");
                    progressBar.setMax((int) info.getTotalLength());
                    progressBar.setProgress((int) info.getTotalOffset());
                } else {
                    Log.i("bqt", "【任务不存在】");
                }
                break;
        }
    }

    private void download(int position) {
        ItemInfo itemInfo = list.get(position);
        DownloadTask task = map.get(itemInfo.pkgName);
        // 0:没有下载  1:下载中  2:暂停  3:完成
        if (itemInfo.status == 0) {
            if (task == null) {
                task = createDownloadTask(itemInfo);
                map.put(itemInfo.pkgName, task);
            }
            task.enqueue(createDownloadListener(position));
            itemInfo.status = 1; //更改状态
            Toast.makeText(this, "开始下载", Toast.LENGTH_SHORT).show();
        } else if (itemInfo.status == 1) {//下载中
            if (task != null) {
                task.cancel();
            }
            itemInfo.status = 2;
            Toast.makeText(this, "暂停下载", Toast.LENGTH_SHORT).show();
        } else if (itemInfo.status == 2) {
            if (task != null) {
                task.enqueue(createDownloadListener(position));
            }
            itemInfo.status = 1;
            Toast.makeText(this, "继续下载", Toast.LENGTH_SHORT).show();
        } else if (itemInfo.status == 3) {//下载完成的,直接跳转安装APP
            Utils.launchOrInstallApp(this, itemInfo.pkgName);
            Toast.makeText(this, "下载完成", Toast.LENGTH_SHORT).show();
        }
    }

    private DownloadTask createDownloadTask(ItemInfo itemInfo) {
        return new DownloadTask.Builder(itemInfo.url, new File(Utils.PARENT_PATH)) //设置下载地址和下载目录,这两个是必须的参数
            .setFilename(itemInfo.pkgName)//设置下载文件名,没提供的话先看 response header ,再看 url path(即启用下面那项配置)
            .setFilenameFromResponse(false)//是否使用 response header or url path 作为文件名,此时会忽略指定的文件名,默认false
            .setPassIfAlreadyCompleted(true)//如果文件已经下载完成,再次下载时,是否忽略下载,默认为true(忽略),设为false会从头下载
            .setConnectionCount(1)  //需要用几个线程来下载文件,默认根据文件大小确定;如果文件已经 split block,则设置后无效
            .setPreAllocateLength(false) //在获取资源长度后,设置是否需要为文件预分配长度,默认false
            .setMinIntervalMillisCallbackProcess(100) //通知调用者的频率,避免anr,默认3000
            .setWifiRequired(false)//是否只允许wifi下载,默认为false
            .setAutoCallbackToUIThread(true) //是否在主线程通知调用者,默认为true
            //.setHeaderMapFields(new HashMap<String, List<String>>())//设置请求头
            //.addHeader(String key, String value)//追加请求头
            .setPriority(0)//设置优先级,默认值是0,值越大下载优先级越高
            .setReadBufferSize(4096)//设置读取缓存区大小,默认4096
            .setFlushBufferSize(16384)//设置写入缓存区大小,默认16384
            .setSyncBufferSize(65536)//写入到文件的缓冲区大小,默认65536
            .setSyncBufferIntervalMillis(2000) //写入文件的最小时间间隔,默认2000
            .build();
    }

    private DownloadListener createDownloadListener(int position) {
        switch (position) {
            case 0:
                return new MyDownloadListener4WithSpeed(list.get(position), progressBar);
            case 1:
                return new MyDownloadListener3(list.get(position), progressBar);
            case 2:
                return new MyDownloadListener2(list.get(position), progressBar);
            case 3:
                return new MyDownloadListener1(list.get(position), progressBar);
            default:
                return new MyDownloadListener(list.get(position), progressBar);
        }
    }
}

辅助工具类

public class Utils {
    public static final String PARENT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/aatest";

    public static void launchOrInstallApp(Context context, String pkgName) {
        if (!TextUtils.isEmpty(pkgName)) {
            Intent intent = context.getPackageManager().getLaunchIntentForPackage(pkgName);
            if (intent == null) {//如果未安装,则先安装
                installApk(context, new File(PARENT_PATH, pkgName));
            } else {//如果已安装,跳转到应用
                context.startActivity(intent);
            }
        } else {
            Toast.makeText(context, "包名为空!", Toast.LENGTH_SHORT).show();
            installApk(context, new File(PARENT_PATH, pkgName));
        }
    }

    //1、申请两个权限:WRITE_EXTERNAL_STORAGE 和 REQUEST_INSTALL_PACKAGES ;2、配置FileProvider
    public static void installApk(Context context, File file) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        Uri uri;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file);
            //【content://{$authority}/external/temp.apk】或【content://{$authority}/files/bqt/temp2.apk】
        } else {
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//【file:///storage/emulated/0/temp.apk】
            uri = Uri.fromFile(file);
        }
        Log.i("bqt", "【Uri】" + uri);
        intent.setDataAndType(uri, "application/vnd.android.package-archive");
        context.startActivity(intent);
    }

    public static OkDownload buildOkDownload(Context context) {
        return new OkDownload.Builder(context.getApplicationContext())
            .downloadStore(Util.createDefaultDatabase(context)) //断点信息存储的位置,默认是SQLite数据库
            .callbackDispatcher(new CallbackDispatcher()) //监听回调分发器,默认在主线程回调
            .downloadDispatcher(new DownloadDispatcher()) //下载管理机制,最大下载任务数、同步异步执行下载任务的处理
            .connectionFactory(Util.createDefaultConnectionFactory()) //选择网络请求框架,默认是OkHttp
            .outputStreamFactory(new DownloadUriOutputStream.Factory()) //构建文件输出流DownloadOutputStream,是否支持随机位置写入
            .processFileStrategy(new ProcessFileStrategy()) //多文件写文件的方式,默认是根据每个线程写文件的不同位置,支持同时写入
            //.monitor(monitor); //下载状态监听
            .downloadStrategy(new DownloadStrategy())//下载策略,文件分为几个线程下载
            .build();
    }

    /**
     * 删除一个文件,或删除一个目录下的所有文件
     *
     * @param dirFile      要删除的目录,可以是一个文件
     * @param filter       对要删除的文件的匹配规则(不作用于目录),如果要删除所有文件请设为 null
     * @param isDeleateDir 是否删除目录,false时只删除目录下的文件而不删除目录
     */
    public static void deleateFiles(File dirFile, FilenameFilter filter, boolean isDeleateDir) {
        if (dirFile.isDirectory()) {//是目录
            for (File file : dirFile.listFiles()) {
                deleateFiles(file, filter, isDeleateDir);//递归
            }
            if (isDeleateDir) {
                System.out.println("目录【" + dirFile.getAbsolutePath() + "】删除" + (dirFile.delete() ? "成功" : "失败"));//必须在删除文件后才能删除目录
            }
        } else if (dirFile.isFile()) {//是文件。注意 isDirectory 为 false 并非就等价于 isFile 为 true
            String symbol = isDeleateDir ? "\t" : "";
            if (filter == null || filter.accept(dirFile.getParentFile(), dirFile.getName())) {//是否满足匹配规则
                System.out.println(symbol + "- 文件【" + dirFile.getAbsolutePath() + "】删除" + (dirFile.delete() ? "成功" : "失败"));
            } else {
                System.out.println(symbol + "+ 文件【" + dirFile.getAbsolutePath() + "】不满足匹配规则,不删除");
            }
        } else {
            System.out.println("文件不存在");
        }
    }

    public static void dealEnd(Context context, ItemInfo itemInfo, @NonNull EndCause cause) {
        if (cause == EndCause.COMPLETED) {
            Toast.makeText(context, "任务完成", Toast.LENGTH_SHORT).show();
            itemInfo.status = 3; //修改状态
            Utils.launchOrInstallApp(context, itemInfo.pkgName);
        } else {
            itemInfo.status = 2; //修改状态
            if (cause == EndCause.CANCELED) {
                Toast.makeText(context, "任务取消", Toast.LENGTH_SHORT).show();
            } else if (cause == EndCause.ERROR) {
                Log.i("bqt", "【任务出错】");
            } else if (cause == EndCause.FILE_BUSY || cause == EndCause.SAME_TASK_BUSY || cause == EndCause.PRE_ALLOCATE_FAILED) {
                Log.i("bqt", "【taskEnd】" + cause.name());
            }
        }
    }
}

辅助Bean

public class ItemInfo {
    String url;
    String pkgName; //包名
    int status;  // 0:没有下载 1:下载中 2:暂停 3:完成

    public ItemInfo(String url, String pkgName) {
        this.url = url;
        this.pkgName = pkgName;
    }
}

DownloadListener4WithSpeed

public class MyDownloadListener4WithSpeed extends DownloadListener4WithSpeed {
    private ItemInfo itemInfo;
    private long totalLength;
    private String readableTotalLength;
    private ProgressBar progressBar;//谨防内存泄漏
    private Context context;//谨防内存泄漏

    public MyDownloadListener4WithSpeed(ItemInfo itemInfo, ProgressBar progressBar) {
        this.itemInfo = itemInfo;
        this.progressBar = progressBar;
        context = progressBar.getContext();
    }

    @Override
    public void taskStart(@NonNull DownloadTask task) {
        Log.i("bqt", "【1、taskStart】");
    }

    @Override
    public void infoReady(@NonNull DownloadTask task, @NonNull BreakpointInfo info, boolean fromBreakpoint, @NonNull Listener4SpeedAssistExtend.Listener4SpeedModel model) {
        totalLength = info.getTotalLength();
        readableTotalLength = Util.humanReadableBytes(totalLength, true);
        Log.i("bqt", "【2、infoReady】当前进度" + (float) info.getTotalOffset() / totalLength * 100 + "%" + "," + info.toString());
        progressBar.setMax((int) totalLength);
    }

    @Override
    public void connectStart(@NonNull DownloadTask task, int blockIndex, @NonNull Map<String, List<String>> requestHeaders) {
        Log.i("bqt", "【3、connectStart】" + blockIndex);
    }

    @Override
    public void connectEnd(@NonNull DownloadTask task, int blockIndex, int responseCode, @NonNull Map<String, List<String>> responseHeaders) {
        Log.i("bqt", "【4、connectEnd】" + blockIndex + "," + responseCode);
    }

    @Override
    public void progressBlock(@NonNull DownloadTask task, int blockIndex, long currentBlockOffset, @NonNull SpeedCalculator blockSpeed) {
        //Log.i("bqt", "【5、progressBlock】" + blockIndex + "," + currentBlockOffset);
    }

    @Override
    public void progress(@NonNull DownloadTask task, long currentOffset, @NonNull SpeedCalculator taskSpeed) {
        String readableOffset = Util.humanReadableBytes(currentOffset, true);
        String progressStatus = readableOffset + "/" + readableTotalLength;
        String speed = taskSpeed.speed();
        float percent = (float) currentOffset / totalLength * 100;
        Log.i("bqt", "【6、progress】" + currentOffset + "[" + progressStatus + "],速度:" + speed + ",进度:" + percent + "%");
        progressBar.setProgress((int) currentOffset);
    }

    @Override
    public void blockEnd(@NonNull DownloadTask task, int blockIndex, BlockInfo info, @NonNull SpeedCalculator blockSpeed) {
        Log.i("bqt", "【7、blockEnd】" + blockIndex);
    }

    @Override
    public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause, @NonNull SpeedCalculator taskSpeed) {
        Log.i("bqt", "【8、taskEnd】" + cause.name() + ":" + (realCause != null ? realCause.getMessage() : "无异常"));
        Utils.dealEnd(context, itemInfo, cause);
    }
}

源码分析

详见这篇文章

OkDownload

首先看一下OkDownload这个类,这个类定义了所有的下载策略,我们可以自定义一些下载策略,可以通过OkDownload的Builder构造自定义的一个OkDownload实例,再通过OkDownload.setSingletonInstance进行设置:

OkDownload.Builder builder = new OkDownload.Builder(context)
    .downloadStore(downloadStore) //断点信息存储的位置,默认是SQLite数据库 
    .callbackDispatcher(callbackDispatcher) //监听回调分发器,默认在主线程回调 
    .downloadDispatcher(downloadDispatcher) //下载管理机制,最大下载任务数、同步异步执行下载任务的处理
    .connectionFactory(connectionFactory) //选择网络请求框架,默认是OkHttp 
    .outputStreamFactory(outputStreamFactory) //构建文件输出流DownloadOutputStream,是否支持随机位置写入
    .downloadStrategy(downloadStrategy) //下载策略,文件分为几个线程下载
    .processFileStrategy(processFileStrategy) //多文件写文件的方式,默认是根据每个线程写文件的不同位置,支持同时写入
    .monitor(monitor); //下载状态监听 
OkDownload.setSingletonInstance(builder.build());

DownloadTask

DownloadTask下载任务类,可通过它的Builder来构造一个下载任务,我们看它是如何执行的:

public void execute(DownloadListener listener) {
    this.listener = listener;
    OkDownload.with().downloadDispatcher().execute(this);
}

public void enqueue(DownloadListener listener) {
    this.listener = listener;
    OkDownload.with().downloadDispatcher().enqueue(this);
}

可以看到都是通过downloadDispatcher来执行下载任务的,默认的downloadDispatcher是一个DownloadDispatcher实例,我们以同步执行一个下载任务为例,看它是如何下载的:

public void execute(DownloadTask task) {
    Util.d(TAG, "execute: " + task);
    final DownloadCall call;
    synchronized (this) {
        if (inspectCompleted(task)) return;
        if (inspectForConflict(task)) return;

        call = DownloadCall.create(task, false, store); //创建DownloadCall对象
        runningSyncCalls.add(call);
    }
    syncRunCall(call); //调用DownloadCall的run()方法,最终调用了其execute()方法
}

void syncRunCall(DownloadCall call) {
    call.run();
}
public abstract class NamedRunnable implements Runnable {
    @Override
    public final void run() {
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(name);
        try {
            execute();
        } catch (InterruptedException e) {
            interrupted(e);
        } finally {
            Thread.currentThread().setName(oldName);
            finished();
        }
    }

    protected abstract void execute() throws InterruptedException;
    //...
}

大致流程:
在execute方法里将一个DownloadTask实例又封装为了一个DownloadCall对象,然后在syncRunCall方法里执行了DownloadCall对象的run方法。通过看DownloadCall源码可以知道该类继承自NamedRunnable,而NamedRunnable实现了Runnable,在run方法里调用了execute()方法。

调用enqueue执行任务最终则是调用 getExecutorService().execute(call)来异步执行的:

private synchronized void enqueueIgnorePriority(DownloadTask task) {
    final DownloadCall call = DownloadCall.create(task, true, store);
    if (runningAsyncSize() < maxParallelRunningCount) {
        runningAsyncCalls.add(call);
        getExecutorService().execute(call);
    } else {
        readyAsyncCalls.add(call);
    }
}

DownloadCall

先看一下DownloadCall是如何实现execute方法的,该方法比较长,首先执行的是inspectTaskStart()方法:

private void inspectTaskStart() {
    store.onTaskStart(task.getId());
    OkDownload.with().callbackDispatcher().dispatch().taskStart(task);
}

这里的store是调用BreakpointStoreOnSQLitecreateRemitSelf方法生成的一个实例:

public DownloadStore createRemitSelf() {
    return new RemitStoreOnSQLite(this);
}

可以看到是RemitStoreOnSQLite的一个实例,其主要用来保存任务及断点信息至本地数据库。RemitStoreOnSQLite里持有BreakpointStoreOnSQLite对象,BreakpointStoreOnSQLite里面包含了BreakpointSQLiteHelper(用于操作数据)和BreakpointStoreOnCache(用于做数据操作之前的数据缓存)。

最终会调用上述store的syncCacheToDB方法,先删除数据库中的任务信息,若缓存(创建BreakpointStoreOnCache对象时,会调用loadToCache方法将数据库中所有任务信息进行缓存)中有该任务,则检查任务信息是否合法,若合法则再次将该任务及断点信息保存在本地数据库中。

@Override 
public void syncCacheToDB(int id) throws IOException {
    sqLiteHelper.removeInfo(id); //先删除数据库中的任务信息

    final BreakpointInfo info = sqliteCache.get(id);
    if (info == null || info.getFilename() == null || info.getTotalOffset() <= 0) return; //检查任务信息是否合法

    sqLiteHelper.insert(info); //若合法则再次将该任务及断点信息保存在本地数据库中
}

inspectTaskStart方法结束后,会进入一个do-while循环,首先做一些下载前的准备工作:

  • 1.判断当前任务的下载链接长度是否大于0,否则就抛出异常;
  • 2.从缓存中获取任务的断点信息,若没有断点信息,则创建断点信息并保存至数据库;
  • 3.创建带缓存的下载输出流;
  • 4.访问下载链接判断断点信息是否合理;
  • 5.确定文件路径后等待文件锁释放;
  • 6.判断缓存中是否有相同的任务,若有则复用缓存中的任务的分块信息;
  • 7.检查断点信息是否是可恢复的,若不可恢复,则根据文件大小进行分块,重新下载,否则继续进行下一步;
  • 8.判断断点信息是否是脏数据(文件存在且断点信息正确且下载链接支持断点续传);
  • 9.若是脏数据则根据文件大小进行分块,重新开始下载,否则从断点位置开始下载;
  • 10.开始下载。

文件分成多少块进行下载由DownloadStrategy决定的:文件大小在0-1MB、1-5MB、5-50MB、50-100MB、100MB以上时分别开启1、2、3、4、5个线程进行下载。

我们重点看一下下载部分的源码,也就是start(cache,info)方法:

void start(final DownloadCache cache, BreakpointInfo info) throws InterruptedException {
    final int blockCount = info.getBlockCount();
    final List<DownloadChain> blockChainList = new ArrayList<>(info.getBlockCount());
    final List<Integer> blockIndexList = new ArrayList<>();
    for (int i = 0; i < blockCount; i++) {
        final BlockInfo blockInfo = info.getBlock(i);
        if (Util.isCorrectFull(blockInfo.getCurrentOffset(), blockInfo.getContentLength())) {
            continue;
        }

        Util.resetBlockIfDirty(blockInfo);
        final DownloadChain chain = DownloadChain.createChain(i, task, info, cache, store);
        blockChainList.add(chain);
        blockIndexList.add(chain.getBlockIndex());
    }

    if (canceled) {
        return;
    }

    cache.getOutputStream().setRequireStreamBlocks(blockIndexList);

    startBlocks(blockChainList);
}

可以看到它是分块下载的,每一个分块都是一个DownloadChain实例,DownloadChain实现了Runnable接口。

继续看DownloadCall的startBlocks方法:

void startBlocks(List<DownloadChain> tasks) throws InterruptedException {
    ArrayList<Future> futures = new ArrayList<>(tasks.size());
    try {
        for (DownloadChain chain : tasks) {
            futures.add(submitChain(chain));
        }
    //...
}
Future<?> submitChain(DownloadChain chain) {
    return EXECUTOR.submit(chain);
}

对于每一个分块任务,都调用了submitChain方法,即由一个线程池去处理每一个DownloadChain分块。

DownloadChain

我们看一下DownloadChain的start方法:

void start() throws IOException {
    final CallbackDispatcher dispatcher = OkDownload.with().callbackDispatcher();
    // 处理请求拦截链,connect chain
    final RetryInterceptor retryInterceptor = new RetryInterceptor();
    final BreakpointInterceptor breakpointInterceptor = new BreakpointInterceptor();
    connectInterceptorList.add(retryInterceptor);
    connectInterceptorList.add(breakpointInterceptor);
    connectInterceptorList.add(new HeaderInterceptor());
    connectInterceptorList.add(new CallServerInterceptor());

    connectIndex = 0;
    final DownloadConnection.Connected connected = processConnect();
    if (cache.isInterrupt()) {
        throw InterruptException.SIGNAL;
    }

    dispatcher.dispatch().fetchStart(task, blockIndex, getResponseContentLength());
    // 获取数据拦截链,fetch chain
    final FetchDataInterceptor fetchDataInterceptor =
            new FetchDataInterceptor(blockIndex, connected.getInputStream(),
                    getOutputStream(), task);
    fetchInterceptorList.add(retryInterceptor);
    fetchInterceptorList.add(breakpointInterceptor);
    fetchInterceptorList.add(fetchDataInterceptor);

    fetchIndex = 0;
    final long totalFetchedBytes = processFetch();
    dispatcher.dispatch().fetchEnd(task, blockIndex, totalFetchedBytes);
}

可以看到它主要使用责任链模式进行了两个链式调用:处理请求拦截链获取数据拦截链

  • 处理请求拦截链包含了RetryInterceptor重试拦截器、BreakpointInterceptor断点拦截器、RedirectInterceptor重定向拦截器、HeaderInterceptor头部信息处理拦截器、CallServerInterceptor请求拦截器,该链式调用过程会逐个调用拦截器的interceptConnect方法。
  • 获取数据拦截链包含了RetryInterceptor重试拦截器、BreakpointInterceptor断点拦截器、RedirectInterceptor重定向拦截器、HeaderInterceptor头部信息处理拦截器、FetchDataInterceptor获取数据拦截器,该链式调用过程会逐个调用拦截器的interceptFetch方法。
public class RetryInterceptor implements Interceptor.Connect, Interceptor.Fetch {

    @NonNull @Override
    public DownloadConnection.Connected interceptConnect(DownloadChain chain) throws IOException {
        //...
        return chain.processConnect();
    }

    @Override
    public long interceptFetch(DownloadChain chain) throws IOException {
        //...
        return chain.processFetch();
    }
}

OKDownload 下载框架 断点续传 MD 随笔 第2张

OKDownload 下载框架 断点续传 MD 随笔 第3张

每一个DownloadChain都完成后,最终会调用inspectTaskEnd方法,从数据库中删除该任务,并回调通知任务完成。这样,一个完整的下载任务就完成了。

总体流程

总体流程如下:

OKDownload 下载框架 断点续传 MD 随笔 第4张

2019-4-8

附件列表

     

    扫码关注我们
    微信号:SRE实战
    拒绝背锅 运筹帷幄