DownloadManager是Android用系統(tǒng)服務(wù)的方式提供的用來(lái)優(yōu)化處理長(zhǎng)時(shí)間下載任務(wù)的工具巍实。
本文將基于Android N的源碼進(jìn)行分析。
DownloadManager的使用方式
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
Uri uri = Uri.parse("downloadUrl");
DownloadManager.Request request = new Request(uri);
long reference = downloadManager.enqueue(request);
調(diào)用enqueue方法之后东涡,只要數(shù)據(jù)連接可用并且Download Manager可用冯吓,下載就會(huì)開(kāi)始。
要在下載完成的時(shí)候獲得一個(gè)系統(tǒng)通知(notification),注冊(cè)一個(gè)廣播接受者來(lái)接收ACTION_DOWNLOAD_COMPLETE廣播疮跑,這個(gè)廣播會(huì)包含一個(gè)EXTRA_DOWNLOAD_ID信息在intent中包含了已經(jīng)完成的這個(gè)下載的ID组贺。
其他更詳細(xì)API使用方法請(qǐng)參考Android DownloadManager的使用一文,此處不再詳述顷牌。
DownloadManager的調(diào)用處理
DownloadManager的執(zhí)行入口方法enqueue的源碼如下所示:
ContentValues values = request.toContentValues(mPackageName);
Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values);
long id = Long.parseLong(downloadUri.getLastPathSegment());
return id;
其中卫病,request為請(qǐng)求初始化傳入的DownloadManager.Rquest對(duì)象勿负,傳入請(qǐng)求后
toContentValues()方法會(huì)以傳入包名將待插入的數(shù)據(jù)生成ContentValues,方法中會(huì)有一個(gè)斷言檢查,代碼如下所示:
ContentValues toContentValues(String packageName) {
ContentValues values = new ContentValues();
assert mUri != null;
//.......
}
其實(shí)看到這處斷言檢查有點(diǎn)疑惑峡继,在構(gòu)造Uri對(duì)象的時(shí)候已經(jīng)進(jìn)行了空判斷呻畸,為什么此處還要進(jìn)行一次斷言檢查呢,不是會(huì)有冗余嗎?
在插入ContentValues時(shí)写穴,mResolver.insert()實(shí)際調(diào)用的是系統(tǒng)DownloadProvider中的insert方法,插入返回的downloadUri會(huì)在原有Uri基礎(chǔ)上調(diào)用ContentUris.withAppendedId(Downloads.Impl.CONTENT_URI, rowID)
添加一個(gè)rowId返回一個(gè)形如content://downloads/my_downloads/33
的Uri共苛,經(jīng)過(guò)Uri截取之后,實(shí)際操作的reference其實(shí)是數(shù)據(jù)庫(kù)中的rowId(數(shù)據(jù)庫(kù)行號(hào))仪吧。
DownloadProvider的調(diào)用處理
在之前版本中庄新,DownloadProvider在插入數(shù)據(jù)后,會(huì)直接以context.startService的方式
來(lái)啟動(dòng)DownloadService薯鼠。進(jìn)行異步任務(wù)下載摄咆。而在Android N版本中引入了JobSchedule組件來(lái)進(jìn)行異步下載任務(wù)的處理。
在Android L版本中引入的JobScheduler可以控制耗電人断,具體使用可以參考:Android JobSchedule工作調(diào)度,
其中DownlaodProvider中的insert方法中的關(guān)鍵操作如下所示:
final long token = Binder.clearCallingIdentity();
try {
Helpers.scheduleJob(getContext(), rowID);
} finally {
Binder.restoreCallingIdentity(token);
}
其中Helpers.scheduleJob()方法中使用rowId將那條下載信息查詢出來(lái),然后調(diào)用綁定的DownloadJobService進(jìn)行下載任務(wù)朝蜘。如果線程調(diào)度失敗恶迈,會(huì)返回false。
public static void scheduleJob(Context context, long downloadId) {
final boolean scheduled = scheduleJob(context, DownloadInfo.queryDownloadInfo(context, downloadId));
if (!scheduled) {
// If we didn't schedule a future job, kick off a notification
// update pass immediately
getDownloadNotifier(context).update();
}
}
此時(shí)getDownloadNotifier(context).update()會(huì)將遍歷出所有未刪除的
DownloadJobService調(diào)度執(zhí)行
DownloadService中調(diào)度的線程開(kāi)始下載谱醇,在onStartJob中用rowId查出來(lái)后暇仲,直接開(kāi)線程開(kāi)始下載,具體代碼如下所示:
public boolean onStartJob(JobParameters params) {
final int id = params.getJobId();
// Spin up thread to handle this download
final DownloadInfo info = DownloadInfo.queryDownloadInfo(this, id);
if (info == null) {
Log.w(TAG, "Odd, no details found for download " + id);
return false;
}
final DownloadThread thread;
synchronized (mActiveThreads) {
thread = new DownloadThread(this, params, info);
mActiveThreads.put(id, thread);
}
thread.start();
return true;
}
DownloadJobService中的暫停副渴、取消與完成
DownloadJobService中在線程開(kāi)啟后奈附,會(huì)刷新展示相應(yīng)的通知欄,通過(guò)通知欄UI中的相應(yīng)控制煮剧,可以實(shí)現(xiàn)對(duì)于下載任務(wù)的控制斥滤。
在開(kāi)始下載后,當(dāng)點(diǎn)擊取消后勉盅,會(huì)發(fā)送廣播到DownlaodReceiver,當(dāng)接受到這個(gè)廣播后佑颇,會(huì)調(diào)用DownloadManager.remove(downloadIds),而DownloadManager.remove()方法則會(huì)調(diào)用DownloadProvider.delete去刪除記錄任務(wù)草娜。同時(shí)會(huì)依據(jù)rowId移除該線程調(diào)度挑胸。
任務(wù)完成時(shí),會(huì)發(fā)送一個(gè)廣播宰闰,通知下載完成茬贵,但是這里比較意外的是,下載完成的廣播發(fā)送是放在DownloadInfo中調(diào)用DownloadInfo.sendIntentIfRequested()發(fā)送的移袍, 而不是在DownloadThread中解藻。
暫停,比較奇怪的是葡盗,DownloadManager的異步下載線程提供了斷點(diǎn)下載的功能舆逃,寫入文件也會(huì)檢查任務(wù)的下載狀態(tài)是不是暫停,但是,卻并未提供暫停下載任務(wù)的API方法路狮,同時(shí)它的下載狀態(tài)查詢的方法也是私有類型的虫啥。如果需要暫停任務(wù)就需要自定義自己的下載任務(wù)了。
DownloadThread中的斷點(diǎn)下載的實(shí)現(xiàn)方法
其實(shí)在DownloadThread中奄妨,主要的下載方法就是就是線程中的excuteDownload()方法涂籽。部分關(guān)鍵代碼如下:
private void executeDownload() throws StopRequestException {
final boolean resuming = mInfoDelta.mCurrentBytes != 0;
...
int redirectionCount = 0;
while (redirectionCount++ < Constants.MAX_REDIRECTS) {
......
conn = (HttpURLConnection) mNetwork.openConnection(url);
addRequestHeaders(conn, resuming);
final int responseCode = conn.getResponseCode();
switch (responseCode) {
case HTTP_OK:
if (resuming) {
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected partial, but received OK");
}
parseOkHeaders(conn);
transferData(conn);
return;
case HTTP_PARTIAL:
if (!resuming) {
throw new StopRequestException(
STATUS_CANNOT_RESUME, "Expected OK, but received partial");
}
transferData(conn);
return;
......
}
......
}
}
在addRequestHeaders()方法中,如果從數(shù)據(jù)庫(kù)中查出的數(shù)據(jù)已讀取寫入文件的字節(jié)數(shù)不為0砸抛,則會(huì)在請(qǐng)求頭前添加一個(gè)rangeconn.addRequestProperty("Range", "bytes=" + mInfoDelta.mCurrentBytes + "-");
评雌,當(dāng)添加上此請(qǐng)求頭后,當(dāng)求求成功后直焙,服務(wù)器會(huì)返回HTTP_PARTIAL,將接收到的數(shù)據(jù)通過(guò)transferData()方法寫入到文件中景东。在寫入文件中時(shí),DownloadThread引入了android.drm.DrmManagerClient與android.drm.DrmOutputStream奔誓,這兩個(gè)包位于framework/base/core/drm包下斤吐,部分引用代碼如下所示:
if (DownloadDrmHelper.isDrmConvertNeeded(mInfoDelta.mMimeType)) {
drmClient = new DrmManagerClient(mContext);
out = new DrmOutputStream(drmClient, outPfd, mInfoDelta.mMimeType);
} else {
out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd);
}
對(duì)于這兩個(gè)類的引入,目前還不是特別熟悉厨喂,后續(xù)研究后會(huì)進(jìn)一步進(jìn)行分析
最后喪心病狂的自己畫個(gè)圖和措,簡(jiǎn)單總結(jié)下DownloadManager的工作流程:整體外源應(yīng)用層通過(guò)FrameWork層DownloadManager API調(diào)用到DownloadProvider,通過(guò)操作數(shù)據(jù)庫(kù),最后通過(guò)DownloadService中的線程調(diào)度完成工作蜕煌。整體上都是由DownloadProvider進(jìn)行過(guò)渡調(diào)用派阱。而數(shù)據(jù)庫(kù)與Service都通過(guò)DownloadProvider進(jìn)行隔離。
DownloadManager中的分析目前就先告一段落斜纪,文中如有分析錯(cuò)誤或描述不清楚之處贫母,請(qǐng)大家留言指出~:)