造輪子之 Android 多線程多任務(wù)斷點(diǎn)續(xù)傳下載器(設(shè)計(jì)篇)

前段時(shí)間面試楞抡,被問(wèn)到 app 的自動(dòng)更新是怎么做的伟众,文件下載怎么實(shí)現(xiàn)的?用了多線程嗎召廷?是否支持?jǐn)帱c(diǎn)續(xù)傳凳厢?一下蒙逼,因?yàn)橹苯佑玫谌娇蚣軐?shí)現(xiàn)的文件下載竞慢,這些問(wèn)題完全沒(méi)想過(guò)先紫。
回來(lái)后覺(jué)得這里面其實(shí)涉及很多知識(shí)點(diǎn),就打算自己動(dòng)手封裝一個(gè)支持多線程多任務(wù)斷點(diǎn)續(xù)傳的庫(kù)梗顺,用了一個(gè)星期的業(yè)余時(shí)間泡孩,目前主要功能基本完成,所以記錄一下這個(gè)過(guò)程中遇到的問(wèn)題和收獲

1. 涉及知識(shí)點(diǎn)

聽(tīng)起來(lái)并不復(fù)雜的一個(gè)功能寺谤,但是實(shí)際動(dòng)手做起來(lái)發(fā)現(xiàn)還是涉及了很多知識(shí)點(diǎn)的仑鸥,概括一下主要涉及以下幾個(gè)部分:

  • HTTP 請(qǐng)求:為了盡量的減少對(duì)第三方框架的依賴,這個(gè)庫(kù)里的 HTTP 請(qǐng)求部分就直接用 HttpURLConnection 來(lái)實(shí)現(xiàn)
  • 斷點(diǎn)續(xù)傳:主要借助 RandomAccessFile 這個(gè)類來(lái)實(shí)現(xiàn)变屁,這個(gè)類可以從文件的任意指定位置開(kāi)始進(jìn)行讀寫(xiě)
  • 多線程下載:涉及到多個(gè)子線程的同步眼俊,中斷異常的處理,線程池的使用等
  • 多任務(wù)下載:這里涉及到任務(wù)的調(diào)度和同步粟关,比如限制同時(shí)下載的任務(wù)數(shù)疮胖,達(dá)到這個(gè)上限以后,再添加任務(wù)要等待闷板,如何處理澎灸。暫停或者取消一個(gè)任務(wù)后如何自動(dòng)啟動(dòng)等待的任務(wù)等遮晚。這里使用了阻塞隊(duì)列和信號(hào)量來(lái)實(shí)現(xiàn)任務(wù)的調(diào)度
  • 事件的發(fā)布:可以用廣播性昭, EventBus,Handler县遣,回調(diào)
  • 數(shù)據(jù)的持久化:退出程序后糜颠,保存下載任務(wù)的狀態(tài),再次打開(kāi)后加載所有的任務(wù)萧求,并能夠繼續(xù)下載其兴。這里可以用數(shù)據(jù)庫(kù),臨時(shí)文件 或者 SharedPreference 實(shí)現(xiàn)夸政。

2. 整體設(shè)計(jì)

1. 下載請(qǐng)求的封裝
用一個(gè) JavaBean 類封裝一個(gè)下載請(qǐng)求元旬,包括了下載的 url 地址,文件保存的目錄以及文件名,基本的參數(shù)其實(shí)就這三個(gè)法绵,還可以根據(jù)需要添加更多的配置參數(shù)箕速,比如指定并發(fā)的線程數(shù)等。如果參數(shù)比較多的情況朋譬,可以使用 Builder 模式

