您好,登錄后才能下訂單哦!
怎么在Android中利用多線程實(shí)現(xiàn)一個(gè)斷點(diǎn)續(xù)傳下載功能?相信很多沒(méi)有經(jīng)驗(yàn)的人對(duì)此束手無(wú)策,為此本文總結(jié)了問(wèn)題出現(xiàn)的原因和解決方法,通過(guò)這篇文章希望你能解決這個(gè)問(wèn)題。
原理
其實(shí)斷點(diǎn)續(xù)傳的原理很簡(jiǎn)單,從字面上理解,所謂斷點(diǎn)續(xù)傳就是從停止的地方重新下載。
斷點(diǎn):線程停止的位置。
續(xù)傳:從停止的位置重新下載。
用代碼解析就是:
斷點(diǎn):當(dāng)前線程已經(jīng)下載完成的數(shù)據(jù)長(zhǎng)度。
續(xù)傳:向服務(wù)器請(qǐng)求上次線程停止位置之后的數(shù)據(jù)。
原理知道了,功能實(shí)現(xiàn)起來(lái)也簡(jiǎn)單。每當(dāng)線程停止時(shí)就把已下載的數(shù)據(jù)長(zhǎng)度寫(xiě)入記錄文件,當(dāng)重新下載時(shí),從記錄文件讀取已經(jīng)下載了的長(zhǎng)度。而這個(gè)長(zhǎng)度就是所需要的斷點(diǎn)。
續(xù)傳的實(shí)現(xiàn)也簡(jiǎn)單,可以通過(guò)設(shè)置網(wǎng)絡(luò)請(qǐng)求參數(shù),請(qǐng)求服務(wù)器從指定的位置開(kāi)始讀取數(shù)據(jù)。
而要實(shí)現(xiàn)這兩個(gè)功能只需要使用到HttpURLconnection里面的setRequestProperty方法便可以實(shí)現(xiàn).
public void setRequestProperty(String field, String newValue)
如下所示,便是向服務(wù)器請(qǐng)求500-1000之間的500個(gè)byte:
conn.setRequestProperty("Range", "bytes=" + 500 + "-" + 1000);
以上只是續(xù)傳的一部分需求,當(dāng)我們獲取到下載數(shù)據(jù)時(shí),還需要將數(shù)據(jù)寫(xiě)入文件,而普通發(fā)File對(duì)象并不提供從指定位置寫(xiě)入數(shù)據(jù)的功能,這個(gè)時(shí)候,就需要使用到RandomAccessFile來(lái)實(shí)現(xiàn)從指定位置給文件寫(xiě)入數(shù)據(jù)的功能。
public void seek(long offset)
如下所示,便是從文件的的第100個(gè)byte后開(kāi)始寫(xiě)入數(shù)據(jù)。
raFile.seek(100);
而開(kāi)始寫(xiě)入數(shù)據(jù)時(shí)還需要用到RandomAccessFile里面的另外一個(gè)方法
public void write(byte[] buffer, int byteOffset, int byteCount)
該方法的使用和OutputStream的write的使用一模一樣…
以上便是斷點(diǎn)續(xù)傳的原理。
多線程斷點(diǎn)續(xù)傳
而多線程斷點(diǎn)續(xù)傳便是在單線程的斷點(diǎn)續(xù)傳上延伸的,而多線程斷點(diǎn)續(xù)傳是把整個(gè)文件分割成幾個(gè)部分,每個(gè)部分由一條線程執(zhí)行下載,而每一條下載線程都要實(shí)現(xiàn)斷點(diǎn)續(xù)傳功能。
為了實(shí)現(xiàn)文件分割功能,我們需要使用到HttpURLconnection的另外一個(gè)方法:
public int getContentLength()
當(dāng)請(qǐng)求成功時(shí),可以通過(guò)該方法獲取到文件的總長(zhǎng)度。
每一條線程下載大小 = fileLength / THREAD_NUM
如下圖所示,描述的便是多線程的下載模型:
在多線程斷點(diǎn)續(xù)傳下載中,有一點(diǎn)需要特別注意:
由于文件是分成多個(gè)部分是被不同的線程的同時(shí)下載的,這就需要,每一條線程都分別需要有一個(gè)斷點(diǎn)記錄,和一個(gè)線程完成狀態(tài)的記錄;
如下圖所示:
只有所有線程的下載狀態(tài)都處于完成狀態(tài)時(shí),才能表示文件已經(jīng)下載完成。
實(shí)現(xiàn)記錄的方法多種多樣,我這里采用的是JDK自帶的Properties類(lèi)來(lái)記錄下載參數(shù)。
斷點(diǎn)續(xù)傳結(jié)構(gòu)
通過(guò)原理的了解,便可以很快的設(shè)計(jì)出斷點(diǎn)續(xù)傳工具類(lèi)的基本結(jié)構(gòu)圖
IDownloadListener.java
package com.arialyy.frame.http.inf; import java.net.HttpURLConnection; /** * 在這里面編寫(xiě)你的業(yè)務(wù)邏輯 */ public interface IDownloadListener { /** * 取消下載 */ public void onCancel(); /** * 下載失敗 */ public void onFail(); /** * 下載預(yù)處理,可通過(guò)HttpURLConnection獲取文件長(zhǎng)度 */ public void onPreDownload(HttpURLConnection connection); /** * 下載監(jiān)聽(tīng) */ public void onProgress(long currentLocation); /** * 單一線程的結(jié)束位置 */ public void onChildComplete(long finishLocation); /** * 開(kāi)始 */ public void onStart(long startLocation); /** * 子程恢復(fù)下載的位置 */ public void onChildResume(long resumeLocation); /** * 恢復(fù)位置 */ public void onResume(long resumeLocation); /** * 停止 */ public void onStop(long stopLocation); /** * 下載完成 */ public void onComplete(); }
該類(lèi)是下載監(jiān)聽(tīng)接口
DownloadListener.java
import java.net.HttpURLConnection; /** * 下載監(jiān)聽(tīng) */ public class DownloadListener implements IDownloadListener { @Override public void onResume(long resumeLocation) { } @Override public void onCancel() { } @Override public void onFail() { } @Override public void onPreDownload(HttpURLConnection connection) { } @Override public void onProgress(long currentLocation) { } @Override public void onChildComplete(long finishLocation) { } @Override public void onStart(long startLocation) { } @Override public void onChildResume(long resumeLocation) { } @Override public void onStop(long stopLocation) { } @Override public void onComplete() { } }
下載參數(shù)實(shí)體
/** * 子線程下載信息類(lèi) */ private class DownloadEntity { //文件總長(zhǎng)度 long fileSize; //下載鏈接 String downloadUrl; //線程Id int threadId; //起始下載位置 long startLocation; //結(jié)束下載的文章 long endLocation; //下載文件 File tempFile; Context context; public DownloadEntity(Context context, long fileSize, String downloadUrl, File file, int threadId, long startLocation, long endLocation) { this.fileSize = fileSize; this.downloadUrl = downloadUrl; this.tempFile = file; this.threadId = threadId; this.startLocation = startLocation; this.endLocation = endLocation; this.context = context; } }
該類(lèi)是下載信息配置類(lèi),每一條子線程的下載都需要一個(gè)下載實(shí)體來(lái)配置下載信息。
下載任務(wù)線程
/** * 多線程下載任務(wù)類(lèi) */ private class DownLoadTask implements Runnable { private static final String TAG = "DownLoadTask"; private DownloadEntity dEntity; private String configFPath; public DownLoadTask(DownloadEntity downloadInfo) { this.dEntity = downloadInfo; configFPath = dEntity.context.getFilesDir().getPath() + "/temp/" + dEntity.tempFile.getName() + ".properties"; } @Override public void run() { try { L.d(TAG, "線程_" + dEntity.threadId + "_正在下載【" + "開(kāi)始位置 : " + dEntity.startLocation + ",結(jié)束位置:" + dEntity.endLocation + "】"); URL url = new URL(dEntity.downloadUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //在頭里面請(qǐng)求下載開(kāi)始位置和結(jié)束位置 conn.setRequestProperty("Range", "bytes=" + dEntity.startLocation + "-" + dEntity.endLocation); conn.setRequestMethod("GET"); conn.setRequestProperty("Charset", "UTF-8"); conn.setConnectTimeout(TIME_OUT); conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"); conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"); conn.setReadTimeout(2000); //設(shè)置讀取流的等待時(shí)間,必須設(shè)置該參數(shù) InputStream is = conn.getInputStream(); //創(chuàng)建可設(shè)置位置的文件 RandomAccessFile file = new RandomAccessFile(dEntity.tempFile, "rwd"); //設(shè)置每條線程寫(xiě)入文件的位置 file.seek(dEntity.startLocation); byte[] buffer = new byte[1024]; int len; //當(dāng)前子線程的下載位置 long currentLocation = dEntity.startLocation; while ((len = is.read(buffer)) != -1) { if (isCancel) { L.d(TAG, "++++++++++ thread_" + dEntity.threadId + "_cancel ++++++++++"); break; } if (isStop) { break; } //把下載數(shù)據(jù)數(shù)據(jù)寫(xiě)入文件 file.write(buffer, 0, len); synchronized (DownLoadUtil.this) { mCurrentLocation += len; mListener.onProgress(mCurrentLocation); } currentLocation += len; } file.close(); is.close(); if (isCancel) { synchronized (DownLoadUtil.this) { mCancelNum++; if (mCancelNum == THREAD_NUM) { File configFile = new File(configFPath); if (configFile.exists()) { configFile.delete(); } if (dEntity.tempFile.exists()) { dEntity.tempFile.delete(); } L.d(TAG, "++++++++++++++++ onCancel +++++++++++++++++"); isDownloading = false; mListener.onCancel(); System.gc(); } } return; } //停止?fàn)顟B(tài)不需要?jiǎng)h除記錄文件 if (isStop) { synchronized (DownLoadUtil.this) { mStopNum++; String location = String.valueOf(currentLocation); L.i(TAG, "thread_" + dEntity.threadId + "_stop, stop location ==> " + currentLocation); writeConfig(dEntity.tempFile.getName() + "_record_" + dEntity.threadId, location); if (mStopNum == THREAD_NUM) { L.d(TAG, "++++++++++++++++ onStop +++++++++++++++++"); isDownloading = false; mListener.onStop(mCurrentLocation); System.gc(); } } return; } L.i(TAG, "線程【" + dEntity.threadId + "】下載完畢"); writeConfig(dEntity.tempFile.getName() + "_state_" + dEntity.threadId, 1 + ""); mListener.onChildComplete(dEntity.endLocation); mCompleteThreadNum++; if (mCompleteThreadNum == THREAD_NUM) { File configFile = new File(configFPath); if (configFile.exists()) { configFile.delete(); } mListener.onComplete(); isDownloading = false; System.gc(); } } catch (MalformedURLException e) { e.printStackTrace(); isDownloading = false; mListener.onFail(); } catch (IOException e) { FL.e(this, "下載失敗【" + dEntity.downloadUrl + "】" + FL.getPrintException(e)); isDownloading = false; mListener.onFail(); } catch (Exception e) { FL.e(this, "獲取流失敗" + FL.getPrintException(e)); isDownloading = false; mListener.onFail(); } }
這個(gè)是每條下載子線程的下載任務(wù)類(lèi),子線程通過(guò)下載實(shí)體對(duì)每一條線程進(jìn)行下載配置,由于在多斷點(diǎn)續(xù)傳的概念里,停止表示的是暫停狀態(tài),而恢復(fù)表示的是線程從記錄的斷點(diǎn)重新進(jìn)行下載,所以,線程處于停止?fàn)顟B(tài)時(shí)是不能刪除記錄文件的。
下載入口
/** * 多線程斷點(diǎn)續(xù)傳下載文件,暫停和繼續(xù) * * @param context 必須添加該參數(shù),不能使用全局變量的context * @param downloadUrl 下載路徑 * @param filePath 保存路徑 * @param downloadListener 下載進(jìn)度監(jiān)聽(tīng) {@link DownloadListener} */ public void download(final Context context, @NonNull final String downloadUrl, @NonNull final String filePath, @NonNull final DownloadListener downloadListener) { isDownloading = true; mCurrentLocation = 0; isStop = false; isCancel = false; mCancelNum = 0; mStopNum = 0; final File dFile = new File(filePath); //讀取已完成的線程數(shù) final File configFile = new File(context.getFilesDir().getPath() + "/temp/" + dFile.getName() + ".properties"); try { if (!configFile.exists()) { //記錄文件被刪除,則重新下載 newTask = true; FileUtil.createFile(configFile.getPath()); } else { newTask = false; } } catch (Exception e) { e.printStackTrace(); mListener.onFail(); return; } newTask = !dFile.exists(); new Thread(new Runnable() { @Override public void run() { try { mListener = downloadListener; URL url = new URL(downloadUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Charset", "UTF-8"); conn.setConnectTimeout(TIME_OUT); conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"); conn.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"); conn.connect(); int len = conn.getContentLength(); if (len < 0) { //網(wǎng)絡(luò)被劫持時(shí)會(huì)出現(xiàn)這個(gè)問(wèn)題 mListener.onFail(); return; } int code = conn.getResponseCode(); if (code == 200) { int fileLength = conn.getContentLength(); //必須建一個(gè)文件 FileUtil.createFile(filePath); RandomAccessFile file = new RandomAccessFile(filePath, "rwd"); //設(shè)置文件長(zhǎng)度 file.setLength(fileLength); mListener.onPreDownload(conn); //分配每條線程的下載區(qū)間 Properties pro = null; pro = Util.loadConfig(configFile); int blockSize = fileLength / THREAD_NUM; SparseArray<Thread> tasks = new SparseArray<>(); for (int i = 0; i < THREAD_NUM; i++) { long startL = i * blockSize, endL = (i + 1) * blockSize; Object state = pro.getProperty(dFile.getName() + "_state_" + i); if (state != null && Integer.parseInt(state + "") == 1) { //該線程已經(jīng)完成 mCurrentLocation += endL - startL; L.d(TAG, "++++++++++ 線程_" + i + "_已經(jīng)下載完成 ++++++++++"); mCompleteThreadNum++; if (mCompleteThreadNum == THREAD_NUM) { if (configFile.exists()) { configFile.delete(); } mListener.onComplete(); isDownloading = false; System.gc(); return; } continue; } //分配下載位置 Object record = pro.getProperty(dFile.getName() + "_record_" + i); if (!newTask && record != null && Long.parseLong(record + "") > 0) { //如果有記錄,則恢復(fù)下載 Long r = Long.parseLong(record + ""); mCurrentLocation += r - startL; L.d(TAG, "++++++++++ 線程_" + i + "_恢復(fù)下載 ++++++++++"); mListener.onChildResume(r); startL = r; } if (i == (THREAD_NUM - 1)) { endL = fileLength;//如果整個(gè)文件的大小不為線程個(gè)數(shù)的整數(shù)倍,則最后一個(gè)線程的結(jié)束位置即為文件的總長(zhǎng)度 } DownloadEntity entity = new DownloadEntity(context, fileLength, downloadUrl, dFile, i, startL, endL); DownLoadTask task = new DownLoadTask(entity); tasks.put(i, new Thread(task)); } if (mCurrentLocation > 0) { mListener.onResume(mCurrentLocation); } else { mListener.onStart(mCurrentLocation); } for (int i = 0, count = tasks.size(); i < count; i++) { Thread task = tasks.get(i); if (task != null) { task.start(); } } } else { FL.e(TAG, "下載失敗,返回碼:" + code); isDownloading = false; System.gc(); mListener.onFail(); } } catch (IOException e) { FL.e(this, "下載失敗【downloadUrl:" + downloadUrl + "】\n【filePath:" + filePath + "】" + FL.getPrintException(e)); isDownloading = false; mListener.onFail(); } } }).start(); }
其實(shí)也沒(méi)啥好說(shuō)的,注釋已經(jīng)很完整了,需要注意兩點(diǎn)
1、恢復(fù)下載時(shí):已下載的文件大小 = 該線程的上一次斷點(diǎn)的位置 - 該線程起始下載位置;
2、為了保證下載文件的完整性,只要記錄文件不存在就需要重新進(jìn)行下載;
看完上述內(nèi)容,你們掌握怎么在Android中利用多線程實(shí)現(xiàn)一個(gè)斷點(diǎn)續(xù)傳下載功能的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。