上篇文章Retrofit2的再封裝實(shí)戰(zhàn)—多線程下載與斷點(diǎn)續(xù)傳(一)中,介紹了項(xiàng)目的結(jié)構(gòu)圖娜饵,這次我們從程序入口DownLoadManager和實(shí)際下載類(lèi)DownLoadTask開(kāi)始官辈。
我知道你們要的是代碼
DownLoadManager
在開(kāi)始DownLoadManager之前遍坟,我們要先明確一下下載回調(diào)和下載任務(wù)的數(shù)據(jù)結(jié)構(gòu)愿伴。
一电湘、下載任務(wù)數(shù)據(jù)結(jié)構(gòu)
用什么樣的數(shù)據(jù)結(jié)構(gòu)來(lái)表達(dá)我們的下載任務(wù)?這里我選擇使用List集合存儲(chǔ)所有下載任務(wù)怎诫,每個(gè)任務(wù)是一個(gè)DownLoadEntity贷痪,url是下載地址劫拢,saveName是保存地址,目前你只需要關(guān)心這兩個(gè)屬性阀圾。
public int dataId;
public String url;
public long end;
public long start;
public long downed;
public long total;
public String saveName;
public List<DownLoadEntity> multiList;
}```
####二狗唉、下載回調(diào)
在下載過(guò)程中涡真,你會(huì)關(guān)心哪些下載狀態(tài)哆料?
1.開(kāi)始下載: 用戶觸發(fā)下載條件后,在完成一系列任務(wù)(判斷已下載數(shù)據(jù)是否完整杏节,獲取所有任務(wù)總長(zhǎng)度典阵,計(jì)算已下載百分比,創(chuàng)建下載任務(wù))后回調(diào)百分比嫉鲸。簡(jiǎn)單說(shuō)歹啼,在萬(wàn)事俱備那一刻回調(diào)座菠。
2.取消下載:用戶觸發(fā)取消條件后回調(diào)(只回調(diào)一次)浴滴。
3.下載中: 觸發(fā)條件并非每次I/O后也榄,都會(huì)回調(diào),為了節(jié)省資源降宅,這里每下載1MB回調(diào)一次百分比(這個(gè)當(dāng)然你可以自己設(shè)置)囚霸。
4.完成下載:一個(gè)下載請(qǐng)求的所有任務(wù)完成后回調(diào)拓型。
5.下載出錯(cuò):下載過(guò)程出現(xiàn)異常狀態(tài)回調(diào)。
```public interface DownLoadBackListener {
void onStart(double percent);
void onCancel();
void onDownLoading(double percent);
void onCompleted();
void onError(DownLoadEntity downLoadEntity,Throwable throwable);
}```
onError方法要拿出來(lái)單講一下册养,一個(gè)下載請(qǐng)求可能會(huì)有幾十個(gè)url地址压固,如果某個(gè)任務(wù)失敗了,你會(huì)怎么做呢帐我?我想你會(huì)單獨(dú)拿出失敗的url再單獨(dú)請(qǐng)求一次下載拦键,然后限定一個(gè)重復(fù)次數(shù),比如10次萄金,超過(guò)10次后仍然失敗媚朦,你可能會(huì)提示用戶下載失敗。在這次封裝中福稳,你不必再考慮這些因素瑞侮,因?yàn)橐呀?jīng)幫你處理了失敗情況,每個(gè)失敗的url是會(huì)重新下載的越妈,十次嘗試機(jī)會(huì)梅掠,如果都失敗了,才會(huì)進(jìn)行onError回調(diào)酪我。最后失敗的下載實(shí)體和失敗原因已經(jīng)回調(diào)給你了且叁,至于怎么處理,你自己來(lái)決定欺矫。
####三展氓、下載入口
DownLoadManager做為下載的總?cè)肟谟龉Y(jié)合上面說(shuō)的下載結(jié)構(gòu)和回調(diào),我們提供下載方法:
```public void downLoad(final List<DownLoadEntity> list, final String tag, final DownLoadBackListener downLoadTaskListener, final long multiLine) {
mExecutorService.submit(new Runnable() {
@Override
public void run() {
DownLoadRequest downLoadRequest = new DownLoadRequest(mDownLoadDatabase,downLoadTaskLister, list, multiLine);
downLoadRequest.start();
mDownLoadRequestMap.put(tag, downLoadRequest);
}
});
}```
List<DownLoadEntity>:整個(gè)請(qǐng)求的下載數(shù)據(jù)教寂。
tag:因?yàn)槲覀円彺婷總€(gè)請(qǐng)求的下載數(shù)據(jù)执庐,使用tag來(lái)區(qū)別不同次請(qǐng)求轨淌,如果還不了解請(qǐng)瀏覽我的另一篇文章[《[Retrofit2的再封裝實(shí)戰(zhàn)—同步與異步請(qǐng)求》](http://www.reibang.com/p/21fd4e468343)](http://www.reibang.com/p/21fd4e468343)看尼,與文章中的Tag相同含義。
DownLoadBackListener:上面說(shuō)的下載回調(diào)躏结。
multiLine:多線程下載分割線媳拴,單位字節(jié),程序默認(rèn)使用多線程下載塞关,分割線默認(rèn)值是10 ? 1024 ? 1024字節(jié)子巾,也就是10mb。比如一個(gè)url的大小是50mb椰于,那么程序會(huì)自動(dòng)把50mb分成5個(gè)10mb一起下載仪搔。如果你不想使用多線程下載僻造,直接傳0就好了;
當(dāng)然竹挡,如果你想簡(jiǎn)單的使用默認(rèn)值立膛,程序還提供了對(duì)應(yīng)的多態(tài)方法:
//默認(rèn)支持多線程下載
public void downLoad(final List<DownLoadEntity> list, final String tag, final DownLoadBackListener downLoadTaskListener) {
downLoad(list, tag, downLoadTaskListener, MULTI_LINE);
}
上篇文章說(shuō)過(guò)了宝泵,DownLoadManager有實(shí)現(xiàn)緩存的功能,我們使用
`private Map<String, DownLoadRequest> mDownLoadRequestMap = new ConcurrentHashMap<>();`
來(lái)記錄下載任務(wù)框往,key就是tag闯捎,value是DownLoadRequest。同時(shí)提供cancel()方法秉版,實(shí)現(xiàn)取消任務(wù)茬祷。具體實(shí)現(xiàn)和[《[Retrofit2的再封裝實(shí)戰(zhàn)—同步與異步請(qǐng)求》](http://www.reibang.com/p/21fd4e468343)](http://www.reibang.com/p/21fd4e468343)這篇文章思路一樣,這里不多說(shuō)借卧。
細(xì)心的朋友可能已經(jīng)發(fā)現(xiàn)在downLoad()方法中使用了mExecutorService線程池筛峭,在這里解釋一下為什么要另開(kāi)一個(gè)線程,其實(shí)就是為了處理上面所說(shuō)的onStart回調(diào)之前那一系列操作所造成的主線程阻塞情況镰吵,在真正開(kāi)始下面之前疤祭,我們要先拿到當(dāng)前任務(wù)所有url的總長(zhǎng)度(不然我怎么回調(diào)百分比呢饵婆?),大概思路是這樣的草穆,首先會(huì)迭代所有url搓译,每個(gè)url先查詢(xún)本地?cái)?shù)據(jù)庫(kù)些己,查看是否有當(dāng)前url的任務(wù)記錄,如果有涯冠,取出數(shù)據(jù)逼庞。如果沒(méi)有,進(jìn)行異步網(wǎng)絡(luò)請(qǐng)求械荷,獲取下載長(zhǎng)度。我們有個(gè)輪循機(jī)制痹兜,要等待所有url都查詢(xún)到長(zhǎng)度后,再開(kāi)始下載对湃。所以上面這一部分拍柒,一定是同步的!一定是同步的脂男!一定是同步的V帜拧(當(dāng)然所有獲取url的網(wǎng)絡(luò)請(qǐng)求是異步執(zhí)行的)也就是說(shuō)爽室,我要等到所有的url都結(jié)束才能真正開(kāi)始下載任務(wù)。如果你的下載請(qǐng)求嘿架,有近百個(gè)url啸箫,這一部分大概會(huì)耗時(shí)2~3秒筐高,這短短的2~3秒對(duì)ui線程來(lái)說(shuō)就是致命的,有潔癖的同學(xué)當(dāng)然不能容忍啦蜀肘!但是這里會(huì)出現(xiàn)個(gè)問(wèn)題稽屏,downLoadTaskListener的所有回調(diào)現(xiàn)在都是在異步線程中的狐榔,至于怎么在異步線程中回調(diào)更新ui,這里不需要使用者再處理收捣,程序中已經(jīng)處理過(guò)了庵楷,怎么實(shí)現(xiàn)?使用一個(gè)獲取主線程Looper的Handler就可以了童漩,如果你看過(guò)Retrofit源碼這點(diǎn)不會(huì)陌生春锋,具體代碼在DownLoadRequest里再給出。
說(shuō)了這么多豆拨,下面放個(gè)使用的簡(jiǎn)單demo施禾,DownLoadManager入口類(lèi)的所有功能就介紹完畢了.
![demo](http://upload-images.jianshu.io/upload_images/3376157-aa0c19feacbcc400.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
##DownLoadTask
上文說(shuō)過(guò)真正的下載任務(wù)是在DownLoadTask進(jìn)行的搁胆,我們已經(jīng)創(chuàng)建好了DownLoadService渠旁,只需要在Task中調(diào)用DownLoadService中的Api進(jìn)行I/O操作就可以了,這里特別強(qiáng)調(diào)一下粤铭,下載地址URL和Task可以是一對(duì)一也可以是一對(duì)多的關(guān)系杂靶。
這里我們使用Builder模式創(chuàng)建Task實(shí)例:
```public static final class Builder {
private DownLoadEntity mDownModel;
private DownLoadTaskListener mDownLoadTaskListener;
public Builder downLoadModel(DownLoadEntity downLoadEntity) {
mDownModel = downLoadEntity;
return this;
}
public Builder downLoadTaskListener(DownLoadTaskListener downLoadTaskListener) {
mDownLoadTaskListener = downLoadTaskListener;
return this;
}
public DownLoadTask build() {
if (mDownModel.url.isEmpty()) {
throw new IllegalStateException("DownLoad URL required.");
}
if (mDownLoadTaskListener == null) {
throw new IllegalStateException("DownLoadTaskListener required.");
}
if (mDownModel.end == 0) {
throw new IllegalStateException("End required.");
}
return new DownLoadTask(mTaskId, mDownModel, mDownLoadTaskListener);
}
}```
(代碼排版了半天吗垮,不知道中間為啥還是空這么大間隔烁登。。锨络。)
DownLoadTaskListener:這是每個(gè)Task的回調(diào)狼牺,不同于我們上面說(shuō)的DownLoadBackListener,DownLoadBackListener是處理UI的回調(diào)失受,從某種意義上講更像是總回調(diào)咏瑟,而DownLoadTaskListener更多關(guān)注的是細(xì)節(jié)码泞,是每個(gè)下載任務(wù)的回調(diào),所以他更多關(guān)心的下載任務(wù)的本身:
public interface DownLoadTaskListener {
void onStart();
void onCancel(DownLoadEntity downLoadEntity);
void onDownLoading(long downSize);
void onCompleted(DownLoadEntity downLoadEntity);
void onError(DownLoadEntity downLoadEntity, Throwable throwable);
}```
回調(diào)方法和DownLoadBackListener基本一致领铐,只是個(gè)別方法參數(shù)不同宋舷,onCancel onCompleted方法返回了DownLoadEntity實(shí)體祝蝠,這些回調(diào)在DownLoadRequest中進(jìn)行處理,最后再統(tǒng)一回調(diào)給DownLoadTaskListener细溅。
DownLoadEntity:每個(gè)DownLoadEntity都是緩存在DB中的喇聊,結(jié)合上面給出的對(duì)象屬性來(lái)看蹦狂,url和saveName上面已經(jīng)說(shuō)過(guò)了,本處不再解釋燕鸽。
dataID:數(shù)據(jù)庫(kù)主鍵啼辣,每個(gè)實(shí)體的id是唯一啊研。屬性的目的是緩存本地Map的DownLoadTask(這里比較繞,在DownLoadRequest里會(huì)解釋)鸥拧。
start:本次下載的開(kāi)始位置
end:本次下載的技術(shù)位置
downed:已經(jīng)下載字節(jié)數(shù)
total党远、multiList:舉個(gè)例子來(lái)說(shuō) 比較好理解 比如我們一個(gè)url有50mb,多線程下載會(huì)拆成5個(gè)DownLoadEntity富弦,這五個(gè)實(shí)體就保存在multiList中沟娱,total值就是50mb,而不是10mb腕柜。這兩個(gè)屬性和下載是沒(méi)有關(guān)系的济似。具體在DownLoadRequest中解釋。
DownLoadTask 實(shí)現(xiàn)Runnable接口砰蠢,我們主要來(lái)看run方法:
49行設(shè)置線程優(yōu)先級(jí)為最高蓖扑。
50-55行 調(diào)用我們上篇文章定義過(guò)的DownLoadService接口方法生成Retrofit Call,判斷downed是否為0,如果是台舱,則直接從start開(kāi)始律杠,不是不為0,開(kāi)始位置就是downed+start竞惋。
下面的代碼很好理解柜去,拿到Call的Response取出響應(yīng)體。63行執(zhí)行I/O操作拆宛,66-75是對(duì)失敗情況進(jìn)行處理嗓奢,并釋放資源。來(lái)看關(guān)鍵的writeToFile方法:
這里的邏輯很簡(jiǎn)單浑厚,先判斷文件是否存在股耽,然后創(chuàng)建文件,88標(biāo)記開(kāi)始寫(xiě)文件位置瞻颂,93-95設(shè)置文件讀取緩沖區(qū)豺谈,相信對(duì)I/O操作熟悉的人,這里不會(huì)陌生贡这,不熟悉的朋友請(qǐng)大家自行查詢(xún)資料茬末,這里不做解釋。繼續(xù)往下看:
這里我們做了優(yōu)化盖矫,如果我們每寫(xiě)4096的字節(jié)丽惭,就回調(diào)一次,那未免太奢侈了辈双,所以我們?cè)O(shè)定一個(gè)常量
private final long CALL_BACK_LENGTH = 1024 * 1024;
每1mb回調(diào)一次责掏,為了統(tǒng)計(jì)每次回調(diào)前的下載量,我們定義屬性
private long mFileSizeDownloaded;
105行 每次寫(xiě)完mFileSizeDownloaded+read;107-110行湃望,如果當(dāng)前mFileSizeDownloaded大于CALL_BACK_LENGTH换衬,也就是說(shuō)到達(dá)回調(diào)臨界值回調(diào)onDowLoading方法,同時(shí)mFileSizeDownloaded置為0证芭,mNeedDownSize屬性是統(tǒng)計(jì)本次下載剩余字節(jié)瞳浦,112-115行,如果剩余字節(jié)不足回調(diào)臨界點(diǎn)废士,那么等下載完最后一字節(jié)叫潦,再回調(diào)。
122-130行 關(guān)閉資源 132 到結(jié)束是你需要處理的IO異常官硝,這里需要根本個(gè)人的業(yè)務(wù)進(jìn)行異常處理矗蕊,也就是你需要定制的地方短蜕。取消線程時(shí),會(huì)觸發(fā)InterruptedIOException異常(不要問(wèn)我為什么傻咖,線程的基礎(chǔ)知識(shí))朋魔。網(wǎng)絡(luò)斷開(kāi),觸發(fā)SocketTimeoutException異常没龙,這里我們的業(yè)務(wù)邏輯是只要不是用戶取消铺厨,都認(rèn)為是Error缎玫。下面給出不同狀態(tài)回調(diào)代碼;
Tips
還有最后一篇文章就完結(jié)了硬纤,這篇文章陸陸續(xù)續(xù)寫(xiě)了將近兩周了,質(zhì)量我不是太滿意赃磨。先把代碼貼出來(lái)吧筝家。本是想最后再給出來(lái)的,大家看不懂的對(duì)著代碼擼一下吧邻辉。溪王。。
希望喜歡的朋友幫我頂一下值骇,如果使用中有bug歡迎反饋給我莹菱。
微信:hly1501
郵箱:hly910206@gmail.com