2. 任務(wù)的調(diào)度
回想我們使用迅雷下載時(shí)盐茎,填入下載鏈接,選好保存路徑之后點(diǎn)擊開(kāi)始徙赢,任務(wù)就自動(dòng)開(kāi)始下載了字柠。如果此時(shí)任務(wù)數(shù)已經(jīng)達(dá)到上限,那么就會(huì)等待狡赐,直到有任務(wù)結(jié)束窑业,再自動(dòng)開(kāi)始等待的任務(wù)。仔細(xì)思考之后枕屉,有以下幾個(gè)要點(diǎn):

  • 因?yàn)樘砑尤蝿?wù)是在主線程進(jìn)行常柄,所以應(yīng)該是非阻塞的,任何時(shí)候執(zhí)行添加一個(gè)任務(wù)都應(yīng)該立即返回搀擂。所以這里考慮使用一個(gè)無(wú)上限的阻塞隊(duì)列 LinkedBlockingQueue
  • 任務(wù)一旦添加就自動(dòng)開(kāi)始西潘,這里參考了 Volley 的 NetworkDispatcher 的設(shè)計(jì),開(kāi)啟一個(gè)專門(mén)的任務(wù)調(diào)度線程哨颂,用一個(gè)死循環(huán)不斷的從阻塞隊(duì)列取出任務(wù)來(lái)執(zhí)行喷市,當(dāng)隊(duì)列為空時(shí)就阻塞,非空時(shí)就被喚醒并執(zhí)行任務(wù)威恼。其實(shí)就是一個(gè)典型的生產(chǎn)-消費(fèi)模型
  • 達(dá)到最大任務(wù)數(shù)后要等待有任務(wù)停止(包括成功品姓,失敗,暫停箫措,取消幾種情況)才能開(kāi)始腹备,這里很自然的想到用 Semaphore 來(lái)實(shí)現(xiàn),當(dāng)從阻塞隊(duì)列取出一個(gè)任務(wù)后斤蔓,還需要先成功獲取一個(gè)信號(hào)量馏谨,才能繼續(xù)開(kāi)始執(zhí)行,否則就阻塞附迷。當(dāng)任務(wù)停止的時(shí)候,釋放一個(gè)信號(hào)量哎媚,之前等待的任務(wù)就可以自動(dòng)開(kāi)始執(zhí)行了喇伯。
    當(dāng)然這里也可以不用信號(hào)量,通過(guò)一個(gè)計(jì)數(shù)器加一個(gè)等待隊(duì)列實(shí)現(xiàn)調(diào)度拨与。一個(gè)任務(wù)結(jié)束后稻据,需要檢查當(dāng)前正在下載的任務(wù)數(shù)寇窑,以及是否有任務(wù)在等待隊(duì)列渡冻,如果有并且計(jì)數(shù)器值小于上限值,就從等待隊(duì)列取出一個(gè)任務(wù)執(zhí)行。個(gè)人感覺(jué)使用信號(hào)量在概念上更加清晰栽惶。

3. 下載任務(wù)的執(zhí)行
一個(gè)支持多線程斷點(diǎn)續(xù)傳的任務(wù)開(kāi)始后,其實(shí)是分成了串行的兩步執(zhí)行的:
(1) 發(fā)起一次 HTTP 請(qǐng)求漫蛔,獲取下載文件的長(zhǎng)度信息
(2) 根據(jù)文件的長(zhǎng)度以及設(shè)置的線程數(shù)N抛蚤,把下載任務(wù)分成N個(gè)子任務(wù),每個(gè)子任務(wù)再分別發(fā)起HTTP請(qǐng)求姓言,負(fù)責(zé)下載自己那一部分的數(shù)據(jù)并寫(xiě)入同一個(gè)文件中(RandomAccessFile 已經(jīng)處理了同步問(wèn)題)瞬项。
所以這里我的設(shè)計(jì)是先使用一個(gè) AsyncTask 獲取文件長(zhǎng)度,再異步的回調(diào)里何荚,開(kāi)啟N個(gè)子任務(wù)線程進(jìn)行下載囱淋。
這里當(dāng)然是使用線程池來(lái)執(zhí)行子任務(wù)了,子任務(wù)都實(shí)現(xiàn) Runnable 丟到線程池里餐塘。另外由于 AsyncTask 默認(rèn)的實(shí)現(xiàn)是串行的妥衣,也可以讓 AsyncTask 在默認(rèn)的線程池上執(zhí)行,這樣就可以實(shí)現(xiàn)多個(gè)任務(wù)同時(shí)開(kāi)始下載了戒傻。

4. 下載任務(wù)的封裝和管理
首先要用一個(gè)類來(lái)描述一個(gè)下載任務(wù)税手,這個(gè)類的設(shè)計(jì)要考慮以下幾點(diǎn):

  • 每一個(gè)下載任務(wù)和一個(gè)下載請(qǐng)求一一對(duì)應(yīng),所以下載任務(wù)中應(yīng)該包含一個(gè)下載請(qǐng)求的字段
  • 每個(gè)任務(wù)需要一個(gè)唯一的ID稠鼻,這里考慮使用url+保存路徑+文件名的字符串進(jìn)行MD5運(yùn)算冈止,來(lái)作為一個(gè)任務(wù)的ID
  • 需要記錄下載文件的大小
  • 需要一個(gè)字段標(biāo)示當(dāng)前任務(wù)所處的狀態(tài),比如正在下載候齿,暫停熙暴,失敗等,操作該字段需要同步
  • 需要一個(gè)字段標(biāo)示當(dāng)前任務(wù)已經(jīng)下載的字節(jié)數(shù)慌盯,操作該字段也需要同步
  • 需要一個(gè)List字段保存已經(jīng)開(kāi)始下載的任務(wù)的子任務(wù)的信息周霉,每個(gè)子任務(wù)中保存當(dāng)前寫(xiě)入文件的位置以及結(jié)束寫(xiě)入的位置

