Retrofit2的再封裝實(shí)戰(zhàn)—多線程下載與斷點(diǎn)續(xù)傳(二)

抱歉時(shí)間太長(zhǎng)搔耕,最近實(shí)在是太忙了弃榨。

上篇文章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方法:

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方法:

writeToFile-part1

這里的邏輯很簡(jiǎn)單浑厚,先判斷文件是否存在股耽,然后創(chuàng)建文件,88標(biāo)記開(kāi)始寫(xiě)文件位置瞻颂,93-95設(shè)置文件讀取緩沖區(qū)豺谈,相信對(duì)I/O操作熟悉的人,這里不會(huì)陌生贡这,不熟悉的朋友請(qǐng)大家自行查詢(xún)資料茬末,這里不做解釋。繼續(xù)往下看:

writeToFile-part2

這里我們做了優(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)。
writeToFile-part3

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)代碼;
多狀態(tài)回調(diào)

Tips

還有最后一篇文章就完結(jié)了硬纤,這篇文章陸陸續(xù)續(xù)寫(xiě)了將近兩周了,質(zhì)量我不是太滿意赃磨。先把代碼貼出來(lái)吧筝家。本是想最后再給出來(lái)的,大家看不懂的對(duì)著代碼擼一下吧邻辉。溪王。。
希望喜歡的朋友幫我頂一下值骇,如果使用中有bug歡迎反饋給我莹菱。
微信:hly1501
郵箱:hly910206@gmail.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吱瘩,隨后出現(xiàn)的幾起案子道伟,更是在濱河造成了極大的恐慌,老刑警劉巖使碾,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蜜徽,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡票摇,警方通過(guò)查閱死者的電腦和手機(jī)拘鞋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)矢门,“玉大人盆色,你說(shuō)我怎么就攤上這事∷钐蓿” “怎么了隔躲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)峡扩。 經(jīng)常有香客問(wèn)我蹭越,道長(zhǎng),這世上最難降的妖魔是什么教届? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任响鹃,我火速辦了婚禮驾霜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘买置。我一直安慰自己粪糙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布忿项。 她就那樣靜靜地躺著蓉冈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪轩触。 梳的紋絲不亂的頭發(fā)上寞酿,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音脱柱,去河邊找鬼伐弹。 笑死,一個(gè)胖子當(dāng)著我的面吹牛榨为,可吹牛的內(nèi)容都是我干的惨好。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼随闺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼日川!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起矩乐,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤龄句,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后绰精,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撒璧,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年笨使,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卿樱。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡硫椰,死狀恐怖繁调,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情靶草,我是刑警寧澤蹄胰,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站奕翔,受9級(jí)特大地震影響裕寨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一宾袜、第九天 我趴在偏房一處隱蔽的房頂上張望捻艳。 院中可真熱鬧,春花似錦庆猫、人聲如沸认轨。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嘁字。三九已至,卻和暖如春杉畜,著一層夾襖步出監(jiān)牢的瞬間纪蜒,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工寻行, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留霍掺,地道東北人匾荆。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓拌蜘,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親牙丽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子简卧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容