前面兩篇文章我們講了專案整體的設計結構、入口類DownloadManager、下載類DownloadTask,這篇文章我們講最重要的類DownLoadRequest。
由於離前兩篇文章時間比較長了,感覺陌生的同學可以先回顧一下:
Retrofit2的再封裝實戰—多執行緒下載與斷點續傳(一)
Retrofit2的再封裝實戰—多執行緒下載與斷點續傳(二)
流程圖
回憶之前文章提到的,我們將需要下載的任務構造成一個List傳入DownLoadManager中,DownLoadManager呼叫方法downLoad生成DownLoadRequest物件,同時將List引數代入,最後呼叫downLoadRequest.start()方法。
一、Start
我們將下載的部分操作封裝成DownLoadHandle物件,59行我們呼叫queryDownLoadData方法,對應上面結構圖的查詢下載總長度步驟,這是一個耗時操作,不用擔心,我們在之前的DownLoadManager中已經建立執行緒了,這裡面的所有操作都是在子執行緒中進行的,UI執行緒是不會被阻塞的。
queryDownLoadData:
//彙總所有下載資訊
List<DownLoadEntity> queryDownLoadData(List<DownLoadEntity> list) {
final Iterator iterator = list.iterator();
while (iterator.hasNext()) {
DownLoadEntity downLoadEntity = (DownLoadEntity) iterator.next();
downLoadEntity.downed = 0;
Call<ResponseBody> mResponseCall = null;
List<DownLoadEntity> dataList = mDownLoadDatabase.query(downLoadEntity.url);
if (dataList.size() > 0) {
downLoadEntity.multiList = dataList;
if (!TextUtils.isEmpty(dataList.get(0).lastModify)) {
mResponseCall = NetWorkRequest.getInstance().getDownLoadService().getHttpHeaderWithIfRange(downLoadEntity.url, dataList.get(0).lastModify, "bytes=" + 0 + "-" + 0);
}
} else {
mResponseCall = NetWorkRequest.getInstance().getDownLoadService().getHttpHeader(downLoadEntity.url, "bytes=" + 0 + "-" + 0);
}
executeGetFileWork(mResponseCall, new GetFileCount(downLoadEntity, mResponseCall));
}
while (!mGetFileService.isShutdown() && getCount() != list.size()) {
}
return list;
}複製程式碼
迭代List,先在資料庫中查詢當前任務的url,如果查詢結果大於0,說明我們曾經下載過此url,將dataList賦值給multList,下面介紹一個概念。如果我們下載過一個檔案,但是伺服器將這個檔案的內容置換掉了,客戶端如何判斷下載檔案的時效性?
http請求頭中有個If-Range屬性,下面摘自網路上解釋:
If-Range是另一個起條件判斷的請求頭(我們之前講過If-Match/If-None-Match,If-Modified-Since/If-Unmodified-Since).If-Range頭用來避免客戶端在下載了某資源(比如圖片)的一部分後,下次重新下載又從頭開始下載。使用If-Range之後,客戶端每次可以從上次下載的部分之後繼續開始下載。
If-Range的使用格式為:If-Range: Etag|Http-Date也就是說If-Range後面可以使用Etag或者Last-Modified返回的值:
If-Range: "df6b0-b4a-3be1b5e1"
If-Range: Tue, 8 Jul 2008 05:05:56 GMT
邏輯上來講,上面2種方式分別和If-Match,If-Unmodified-Since的工作原理一樣,他們的值正是伺服器返回的Etag和Last-Modified值。
初次接觸你可能是蒙圈的,沒關係,這裡舉例來說明一下,我下載過一個檔案A,這是http的response頭資訊:
Last-Modified,直觀上很清晰他是一個關於時間戳的屬性。他代表著檔案最後修改時間,我們需要做的就是保持這個欄位到本地,下次請求時候賦值給If-Range頭資訊,伺服器會告訴你這檔案是否更新過。怎麼判斷?
如果請求報文中的Last-Modified與伺服器目標內容的Last-Modified相等,即沒有發生變化,那麼應答報文的狀態碼為206。如果伺服器目標內容發生了變化,那麼應答報文的狀態碼為200。
這裡需要注意的是:If-Range首部行必須與Range首部行配套使用。如果請求報文中沒有Range首部行,那麼If-Range首部行就會被忽略。如果伺服器不支援If-Range,那麼Range首部行也會被忽略。
好了,理論具備,只欠程式碼了。繼續看queryDownLoadData方法,如果我們下載過此url,並且Modified不為空,呼叫介面來看看他是否更新過
@GET
Call getHttpHeaderWithIfRange(@Url String fileUrl, @Header("If-Range") String lastModify, @Header("Range") String range); 複製程式碼
和我們之前的downloadFile方法差不多,這裡不多解釋。繼續看,如果沒下載過,直接呼叫getHttpHeader方法,不需要If-Range頭。
executeGetFileWork方法很簡單隻有兩行程式碼:
private void executeGetFileWork(Call<ResponseBody> call, GetFileCountListener listener) {
GetFileCountTask getFileCountTask = new GetFileCountTask(call, listener);
mGetFileService.submit(getFileCountTask);
}複製程式碼
GetFileCountTask,看名字就知道了,建立獲取檔案長度的任務,然後加入執行緒池。
GetFileCountListener查詢結果回撥:
public interface GetFileCountListener {
void success(boolean isSupportMulti, boolean isNew, String modified, Long fileSize);
void failed()
}複製程式碼
很簡單兩個方法,成功和失敗。GetFileCountTask中通過response的返回報文,判斷是否支援多執行緒下載,是否更新過,modified值,下載長度,程式碼很簡單這裡就不貼了,感興趣的同學自己擼程式碼看吧。下面看GetFileCountListener回撥:
先看失敗 如果重試次數小於0,停止所有任務,如果未到0,則重新嘗試獲取長度,重複次數預設為3次。
成功後賦值mDownLoadEntity相關屬性,93-108行,如果未更換檔案,判斷下載檔案還是否存在,存在說明只要下載剩餘任務就可以了,不存在,當新任務對待。
setCount方法結合queryDownLoadData最後的while迴圈看,有個全域性變數記錄任務的完成數,每個url任務完成或者失敗後count +1,如果未完成任務,或者執行緒池未被關閉則一直迴圈等待。
這裡提醒下:尤其每個task都是一個執行緒,所以這裡的計數,必須要考慮執行緒同步問題!這裡我們選擇使用synchronized。
整個queryDownLoadData就結束了,再回到start方法繼續看,60-86行遍歷所有下載任務,如果其中有total未獲取到的任務(對應前面獲取長度失敗),那麼直接返回錯誤,終止下載任務。如果都正常,疊加獲得總下載值,如果總下載值=已經下載值,直接回撥UI執行緒,已經下載結束了。88行,onStart()這時就已經回撥給主執行緒下載百分比了,細心的朋友可能發現了,這是使用mMainThread回撥UI執行緒,mMainThread是什麼?看過Retrofit原始碼的朋友肯定不陌生,他的實現原理其實就是運用了擁有MainLooper的hander,因為我們的操作都是在非同步執行緒中進行的,所以需要mMainThread是什麼回撥主執行緒(這個在之前已經講過了),87行生成下載總回撥,一個url是一個下載執行緒,一個下載執行緒對應一個自己的回撥,那麼每個執行緒的回撥,統一匯聚到下載總回撥,只有這個回撥負責和UI介面通訊。
一張圖可能更能說明:
從下向上看,UI回撥和總回撥1對1關係,總回撥裡有UI回撥引用,總回撥和每個Task的回撥,1對多關係,每個Listener中有總回撥引用。
現在從上向下看,Listener下載了1MB,告訴總回撥:“你可以給UI回撥了”,UI回撥就老老實實告訴UI我下載了1MB了。簡單的說,總回撥就是一個代理類。
二、AddDownLoadTask
我們還差什麼?入口類完成了,真正的下載類完成了,下載之前的巴拉巴拉已經完成了,那就只差下載任務了對不對?下面就真的easy了。
private void addDownLoadTask(DownLoadEntity downLoadEntity) {
Map<Integer, Future> downLoadTaskMap = new ConcurrentHashMap<>();
MultiDownLoaderListener multiDownLoaderListener = new MultiDownLoaderListener(mDownCallBackListener);
if (downLoadEntity.multiList != null && downLoadEntity.multiList.size() != 0) {
for (int i = 0; i < downLoadEntity.multiList.size(); i++) {
DownLoadEntity entity = downLoadEntity.multiList.get(i);
//當前分支是否下載完成
if (entity.downed + entity.start > entity.end) { continue;
}
DownLoadTask downLoadTask = new DownLoadTask.Builder().downLoadModel(entity).downLoadTaskListener(multiDownLoaderListener).build();
executeNetWork(entity, downLoadTask, downLoadTaskMap);
}
} else {
//檔案不存在 直接下載
createDownLoadTask(downLoadEntity, NEW_DOWN_BEGIN, downLoadTaskMap, multiDownLoaderListener);
}
}複製程式碼
map是記憶體快取,之前就提過了,我們用Taskprivate Map<String, Map<Integer, Future>> mUrlTaskMap = new ConcurrentHashMap<>();
儲存快取資訊,String是url,Map
如果有下載記錄,就找未完成的生成DownLoadTask, executeNetWork就是加入執行緒池。如果沒有下載記錄,就是新檔案,createDownLoadTask建立下載任務。
127-141 如果下載任務大於多執行緒下載的分割值,切成多段進行下載。else 單執行緒下載。
好了 大概的流程到這裡就結束了,還差什麼?Task任務回撥,主執行緒回撥,這些程式碼沒有貼出來,大家自己去發現吧。這裡用了代理模式,還有很多的多執行緒資料安全方面的程式碼。下載Error重置下載機制,判斷下載是否真正結束機制。對快取的操作,map套map的增刪改查。
總結
到這所有的多執行緒下載和斷點續傳就結束了,其實寫作過程是痛苦的,但是到結束還是很欣慰的,相信您從開始看到這篇結束,整個專案的流程您是瞭解的,怎麼定製,怎麼修改bug應該也沒有問題了,畢竟思路有了,就差不停的實踐了,對嗎?
我希望這篇文章再思路上可以幫助到您,那也是我的初衷啊!
下篇文章我會整理封裝的支援上拉,下拉,可以新增Head的RecycleView。
最後,感謝私信過我,鼓勵過我,打賞過我的朋友,謝謝你們的支援。
GitHub地址
我希望大家可以積極fork,一起修改,如發現問題,歡迎反饋。
微信:hly1501