然后就是需要一個(gè)集合來(lái)保存所有的已添加的任務(wù),因?yàn)楦鞣N對(duì)任務(wù)的操作亚皂,比如暫停俱箱,取消,刪除等都是要根據(jù)ID來(lái)找到一個(gè)對(duì)應(yīng)任務(wù)灭必,所以使用Map來(lái)保存可以保證查找的效率狞谱。

5. 事件發(fā)布設(shè)計(jì)
所有的事件都通過(guò) LocalBroadcastManager 發(fā)布,然后使用者可以有兩種方法實(shí)現(xiàn)對(duì)事件的監(jiān)聽(tīng)禁漓,一種是定義自己的 Receiver 接收處理各種廣播事件跟衅。還有一種是注冊(cè) Listener,然后我們?cè)诳蚣軆?nèi)部實(shí)現(xiàn)一個(gè) BroadcastReceiver播歼,根據(jù)不同的事件調(diào)用 Listener 的不同的方法伶跷,這樣封裝的更好,不過(guò)某些場(chǎng)景自己注冊(cè)Receiver還是更靈活一些,可以在 switch 里面對(duì)多個(gè) case 合并處理

6. 任務(wù)狀態(tài)的切換
最主要的部分就是如何暫桶饶或者取消一個(gè)正在進(jìn)行的任務(wù)蹈集。在下載的子任務(wù)線程里,會(huì)有一個(gè)循環(huán)從InputStream讀取數(shù)據(jù)并寫(xiě)入文件的操作雇初,我們就在這個(gè)循環(huán)這里加入對(duì)任務(wù)狀態(tài)的判斷拢肆,當(dāng)狀態(tài)是Downloading時(shí),就繼續(xù)下載抵皱,當(dāng)狀態(tài)被設(shè)為 Paused 時(shí)善榛,就跳出循環(huán),這樣就實(shí)現(xiàn)了任務(wù)的暫停呻畸。
當(dāng)然也可以用 FutureTask.cancel()移盆,在循環(huán)里判斷 isInterrupted() 來(lái)實(shí)現(xiàn),不過(guò)因?yàn)槲覀円呀?jīng)有了一個(gè)表示任務(wù)狀態(tài)的字段伤为,直接使用這個(gè)字段可以達(dá)到同樣的效果咒循。
當(dāng)恢復(fù)一個(gè)暫停的任務(wù)時(shí),不能讓它直接開(kāi)始绞愚,要把重新加到任務(wù)隊(duì)列里面去叙甸,然后等待調(diào)度。因?yàn)榭赡芤呀?jīng)達(dá)到任務(wù)上限位衩,所以還是要重新拿到信號(hào)量才可以開(kāi)始裆蒸。

7. 任務(wù)的持久化
不考慮大量任務(wù)管理的場(chǎng)景的話,可以直接用 SharedPreference 配合 Gson 的序列化和反序列化糖驴,實(shí)現(xiàn)任務(wù)的持久化僚祷。用數(shù)據(jù)庫(kù)的話就麻煩一點(diǎn),要自己讀寫(xiě)各個(gè)字段贮缕,當(dāng)然也可以用 GreenDao辙谜,Realm 等orm框架,不過(guò)作為一個(gè)實(shí)驗(yàn)性項(xiàng)目感昼,這塊暫時(shí)先不做那么復(fù)雜吧装哆。

3. 總結(jié)

初步的分析結(jié)束,整體的思路已經(jīng)清楚了定嗓,主要的難點(diǎn)應(yīng)該是在任務(wù)的調(diào)度蜕琴,多線程的協(xié)作和同步。最后從用戶的角度總結(jié)一下最終要實(shí)現(xiàn)的功能:

  • 定義一個(gè)下載請(qǐng)求并加入下載隊(duì)列宵溅,獲得任務(wù)的ID以便后續(xù)的操作奸绷。任務(wù)自動(dòng)開(kāi)始下載,如果達(dá)到上限就等待
  • 通過(guò)任務(wù)ID可以暫停层玲,取消,恢復(fù)一個(gè)任務(wù)的執(zhí)行
  • 任何情況下退出都應(yīng)該能保存任務(wù)的下載狀態(tài)

下一篇就寫(xiě)具體的代碼實(shí)現(xiàn)。

最后編輯于
?著作權(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)容