前面兩篇文章我們講了項(xiàng)目整體的設(shè)計(jì)結(jié)構(gòu)、入口類DownloadManager、下載類DownloadTask适荣,這篇文章我們講最重要的類DownLoadRequest。
由于離前兩篇文章時間比較長了院领,感覺陌生的同學(xué)可以先回顧一下:
Retrofit2的再封裝實(shí)戰(zhàn)—多線程下載與斷點(diǎn)續(xù)傳(一)
Retrofit2的再封裝實(shí)戰(zhàn)—多線程下載與斷點(diǎn)續(xù)傳(二)
流程圖
回憶之前文章提到的弛矛,我們將需要下載的任務(wù)構(gòu)造成一個List傳入DownLoadManager中,DownLoadManager調(diào)用方法downLoad生成DownLoadRequest對象比然,同時將List參數(shù)代入丈氓,最后調(diào)用downLoadRequest.start()方法。
一强法、Start
我們將下載的部分操作封裝成DownLoadHandle對象万俗,59行我們調(diào)用queryDownLoadData方法,對應(yīng)上面結(jié)構(gòu)圖的查詢下載總長度步驟饮怯,這是一個耗時操作闰歪,不用擔(dān)心,我們在之前的DownLoadManager中已經(jīng)創(chuàng)建線程了蓖墅,這里面的所有操作都是在子線程中進(jìn)行的库倘,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,先在數(shù)據(jù)庫中查詢當(dāng)前任務(wù)的url教翩,如果查詢結(jié)果大于0杆勇,說明我們曾經(jīng)下載過此url,將dataList賦值給multList饱亿,下面介紹一個概念蚜退。如果我們下載過一個文件,但是服務(wù)器將這個文件的內(nèi)容置換掉了路捧,客戶端如何判斷下載文件的時效性关霸?
http請求頭中有個If-Range屬性,下面摘自網(wǎng)絡(luò)上解釋:
If-Range是另一個起條件判斷的請求頭(我們之前講過If-Match/If-None-Match,If-Modified-Since/If-Unmodified-Since).If-Range頭用來避免客戶端在下載了某資源(比如圖片)的一部分后杰扫,下次重新下載又從頭開始下載队寇。使用If-Range之后,客戶端每次可以從上次下載的部分之后繼續(xù)開始下載章姓。
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的工作原理一樣,他們的值正是服務(wù)器返回的Etag和Last-Modified值凡伊。
初次接觸你可能是蒙圈的零渐,沒關(guān)系,這里舉例來說明一下系忙,我下載過一個文件A诵盼,這是http的response頭信息:
Last-Modified,直觀上很清晰他是一個關(guān)于時間戳的屬性银还。他代表著文件最后修改時間风宁,我們需要做的就是保持這個字段到本地,下次請求時候賦值給If-Range頭信息蛹疯,服務(wù)器會告訴你這文件是否更新過戒财。怎么判斷?
如果請求報文中的Last-Modified與服務(wù)器目標(biāo)內(nèi)容的Last-Modified相等捺弦,即沒有發(fā)生變化饮寞,那么應(yīng)答報文的狀態(tài)碼為206。如果服務(wù)器目標(biāo)內(nèi)容發(fā)生了變化列吼,那么應(yīng)答報文的狀態(tài)碼為200幽崩。
好了,理論具備寞钥,只欠代碼了慌申。繼續(xù)看queryDownLoadData方法,如果我們下載過此url凑耻,并且Modified不為空太示,調(diào)用接口來看看他是否更新過
@Streaming @GET Call<ResponseBody> getHttpHeaderWithIfRange(@Url String fileUrl, @Header("If-Range") String lastModify, @Header("Range") String range);
和我們之前的downloadFile方法差不多柠贤,這里不多解釋。繼續(xù)看类缤,如果沒下載過臼勉,直接調(diào)用getHttpHeader方法,不需要If-Range頭餐弱。
executeGetFileWork方法很簡單只有兩行代碼:
private void executeGetFileWork(Call<ResponseBody> call, GetFileCountListener listener) {
GetFileCountTask getFileCountTask = new GetFileCountTask(call, listener);
mGetFileService.submit(getFileCountTask);
}
GetFileCountTask宴霸,看名字就知道了,創(chuàng)建獲取文件長度的任務(wù)膏蚓,然后加入線程池瓢谢。
GetFileCountListener查詢結(jié)果回調(diào):
public interface GetFileCountListener {
void success(boolean isSupportMulti, boolean isNew, String modified, Long fileSize);
void failed()
}
很簡單兩個方法,成功和失敗驮瞧。GetFileCountTask中通過response的返回報文氓扛,判斷是否支持多線程下載,是否更新過论笔,modified值采郎,下載長度,代碼很簡單這里就不貼了狂魔,感興趣的同學(xué)自己擼代碼看吧蒜埋。下面看GetFileCountListener回調(diào):
先看失敗 如果重試次數(shù)小于0,停止所有任務(wù)最楷,如果未到0整份,則重新嘗試獲取長度,重復(fù)次數(shù)默認(rèn)為3次籽孙。
成功后賦值mDownLoadEntity相關(guān)屬性烈评,93-108行,如果未更換文件蚯撩,判斷下載文件還是否存在础倍,存在說明只要下載剩余任務(wù)就可以了烛占,不存在胎挎,當(dāng)新任務(wù)對待。
setCount方法結(jié)合queryDownLoadData最后的while循環(huán)看忆家,有個全局變量記錄任務(wù)的完成數(shù)犹菇,每個url任務(wù)完成或者失敗后count +1,如果未完成任務(wù)芽卿,或者線程池未被關(guān)閉則一直循環(huán)等待揭芍。
這里提醒下:尤其每個task都是一個線程,所以這里的計(jì)數(shù)卸例,必須要考慮線程同步問題称杨!
整個queryDownLoadData就結(jié)束了肌毅,再回到start方法繼續(xù)看,60-86行遍歷所有下載任務(wù)姑原,獲得總下載值悬而,如果總下載值=已經(jīng)下載值,直接回調(diào)UI線程锭汛,已經(jīng)下載結(jié)束了笨奠。87生成下載總回調(diào),我們知道一個url是一個線程唤殴,一個線程對應(yīng)一個自己的回調(diào)般婆,那么每個線程的回調(diào),統(tǒng)一匯聚到下載總回調(diào)朵逝,只有這個回調(diào)負(fù)責(zé)和UI接口通信蔚袍。
一張圖可能更能說明:
從下向上看,UI回調(diào)和總回調(diào)1對1關(guān)系配名,總回調(diào)里有UI回調(diào)引用页响,總回調(diào)和每個Task的回調(diào),1對多關(guān)系段誊,每個Listener中有總回調(diào)引用闰蚕。
現(xiàn)在從上向下看,Listener下載了1MB连舍,告訴總回調(diào):“你可以給UI回調(diào)了”没陡,UI回調(diào)就老老實(shí)實(shí)告訴UI我下載了1MB了。簡單的說索赏,總回調(diào)就是一個代理類盼玄。
二、AddDownLoadTask
我們還差什么潜腻?入口類完成了埃儿,真正的下載類完成了,下載之前的巴拉巴拉已經(jīng)完成了融涣,那就只差下載任務(wù)了對不對童番?下面就真的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);
//當(dāng)前分支是否下載完成
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是內(nèi)存緩存威鹿,之前就提過了剃斧,我們用
//URL下載Taskprivate Map<String, Map<Integer, Future>> mUrlTaskMap = new ConcurrentHashMap<>();
保存緩存信息,String是url忽你,Map<Integer, Future>是當(dāng)前url下的任務(wù)幼东,為啥又用個Map?因?yàn)榭赡苁嵌嗑€程啊根蟹!Integer脓杉,下載任務(wù)的唯一ID,這里是數(shù)據(jù)庫主鍵简逮,F(xiàn)uture不了解的同學(xué)請自行百度丽已,這就是下載任務(wù)。
如果有下載記錄买决,就找未完成的生成DownLoadTask, executeNetWork就是加入線程池沛婴。如果沒有下載記錄,就是新文件督赤,createDownLoadTask創(chuàng)建下載任務(wù)嘁灯。
127-141 如果下載任務(wù)大于多線程下載的分割值,切成多段進(jìn)行下載躲舌。else 單線程下載丑婿。
好了 大概的流程到這里就結(jié)束了,還差什么没卸?Task任務(wù)回調(diào)羹奉,主線程回調(diào),這些代碼沒有貼出來约计,大家自己去發(fā)現(xiàn)吧诀拭。這里用了代理模式,還有很多的多線程數(shù)據(jù)安全方面的代碼煤蚌。下載Error重置下載機(jī)制耕挨,判斷下載是否真正結(jié)束機(jī)制。對緩存的操作尉桩,map套map的增刪改查筒占。
總結(jié)
到這所有的多線程下載和斷點(diǎn)續(xù)傳就結(jié)束了,其實(shí)寫作過程是痛苦的蜘犁,但是到結(jié)束還是很欣慰的翰苫,相信您從開始看到這篇結(jié)束,整個項(xiàng)目的流程您是了解的这橙,怎么定制奏窑,怎么修改bug應(yīng)該也沒有問題了,畢竟思路有了析恋,就差不停的實(shí)踐了良哲,對嗎盛卡?
我希望這篇文章再思路上可以幫助到您助隧,那也是我的初衷啊!
下篇文章我會整理封裝的支持上拉并村,下拉巍实,可以添加Head的RecycleView。
最后哩牍,感謝私信過我棚潦,鼓勵過我,打賞過我的朋友膝昆,謝謝你們的支持丸边。
GitHub地址
我希望大家可以積極fork,一起修改荚孵,如發(fā)現(xiàn)問題妹窖,歡迎反饋。
微信:hly1501