Markdown版本筆記 | 我的GitHub首頁 | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
目錄
簡介
基本使用
開始一個任務
配置 DownloadTask
任務隊列的構建、開始和停止
獲取任務狀態
獲取斷點信息
設置任務監聽
設置多個監聽
動態更改任務的監聽
全局控制
組件注入
動態串行隊列
源碼結構
使用案例
Activity
輔助工具類
輔助Bean
DownloadListener4WithSpeed
源碼分析
OkDownload
DownloadTask
DownloadCall
DownloadChain
總體流程
簡介
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-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); //同時異步執行多個任務
配置 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 ? "\t" : "";
if (filter == null || filter.accept(dirFile.getParentFile(), dirFile.getName())) {//是否滿足匹配規則
System.out.println(symbol + "- 文件【" + dirFile.getAbsolutePath() + "】刪除" + (dirFile.delete() ? "成功" : "失敗"));
} else {
System.out.println(symbol + "+ 文件【" + dirFile.getAbsolutePath() + "】不滿足匹配規則,不刪除");
}
} else {
System.out.println("文件不存在");
}
}
public static void dealEnd(Context context, ItemInfo itemInfo, @NonNull EndCause cause) {
if (cause == EndCause.COMPLETED) {
Toast.makeText(context, "任務完成", Toast.LENGTH_SHORT).show();
itemInfo.status = 3; //修改狀態
Utils.launchOrInstallApp(context, itemInfo.pkgName);
} else {
itemInfo.status = 2; //修改狀態
if (cause == EndCause.CANCELED) {
Toast.makeText(context, "任務取消", Toast.LENGTH_SHORT).show();
} else if (cause == EndCause.ERROR) {
Log.i("bqt", "【任務出錯】");
} else if (cause == EndCause.FILE_BUSY || cause == EndCause.SAME_TASK_BUSY || cause == EndCause.PRE_ALLOCATE_FAILED) {
Log.i("bqt", "【taskEnd】" + cause.name());
}
}
}
}
輔助Bean
public class ItemInfo {
String url;
String pkgName; //包名
int status; // 0:沒有下載 1:下載中 2:暫停 3:完成
public ItemInfo(String url, String pkgName) {
this.url = url;
this.pkgName = pkgName;
}
}
DownloadListener4WithSpeed
public class MyDownloadListener4WithSpeed extends DownloadListener4WithSpeed {
private ItemInfo itemInfo;
private long totalLength;
private String readableTotalLength;
private ProgressBar progressBar;//謹防內存泄漏
private Context context;//謹防內存泄漏
public MyDownloadListener4WithSpeed(ItemInfo itemInfo, ProgressBar progressBar) {
this.itemInfo = itemInfo;
this.progressBar = progressBar;
context = progressBar.getContext();
}
@Override
public void taskStart(@NonNull DownloadTask task) {
Log.i("bqt", "【1、taskStart】");
}
@Override
public void infoReady(@NonNull DownloadTask task, @NonNull BreakpointInfo info, boolean fromBreakpoint, @NonNull Listener4SpeedAssistExtend.Listener4SpeedModel model) {
totalLength = info.getTotalLength();
readableTotalLength = Util.humanReadableBytes(totalLength, true);
Log.i("bqt", "【2、infoReady】當前進度" + (float) info.getTotalOffset() / totalLength * 100 + "%" + "," + info.toString());
progressBar.setMax((int) totalLength);
}
@Override
public void connectStart(@NonNull DownloadTask task, int blockIndex, @NonNull Map<String, List<String>> requestHeaders) {
Log.i("bqt", "【3、connectStart】" + blockIndex);
}
@Override
public void connectEnd(@NonNull DownloadTask task, int blockIndex, int responseCode, @NonNull Map<String, List<String>> responseHeaders) {
Log.i("bqt", "【4、connectEnd】" + blockIndex + "," + responseCode);
}
@Override
public void progressBlock(@NonNull DownloadTask task, int blockIndex, long currentBlockOffset, @NonNull SpeedCalculator blockSpeed) {
//Log.i("bqt", "【5、progressBlock】" + blockIndex + "," + currentBlockOffset);
}
@Override
public void progress(@NonNull DownloadTask task, long currentOffset, @NonNull SpeedCalculator taskSpeed) {
String readableOffset = Util.humanReadableBytes(currentOffset, true);
String progressStatus = readableOffset + "/" + readableTotalLength;
String speed = taskSpeed.speed();
float percent = (float) currentOffset / totalLength * 100;
Log.i("bqt", "【6、progress】" + currentOffset + "[" + progressStatus + "],速度:" + speed + ",進度:" + percent + "%");
progressBar.setProgress((int) currentOffset);
}
@Override
public void blockEnd(@NonNull DownloadTask task, int blockIndex, BlockInfo info, @NonNull SpeedCalculator blockSpeed) {
Log.i("bqt", "【7、blockEnd】" + blockIndex);
}
@Override
public void taskEnd(@NonNull DownloadTask task, @NonNull EndCause cause, @Nullable Exception realCause, @NonNull SpeedCalculator taskSpeed) {
Log.i("bqt", "【8、taskEnd】" + cause.name() + ":" + (realCause != null ? realCause.getMessage() : "無異常"));
Utils.dealEnd(context, itemInfo, cause);
}
}
源碼分析
OkDownload
首先看一下OkDownload這個類,這個類定義了所有的下載策略,我們可以自定義一些下載策略,可以通過OkDownload的Builder構造自定義的一個OkDownload實例,再通過OkDownload.setSingletonInstance
進行設置:
OkDownload.Builder builder = new OkDownload.Builder(context)
.downloadStore(downloadStore) //斷點信息存儲的位置,默認是SQLite數據庫
.callbackDispatcher(callbackDispatcher) //監聽回調分發器,默認在主線程回調
.downloadDispatcher(downloadDispatcher) //下載管理機制,最大下載任務數、同步異步執行下載任務的處理
.connectionFactory(connectionFactory) //選擇網絡請求框架,默認是OkHttp
.outputStreamFactory(outputStreamFactory) //構建文件輸出流DownloadOutputStream,是否支持隨機位置寫入
.downloadStrategy(downloadStrategy) //下載策略,文件分為幾個線程下載
.processFileStrategy(processFileStrategy) //多文件寫文件的方式,默認是根據每個線程寫文件的不同位置,支持同時寫入
.monitor(monitor); //下載狀態監聽
OkDownload.setSingletonInstance(builder.build());
DownloadTask
DownloadTask下載任務類,可通過它的Builder來構造一個下載任務,我們看它是如何執行的:
public void execute(DownloadListener listener) {
this.listener = listener;
OkDownload.with().downloadDispatcher().execute(this);
}
public void enqueue(DownloadListener listener) {
this.listener = listener;
OkDownload.with().downloadDispatcher().enqueue(this);
}
可以看到都是通過downloadDispatcher來執行下載任務的,默認的downloadDispatcher是一個DownloadDispatcher
實例,我們以同步執行一個下載任務為例,看它是如何下載的:
public void execute(DownloadTask task) {
Util.d(TAG, "execute: " + task);
final DownloadCall call;
synchronized (this) {
if (inspectCompleted(task)) return;
if (inspectForConflict(task)) return;
call = DownloadCall.create(task, false, store); //創建DownloadCall對象
runningSyncCalls.add(call);
}
syncRunCall(call); //調用DownloadCall的run()方法,最終調用了其execute()方法
}
void syncRunCall(DownloadCall call) {
call.run();
}
public abstract class NamedRunnable implements Runnable {
@Override
public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} catch (InterruptedException e) {
interrupted(e);
} finally {
Thread.currentThread().setName(oldName);
finished();
}
}
protected abstract void execute() throws InterruptedException;
//...
}
大致流程:
在execute方法里將一個DownloadTask實例又封裝為了一個DownloadCall
對象,然后在syncRunCall方法里執行了DownloadCall
對象的run方法。通過看DownloadCall源碼可以知道該類繼承自NamedRunnable
,而NamedRunnable實現了Runnable,在run方法里調用了execute()
方法。
調用enqueue執行任務最終則是調用 getExecutorService().execute(call)
來異步執行的:
private synchronized void enqueueIgnorePriority(DownloadTask task) {
final DownloadCall call = DownloadCall.create(task, true, store);
if (runningAsyncSize() < maxParallelRunningCount) {
runningAsyncCalls.add(call);
getExecutorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
DownloadCall
先看一下DownloadCall是如何實現execute
方法的,該方法比較長,首先執行的是inspectTaskStart()
方法:
private void inspectTaskStart() {
store.onTaskStart(task.getId());
OkDownload.with().callbackDispatcher().dispatch().taskStart(task);
}
這里的store是調用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循環,首先做一些下載前的准備工作:
- 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
附件列表