Retrofit2的再封裝實戰—多線程下載與斷點續傳(三)

前面兩篇文章咱們講了項目總體的設計結構、入口類DownloadManager、下載類DownloadTask,這篇文章咱們講最重要的類DownLoadRequest。
因爲離前兩篇文章時間比較長了,感受陌生的同窗能夠先回顧一下:
Retrofit2的再封裝實戰—多線程下載與斷點續傳(一)
Retrofit2的再封裝實戰—多線程下載與斷點續傳(二)javascript

流程圖


回憶以前文章提到的,咱們將須要下載的任務構形成一個List傳入DownLoadManager中,DownLoadManager調用方法downLoad生成DownLoadRequest對象,同時將List參數代入,最後調用downLoadRequest.start()方法。

1、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,下面介紹一個概念。若是咱們下載過一個文件,可是服務器將這個文件的內容置換掉了,客戶端如何判斷下載文件的時效性?
java

request

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值。git

初次接觸你多是蒙圈的,不要緊,這裏舉例來講明一下,我下載過一個文件A,這是http的response頭信息:
github

response

Last-Modified,直觀上很清晰他是一個關於時間戳的屬性。他表明着文件最後修改時間,咱們須要作的就是保持這個字段到本地,下次請求時候賦值給If-Range頭信息,服務器會告訴你這文件是否更新過。怎麼判斷?

若是請求報文中的Last-Modified與服務器目標內容的Last-Modified相等,即沒有發生變化,那麼應答報文的狀態碼爲206。若是服務器目標內容發生了變化,那麼應答報文的狀態碼爲200。數據庫

這裏須要注意的是:If-Range首部行必須與Range首部行配套使用。若是請求報文中沒有Range首部行,那麼If-Range首部行就會被忽略。若是服務器不支持If-Range,那麼Range首部行也會被忽略。
好了,理論具有,只欠代碼了。繼續看queryDownLoadData方法,若是咱們下載過此url,而且Modified不爲空,調用接口來看看他是否更新過api

@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回調:
服務器

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了。簡單的說,總回調就是一個代理類。

2、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 是當前url下的任務,爲啥又用個Map?由於多是多線程啊!Integer,下載任務的惟一ID,這裏是數據庫主鍵,Future不瞭解的同窗請自行百度,這就是下載任務。
若是有下載記錄,就找未完成的生成DownLoadTask, executeNetWork就是加入線程池。若是沒有下載記錄,就是新文件,createDownLoadTask建立下載任務。

createDownLoadTask

127-141 若是下載任務大於多線程下載的分割值,切成多段進行下載。else 單線程下載。
好了 大概的流程到這裏就結束了,還差什麼?Task任務回調,主線程回調,這些代碼沒有貼出來,你們本身去發現吧。這裏用了代理模式,還有不少的多線程數據安全方面的代碼。下載Error重置下載機制,判斷下載是否真正結束機制。對緩存的操做,map套map的增刪改查。

總結

到這全部的多線程下載和斷點續傳就結束了,其實寫做過程是痛苦的,可是到結束仍是很欣慰的,相信您從開始看到這篇結束,整個項目的流程您是瞭解的,怎麼定製,怎麼修改bug應該也沒有問題了,畢竟思路有了,就差不停的實踐了,對嗎?
我但願這篇文章再思路上能夠幫助到您,那也是個人初衷啊!
下篇文章我會整理封裝的支持上拉,下拉,能夠添加Head的RecycleView。
最後,感謝私信過我,鼓勵過我,打賞過個人朋友,謝謝大家的支持。
GitHub地址我但願你們能夠積極fork,一塊兒修改,如發現問題,歡迎反饋。微信:hly1501

相關文章
相關標籤/搜索