版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)
文檔簡介
【移動應(yīng)用開發(fā)技術(shù)】Android多線程斷點續(xù)傳下載原理及實現(xiàn)
這段時間看了看工作室的工具庫的下載組件,發(fā)現(xiàn)其存在一些問題:
1.下載核心邏輯有bug,在暫停下載或下載失敗等情況時有概率無法順利完成下載。2.雖然原來的設(shè)計是采用多線程斷點續(xù)傳的設(shè)計,但打了一下日志發(fā)現(xiàn)其實下載任務(wù)都是在同一個線程下串行執(zhí)行,并沒有起到加快下載速度的作用。
1.下載核心邏輯有bug,在暫停下載或下載失敗等情況時有概率無法順利完成下載。2.雖然原來的設(shè)計是采用多線程斷點續(xù)傳的設(shè)計,但打了一下日志發(fā)現(xiàn)其實下載任務(wù)都是在同一個線程下串行執(zhí)行,并沒有起到加快下載速度的作用??紤]到原來的代碼并不復(fù)雜,因此對這部分下載組件進(jìn)行了重寫。這里記錄一下里面的多線程斷點續(xù)傳功能的實現(xiàn)。請查看完整的PDF版(更多完整項目下載。未完待續(xù)。源碼。圖文知識后續(xù)上傳github。)可以點擊關(guān)于我聯(lián)系我獲取完整PDF(VX:m多線程下載意義首先我們談一談,多線程下載的意義。在日常的場景下,網(wǎng)絡(luò)中不可能只有下載方與服務(wù)器之間這樣一條連接,為了避免在這樣的場景下的網(wǎng)絡(luò)擁塞,TCP協(xié)議通過調(diào)節(jié)窗口的大小來避免出現(xiàn)擁塞,但這個窗口的大小可能沒辦法達(dá)到我們預(yù)期的效果:充分利用我們的帶寬。因此我們可以采用多個TCP連接的形式來提高我們帶寬的利用率,從而加快下載速度。打個比喻就是我們要從一個水缸中用抽水機(jī)通過水管抽水,由于管子的直徑等等的限制,我們單條管子無法完全利用我們的抽水機(jī)的抽水動力。因此我們就將這些抽水的任務(wù)分成了多份,分?jǐn)偟蕉鄠€管子上,這樣就可以更充分的利用我們的抽水機(jī)動力,從而提高抽水的速度。因此,我們使用多線程下載的主要意義就是——提高下載速度。多線程下載原理前面提到了我們主要的目的是將一個總的下載任務(wù)分?jǐn)偟蕉鄠€子任務(wù)中,比如假設(shè)我們用5個線程下載這個文件,那么我們就可以對一個長度為N的任務(wù)進(jìn)行如下圖的均分:但真實場景下往往N都不是剛好為5的倍數(shù)的,因此對于最后一個任務(wù)還需要加上剩余的任務(wù)量,也就是N/5+N%5。HttpRange請求頭上面的任務(wù)分配我們已經(jīng)了解了,看起來很理想,但有一個問題,我們?nèi)绾螌崿F(xiàn)向服務(wù)器只請求這個文件的某一段而不是全部呢?我們可以通過在請求頭中加入Range字段來指定請求的范圍,從而實現(xiàn)指定某一段的數(shù)據(jù)。如:RANGEbytes=10000-19999就指定了10000-19999這段字節(jié)的數(shù)據(jù)所以我們的核心思想就是通過它拿到文件對應(yīng)字節(jié)段的InputStream,然后對它讀取并寫入文件。RandomAccessFile文件寫入下面再講講文件寫入問題,由于我們是多線程下載,因此文件并不是每次都是從前往后一個個字節(jié)寫入的,隨時可能在文件的任何一個地方寫入數(shù)據(jù)。因此我們需要能夠在文件的指定位置寫入數(shù)據(jù)。這里我們用到了RandomAccessFile來實現(xiàn)這個功能。RandomAccessFile是一個隨機(jī)訪問文件類,同時整合了FileOutputStream和FileInputStream,支持從文件的任何字節(jié)處讀寫數(shù)據(jù)。通過它我們就可以在文件的任何字節(jié)處寫入數(shù)據(jù)。接下來簡單講講我們這里是如何使用RandomAccessFile的。我們對于每個子任務(wù)來說都有一個開始和結(jié)束的位置。每個任務(wù)都可以通過RandomAccessFile::seek跳轉(zhuǎn)到文件的對應(yīng)字節(jié)位置,然后從該位置開始讀取InputStream并寫入。這樣,就實現(xiàn)了不同線程對文件的隨機(jī)寫入。文件大小的獲取由于我們在真正開始下載之前,我們需要先將任務(wù)分配到各個線程,因此我們需要先了解到文件的大小。為了獲取到文件的大小,我們用到ResponseHeaders中的Content-Length字段。如下圖所示,可以看到,打開該下載請求的鏈接后,ResponseHeaders中包含了我們需要的Content-Length,也就是該文件的大小,單位是字節(jié)。斷點續(xù)傳原理對于多個子任務(wù),我們?nèi)绾螌崿F(xiàn)它們的斷點續(xù)傳呢?其實原理很簡單,只需要保證每個子任務(wù)的下載進(jìn)度能夠被即時地記錄即可。這樣繼續(xù)下載時只需要讀取這些下載記錄,從上次下載結(jié)束的位置開始下載即可。它的實現(xiàn)有很多方式,只要能做到數(shù)據(jù)持久化即可。這里我使用的是數(shù)據(jù)庫來實現(xiàn)。這樣,我們的子任務(wù)需要擁有一些必要的信息通過這些信息,我們就能夠記錄子任務(wù)的下載進(jìn)度從而恢復(fù)我們之前的下載,實現(xiàn)斷點續(xù)傳。代碼實現(xiàn)下面我們用代碼來實現(xiàn)這樣一個多線程下載功能。下載狀態(tài)首先,我們定義一下下載中的各個狀態(tài):publicclassDownloadStatus{
publicstaticfinalintIDLE=233;//空閑,默認(rèn)狀態(tài)
publicstaticfinalintCOMPLETED=234;//完成
publicstaticfinalintDOWNLOADING=235;//下載中
publicstaticfinalintPAUSE=236;//暫停
publicstaticfinalintERROR=237;//出錯
}可以看到,這里定義了如上的五種狀態(tài)。這里需要用到如數(shù)據(jù)庫及HTTP請求的功能,我們這里定義其接口如下,具體實現(xiàn)各位可以根據(jù)需要自己實現(xiàn):publicinterfaceDownloadDbHelper{
/**
*從數(shù)據(jù)庫中刪除子任務(wù)記錄
*@paramtask子任務(wù)記錄
*/
voiddelete(SubDownloadTasktask);
/**
*向數(shù)據(jù)庫中插入子任務(wù)記錄
*@paramtask子任務(wù)記錄
*/
voidinsert(SubDownloadTasktask);
/**
*在數(shù)據(jù)庫中更新子任務(wù)記錄
*@paramtask子任務(wù)記錄
*/
voidupdate(SubDownloadTasktask);
/**
*獲取所有指定Task下的子任務(wù)記錄
*@paramtaskTagTask的Tag
*@return子任務(wù)記錄
*/
List<SubDownloadTask>queryByTaskTag(StringtaskTag);
}publicinterfaceDownloadHttpHelper{
/**
*獲取文件總長度
*@paramurl下載url
*@paramcallback獲取文件長度CallBack
*/
voidgetTotalSize(Stringurl,NetCallback<Long>callback);
/**
*獲取InputStream
*@paramurl下載url
*@paramstart開始位置
*@paramend結(jié)束位置
*@paramcallback獲取字節(jié)流的CallBack
*/
voidgetStreamByRange(Stringurl,longstart,longend,NetCallback<InputStream>callback);
}我們先從上到下,從子任務(wù)開始實現(xiàn)。在我的設(shè)計中,它具有如下的成員變量:@Entity
publicclassSubDownloadTaskimplementsRunnable{
publicstaticfinalintBUFFER_SIZE=1024*1024;
privatestaticfinalStringTAG=SubDownloadTask.class.getSimpleName();
@Id
privateLongid;
privateStringurl;//文件下載的url
privateStringtaskTag;//父任務(wù)的Tag
privatelongtaskSize;//子任務(wù)大小
privatelongcompletedSize;//子任務(wù)完成大小
privatelongstartPos;//開始位置
privatelongcurrentPos;//當(dāng)前位置
privatelongendPos;//結(jié)束位置
privatevolatileintstatus;//當(dāng)前下載狀態(tài)
@Transient
privateSubDownloadListenerlistener;//子任務(wù)下載監(jiān)聽,主要用于提示父任務(wù)
@Transient
privateFilesaveFile;//要保存到的文件
...
}由于這里的數(shù)據(jù)庫的操作是用GreenDao實現(xiàn),因此這里有一些相關(guān)注解,各位可以忽略。可以看到,子任務(wù)是一個Runnable,我們可以通過其run方法開始下載,這樣就可以通過如ExecutorService來開啟多個線程執(zhí)行子任務(wù)。我們看到其run方法:@Override
publicvoidrun(){
status=DownloadStatus.DOWNLOADING;
DownloadManager.getInstance()
.getHttpHelper()
.getStreamByRange(url,currentPos,endPos,newNetCallback<InputStream>(){
@Override
publicvoidonResult(InputStreaminputStream){
listener.onSubStart();
writeFile(inputStream);
}
@Override
publicvoidonError(Stringmessage){
listener.onSubError("文件流獲取失敗");
status=DownloadStatus.ERROR;
}
});
}可以看到,我們獲取了其從currentPos到endPos端的字節(jié)流,通過其ResponseBody拿到了它的InputStream,然后調(diào)用了writeFile(InputStream)方法進(jìn)行文件的寫入。文件寫入接下來看到writeFile方法:privatevoidwriteFile(InputStreamin){
try{
RandomAccessFilefile=newRandomAccessFile(saveFile,"rwd");//通過saveFile建立RandomAccessFile
file.seek(currentPos);//跳轉(zhuǎn)到對應(yīng)位置
byte[]buffer=newbyte[BUFFER_SIZE];
while(true){
//循環(huán)讀取InputStream,直到暫?;蜃x取結(jié)束
if(status!=DownloadStatus.DOWNLOADING){
//狀態(tài)不為DOWNLOADING,停止下載
break;
}
intoffset=in.read(buffer,0,BUFFER_SIZE);
if(offset==-1){
//讀取不到數(shù)據(jù),說明讀取結(jié)束
break;
}
//將讀取到的數(shù)據(jù)寫入文件
file.write(buffer,0,offset);
//下載數(shù)據(jù)并在數(shù)據(jù)庫中更新
currentPos+=offset;
completedSize+=offset;
DownloadManager.getInstance()
.getDbHelper()
.update(this);
//通知父任務(wù)下載進(jìn)度
listener.onSubDownloading(offset);
}
if(status==DownloadStatus.DOWNLOADING){
//下載完成
status=DownloadStatus.COMPLETED;
//通知父任務(wù)下載完成
listener.onSubComplete(completedSize);
}
file.close();
in.close();
}catch(IOExceptione){
e.printStackTrace();
listener.onSubError("文件下載失敗");
status=DownloadStatus.ERROR;
resetTask();
}
}具體流程可以看代碼中的注釋。可以看到,子任務(wù)實際上就是循環(huán)讀取InputStream,并寫入文件,同時將下載進(jìn)度同步到數(shù)據(jù)庫。父任務(wù)也就是我們具體的下載任務(wù),我們同樣先看到成員變量:publicclassDownloadTaskimplementsSubDownloadListener{
privatestaticfinalStringTAG=DownloadTask.class.getSimpleName();
privateStringtag;//下載任務(wù)的Tag,用于區(qū)分不同下載任務(wù)
privateStringurl;//下載url
privateStringsavePath;//保存路徑
privateStringfileName;//保存文件名
privateDownloadListenerlistener;//下載監(jiān)聽
privatelongcompleteSize;//下載完成大小
privatelongtotalSize;//下載任務(wù)總大小
privateintstatus;//當(dāng)前下載進(jìn)度
privateintthreadNum;//線程數(shù)(由外部設(shè)置的每個任務(wù)的下載線程數(shù))
privateFilefile;//保存文件
privateList<SubDownloadTask>subTasks;//子任務(wù)列表
privateExecutorServicemExecutorService;//線程池,用于執(zhí)行子任務(wù)
...
}對于一個下載任務(wù),可以通過download方法開始執(zhí)行:publicvoiddownload(){
listener.onStart();
subTasks=querySubTasks();
status=DownloadStatus.DOWNLOADING;
if(subTasks.isEmpty()){
//是新任務(wù)
downloadNewTask();
}elseif(subTasks.size()==threadNum){
//不是新任務(wù)
downloadExistTask();
}else{
//不是新任務(wù),但下載線程數(shù)有誤
listener.onError("斷點數(shù)據(jù)有誤");
resetTask();
}
}可以看到,我們先將子任務(wù)列表從數(shù)據(jù)庫中讀取出來。如果子任務(wù)列表大小不等于線程數(shù),說明當(dāng)前的下載記錄已不可用,于是重置下載任務(wù),從新下載。我們先看到downloadNewTask方法:可以看到,獲取到總長度后,通過調(diào)用initSubTasks方法,對子任務(wù)列表進(jìn)行了初始化(計算子任務(wù)長度等),然后調(diào)用了startAsyncDownload方法后通過ExecutorService運行子任務(wù)進(jìn)入子任務(wù)進(jìn)行下載。我們看到initSubTasks方法:privatevoidinitSubTasks(){
longaverageSize=totalSize/threadNum;
for(inttaskIndex=0;taskIndex<threadNum;taskIndex++){
longtaskSize=averageSize;
if(taskIndex==threadNum-1){
//最后一個任務(wù),則size還需要加入剩余量
taskSize+=totalSize%threadNum;
}
longstart=0L;
intindex=taskIndex;
while(index>0){
start+=subTasks.get(index-1).getTaskSize();
index--;
}
longend=start+taskSize-1;//注意這里
SubDownloadTasksubTask=newSubDownloadTask();
subTask.setUrl(url);
subTask.setStatus(DownloadStatus.IDLE);
subTask.setTaskTag(tag);
subTask.setCompletedSize(0);
subTask.setTaskSize(taskSize);
subTask.setStartPos(start);
subTask.setCurrentPos(start);
subTask.setEndPos(end);
subTask.setSaveFile(file);
subTask.setListener(this);
DownloadManager.getInstance()
.getDbHelper()
.insert(subTask);
subTasks.add(subTask);
}
}可以看到就是計算每個任務(wù)的大小及開始及結(jié)束點的位置,這里要注意的是endPos需要-1,否則各個任務(wù)的下載位置會重疊,并且最后一個任務(wù)會多下載一個字節(jié)導(dǎo)致如文件損壞等影響。具體原因就是比如一個大小為500的文件,則應(yīng)當(dāng)是0-499而不是0-500。接下來我們看看downloadExistTask方法:privatevoiddownloadExistTask(){
//不是新任務(wù),且下載線程數(shù)無誤,計算已下載大小
completeSize=countCompleteSize();
totalSize=countTotalSize();
startAsyncDownload();
}這里其實很簡單,遍歷子任務(wù)列表計算已下載量及總?cè)蝿?wù)量,并調(diào)用startAsyncDownload開始多線程下載。具體執(zhí)行子任務(wù)我們可以看到startAsyncDownload方法:privatevoidstartAsyncDownload(){
for(SubDownloadTasksubTask:subTasks){
if(subTask.getCompletedSize()<subTask.getTaskSize()){
//只下載沒有下載結(jié)束的子任務(wù)
mExecutorService.execute(subTask);
}
}
}可以看到,這里其實只是通過ExecutorService執(zhí)行對應(yīng)子任務(wù)(Runnable)而已。####暫停功能我們接下來看到pause方法:publicvoidpause(){
stopAsyncDownload();
status=DownloadStatus.PAUSE;
listener.onPause();
}可以看到,這里只是調(diào)用了stopAsyncDownload方法停止子任務(wù)??吹絪topAsyncDownload方法:privatevoidstopAsyncDownload(){
for(SubDownloadTasksubTask:subTasks){
if(subTask.getStatus()!=DownloadStatus.COMPLETED){
//下載完成的不再取消
subTask.cancel();
}
}
}可以看到,調(diào)用了子任務(wù)的cancel方法。繼續(xù)看到子任務(wù)的cancel方法:voidcancel(){
status=DownloadStatus.PAUSE;
listener.onSubCancel();
}這里很簡單,僅僅是將下載狀態(tài)設(shè)置為了PAUSE,這樣在寫入文件的下一次while循環(huán)時便會中止循環(huán)從而結(jié)束Runnable的執(zhí)行。看到cancel方法:publicvoidcancel(){
stopAsyncDownload();
resetTask();
listener.onCancel();
}可以看到和暫停的邏輯差不多,只是在暫停后還需要對子任務(wù)重置從而使得下次下載從頭開始。底層到上層的通知機(jī)制前面提到,外部可以通過DownloadListener監(jiān)聽下載的進(jìn)度,下面是DownloadListener接口的定義:publicinterfaceDownloadListener{
defaultvoidonStart(){}
defaultvoidonDownloading(longprogress,longtotal){}
defaultvoidonPause(){}
defaultvoidonCancel(){}
defaultvoidonComplete(){}
defaultvoidonError(Stringmessage){}
}我們實時的下載進(jìn)度其實是在子任務(wù)的保存文件過程中才能體現(xiàn)出來的,同樣,子任務(wù)的下載失敗也需要通知到DownloadListener,這是怎么做到的呢?前面提到了,我們還定義了一個SubDownloadListener,其監(jiān)聽者就是子任務(wù)的父任務(wù)。通過監(jiān)聽我們可以將子任務(wù)狀態(tài)反饋到父任務(wù),父任務(wù)再根據(jù)具體情況反饋數(shù)據(jù)給Down
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025年上外版選擇性必修3生物上冊月考試卷含答案
- 2025年新科版九年級歷史下冊月考試卷
- 2025年浙教版選修4地理下冊月考試卷
- 2025年教科新版選修2地理下冊階段測試試卷
- 二零二五年度廣告宣傳攝影合同范本4篇
- 二零二五年度農(nóng)資質(zhì)量安全追溯體系建設(shè)合同3篇
- 二零二五年度牛場環(huán)保設(shè)施建設(shè)與運營合同范本4篇
- 2025年度文物拍賣合同標(biāo)準(zhǔn)版4篇
- 二零二五年度2025版木材加工廢棄物回收利用合同4篇
- 護(hù)工合同范本(2篇)
- 2024年湖南高速鐵路職業(yè)技術(shù)學(xué)院單招職業(yè)技能測試題庫及答案解析
- (正式版)SJT 11449-2024 集中空調(diào)電子計費信息系統(tǒng)工程技術(shù)規(guī)范
- 廣州綠色金融發(fā)展現(xiàn)狀及對策的研究
- 《近現(xiàn)代史》義和團(tuán)運動
- 人教版四年級上冊加減乘除四則混合運算300題及答案
- 合成生物學(xué)技術(shù)在生物制藥中的應(yīng)用
- 消化系統(tǒng)疾病的負(fù)性情緒與心理護(hù)理
- 高考語文文學(xué)類閱讀分類訓(xùn)練:戲劇類(含答案)
- 協(xié)會監(jiān)事會工作報告大全(12篇)
- WS-T 813-2023 手術(shù)部位標(biāo)識標(biāo)準(zhǔn)
- 同意更改小孩名字協(xié)議書
評論
0/150
提交評論