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
具體詳見官方文檔 Simple-Use-Guideline 和 Advanced-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); //同時異步執行多個任務
案例服務器
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
必要的配置
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()); } } } }
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; } }
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的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下載任務類,可經過它的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是如何實現execute
方法的,該方法比較長,首先執行的是inspectTaskStart()
方法:
private void inspectTaskStart() { store.onTaskStart(task.getId()); OkDownload.with().callbackDispatcher().dispatch().taskStart(task); }
這裏的store是調用BreakpointStoreOnSQLite
的createRemitSelf
方法生成的一個實例:
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循環,首先作一些下載前的準備工做:
文件分紅多少塊進行下載由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的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