zoukankan      html  css  js  c++  java
  • OKDownload 下载框架 断点续传 MD

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

    目录

    简介

    项目地址

    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();

    源码结构

    ├── 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 ? "	" : "";
                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();
        }
    }

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

    总体流程

    总体流程如下:

    2019-4-8

    附件列表

    • 相关阅读:
      An internal error occurred during: "Launching MVC on Tomcat 6.x". java.lang.NullPointerException
      bat批处理文件夹内文件名的提取
      人脸识别-常用的人脸数据库
      WPF RichTextBox 做内容展示框 滚动条控制判定是否阅读完成
      WPF+通过配置文件生成菜单(Menu)+源码
      程序员也可以浪漫----倾情奉献一份你值得拥有的浪漫网站源码(情人节快来了~)
      这世界唯一的你:最美程序媛走红网络
      20分钟读懂程序集
      简介4种同步方法的实现
      共享文件夹:The user has not been granted the requested logon type at this computer
    • 原文地址:https://www.cnblogs.com/baiqiantao/p/10679677.html
    Copyright © 2011-2022 走看看