OKDownload 下載框架 斷點續傳 MD

Markdown版本筆記 個人GitHub首頁 個人博客 個人微信 個人郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

目錄

簡介

項目地址java

A Reliable, Flexible, Fast and Powerful download engine.android

引入git

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;是一個支持多線程,多任務,斷點續傳,可靠,靈活,高性能以及強大的下載引擎。github

對比FileDownloader的優點sql

  • 單元測試覆蓋率很高,從而保證框架的可靠性。
  • 簡單的接口設計。
  • 支持任務優先級。
  • 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對多、多對一、多對多的關聯。微信

項目提供了六種監聽供選擇:DownloadListener、DownloadListener一、DownloadListener三、DownloadListener三、DownloadListener四、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

使用案例

必要的配置

  • 一、申請兩個權限:WRITE_EXTERNAL_STORAGE 和 REQUEST_INSTALL_PACKAGES
  • 二、配置 FileProvider
  • 三、添加依賴

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

    //一、申請兩個權限:WRITE_EXTERNAL_STORAGE 和 REQUEST_INSTALL_PACKAGES ;二、配置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", "【一、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", "【二、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", "【三、connectStart】" + blockIndex);
    }

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

    @Override
    public void progressBlock(@NonNull DownloadTask task, int blockIndex, long currentBlockOffset, @NonNull SpeedCalculator blockSpeed) {
        //Log.i("bqt", "【五、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", "【六、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", "【七、blockEnd】" + blockIndex);
    }

    @Override
    public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause, @NonNull SpeedCalculator taskSpeed) {
        Log.i("bqt", "【八、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以上時分別開啓一、二、三、四、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

附件列表

相關文章
相關標籤/搜索