Android實現多執行緒下載檔案,支援斷點
本篇部落格主要介紹多執行緒去下載檔案,以下載多個app為例。不管去下在app,音視訊等檔案,實現起來都一樣。篇幅有點長,先貼張美女圖看看
正在下載的效果圖
下載完成效果圖
小編的下載路徑是放在sd卡的絕對路徑中,方便驗證!
工程目錄圖
介紹下每個類是幹什麼的
DownloadCallback:下載完成回撥介面,包含三個方法 void onSuccess(File file)、void onFailure(Exception e)、void onProgress(long progress,long currentLength);
DownloadDispatcher:負責建立執行緒池,連線下載的檔案;
DownloadRunnable:每個執行緒的執行對應的任務;
DownloadTask:每個apk的下載,這個類需要複用的;
ircleProgressbar:自定義的圓形進度條;
具體思路:
1、首先自定義一個圓形進度條CircleProgressbar,實時更新進度
2、建立執行緒池,計算每個執行緒對應的不同的Range
3、每個執行緒下載完畢之後的回撥,若出現了異常怎麼處理
OkHttpManager類
public class OkHttpManager {
private static final OkHttpManager sOkHttpManager = new OkHttpManager();
private OkHttpClient okHttpClient;
private OkHttpManager() {
okHttpClient = new OkHttpClient();
}
public static OkHttpManager getInstance() {
return sOkHttpManager;
}
public Call asyncCall(String url) {
Request request = new Request.Builder()
.url(url)
.build();
return okHttpClient.newCall(request);
}
public Response syncResponse(String url, long start, long end) throws IOException {
Request request = new Request.Builder()
.url(url)
//Range 請求頭格式Range: bytes=start-end
.addHeader("Range", "bytes=" + start + "-" + end)
.build();
return okHttpClient.newCall(request).execute();
}
}
大家可能會看到這個Range很懵,Range是啥?
什麼是Range?
當用戶在聽一首歌的時候,如果聽到一半(網路下載了一半),網路斷掉了,使用者需要繼續聽的時候,檔案伺服器不支援斷點的話,則使用者需要重新下載這個檔案。而Range支援的話,客戶端應該記錄了之前已經讀取的檔案範圍,網路恢復之後,則向伺服器傳送讀取剩餘Range的請求,服務端只需要傳送客戶端請求的那部分內容,而不用整個 檔案傳送回客戶端,以此節省網路頻寬。
例如:
Range: bytes=10- :第10個位元組及最後個位元組的資料 。
Range: bytes=40-100 :第40個位元組到第100個位元組之間的資料。
注意,這個表示[start,end],即是包含請求頭的start及end位元組的,所以,下一個請求,應該是上一個請求的[end+1, nextEnd]
DownloadCallback類
public interface DownloadCallback {
/**
* 下載成功
*
* @param file
*/
void onSuccess(File file);
/**
* 下載失敗
*
* @param e
*/
void onFailure(Exception e);
/**
* 下載進度
*
* @param progress
*/
void onProgress(long progress,long currentLength);
}
DownloadCallback:下載完成回撥介面,包含三個方法 void onSuccess(File file)下載檔案成功回撥、void onFailure(Exception e)下載檔案失敗回撥、void onProgress(long progress,long currentLength) 下載檔案實時更新下圓形進度條。
DownloadDispatcher 類
public class DownloadDispatcher {
private static final String TAG = "DownloadDispatcher";
private static volatile DownloadDispatcher sDownloadDispatcher;
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int THREAD_SIZE = Math.max(3, Math.min(CPU_COUNT - 1, 5));
//核心執行緒數
private static final int CORE_POOL_SIZE = THREAD_SIZE;
//執行緒池
private ExecutorService mExecutorService;
//private final Deque<DownloadTask> readyTasks = new ArrayDeque<>();
private final Deque<DownloadTask> runningTasks = new ArrayDeque<>();
//private final Deque<DownloadTask> stopTasks = new ArrayDeque<>();
private DownloadDispatcher() {
}
public static DownloadDispatcher getInstance() {
if (sDownloadDispatcher == null) {
synchronized (DownloadDispatcher.class) {
if (sDownloadDispatcher == null) {
sDownloadDispatcher = new DownloadDispatcher();
}
}
}
return sDownloadDispatcher;
}
/**
* 建立執行緒池
*
* @return mExecutorService
*/
public synchronized ExecutorService executorService() {
if (mExecutorService == null) {
mExecutorService = new ThreadPoolExecutor(CORE_POOL_SIZE, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), new ThreadFactory() {
@Override
public Thread newThread(@NonNull Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(false);
return thread;
}
});
}
return mExecutorService;
}
/**
* @param name 檔名
* @param url 下載的地址
* @param callBack 回撥介面
*/
public void startDownload(final String name, final String url, final DownloadCallback callBack) {
Call call = OkHttpManager.getInstance().asyncCall(url);
call.enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callBack.onFailure(e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
//獲取檔案的大小
long contentLength = response.body().contentLength();
Log.i(TAG, "contentLength=" + contentLength);
if (contentLength <= -1) {
return;
}
DownloadTask downloadTask = new DownloadTask(name, url, THREAD_SIZE, contentLength, callBack);
downloadTask.init();
runningTasks.add(downloadTask);
}
});
}
/**
* @param downLoadTask 下載任務
*/
public void recyclerTask(DownloadTask downLoadTask) {
runningTasks.remove(downLoadTask);
//參考OkHttp的Dispatcher()的原始碼
//readyTasks.
}
public void stopDownLoad(String url) {
//這個停止是不是這個正在下載的
}
}
DownloadDispatcher這個類主要負責建立執行緒池,連線下載的檔案,如果你要控制下載檔案的個數,比如3-5個,可以在這個類控制,比如你最大允許同時下載三個檔案,每個檔案有五個執行緒去下載,那麼maxRequest只有15個執行緒,其餘的可以放到readyTasks 中,有一個執行緒下載完畢了可以remove()掉,總結起來說一句話,去仿照okhttp的Dispatcher原始碼去寫,runningTasks、readyTasks、stopTasks。
DownloadTask類
public class DownloadTask {
private static final String TAG = "DownloadTask";
//檔案下載的url
private String url;
//檔案的名稱
private String name;
//檔案的大小
private long mContentLength;
//下載檔案的執行緒的個數
private int mThreadSize;
//執行緒下載成功的個數,變數加個volatile,多執行緒保證變數可見性
private volatile int mSuccessNumber;
//總進度=每個執行緒的進度的和
private long mTotalProgress;
private List<DownloadRunnable> mDownloadRunnables;
private DownloadCallback mDownloadCallback;
public DownloadTask(String name, String url, int threadSize, long contentLength, DownloadCallback callBack) {
this.name = name;
this.url = url;
this.mThreadSize = threadSize;
this.mContentLength = contentLength;
this.mDownloadRunnables = new ArrayList<>();
this.mDownloadCallback = callBack;
}
public void init() {
for (int i = 0; i < mThreadSize; i++) {
//初始化的時候,需要讀取資料庫
//每個執行緒的下載的大小threadSize
long threadSize = mContentLength / mThreadSize;
//開始下載的位置
long start = i * threadSize;
//結束下載的位置
long end = start + threadSize - 1;
if (i == mThreadSize - 1) {
end = mContentLength - 1;
}
DownloadRunnable downloadRunnable = new DownloadRunnable(name, url, mContentLength, i, start, end, new DownloadCallback() {
@Override
public void onFailure(Exception e) {
//有一個執行緒發生異常,下載失敗,需要把其它執行緒停止掉
mDownloadCallback.onFailure(e);
stopDownload();
}
@Override
public void onSuccess(File file) {
mSuccessNumber = mSuccessNumber + 1;
if (mSuccessNumber == mThreadSize) {
mDownloadCallback.onSuccess(file);
DownloadDispatcher.getInstance().recyclerTask(DownloadTask.this);
//如果下載完畢,清除資料庫 todo
}
}
@Override
public void onProgress(long progress, long currentLength) {
//疊加下progress,實時去更新進度條
//這裡需要synchronized下
synchronized (DownloadTask.this) {
mTotalProgress = mTotalProgress + progress;
//Log.i(TAG, "mTotalProgress==" + mTotalProgress);
mDownloadCallback.onProgress(mTotalProgress, currentLength);
}
}
});
//通過執行緒池去執行
DownloadDispatcher.getInstance().executorService().execute(downloadRunnable);
mDownloadRunnables.add(downloadRunnable);
}
}
/**
* 停止下載
*/
public void stopDownload() {
for (DownloadRunnable runnable : mDownloadRunnables) {
runnable.stop();
}
}
DownloadTask負責每個apk的下載,這個類需要複用的。計算每個執行緒下載範圍的大小,具體的每個變數是啥?註釋寫的很清楚。注意的是這個變數mSuccessNumber,執行緒下載成功的個數,變數加個volatile,多執行緒保證變數可見性。還有的就是疊加下progress的時候mTotalProgress = mTotalProgress + progress,需要synchronized(DownloadTask.this)下,保證這個變數mTotalProgress記憶體可見,並同步下。
DownloadRunnable類
public class DownloadRunnable implements Runnable {
private static final String TAG = "DownloadRunnable";
private static final int STATUS_DOWNLOADING = 1;
private static final int STATUS_STOP = 2;
//執行緒的狀態
private int mStatus = STATUS_DOWNLOADING;
//檔案下載的url
private String url;
//檔案的名稱
private String name;
//執行緒id
private int threadId;
//每個執行緒下載開始的位置
private long start;
//每個執行緒下載結束的位置
private long end;
//每個執行緒的下載進度
private long mProgress;
//檔案的總大小 content-length
private long mCurrentLength;
private DownloadCallback downloadCallback;
public DownloadRunnable(String name, String url, long currentLength, int threadId, long start, long end, DownloadCallback downloadCallback) {
this.name = name;
this.url = url;
this.mCurrentLength = currentLength;
this.threadId = threadId;
this.start = start;
this.end = end;
this.downloadCallback = downloadCallback;
}
@Override
public void run() {
InputStream inputStream = null;
RandomAccessFile randomAccessFile = null;
try {
Response response = OkHttpManager.getInstance().syncResponse(url, start, end);
Log.i(TAG, "fileName=" + name + " 每個執行緒負責下載檔案大小contentLength=" + response.body().contentLength()
+ " 開始位置start=" + start + "結束位置end=" + end + " threadId=" + threadId);
inputStream = response.body().byteStream();
//儲存檔案的路徑
File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), name);
randomAccessFile = new RandomAccessFile(file, "rwd");
//seek從哪裡開始
randomAccessFile.seek(start);
int length;
byte[] bytes = new byte[10 * 1024];
while ((length = inputStream.read(bytes)) != -1) {
if (mStatus == STATUS_STOP)
break;
//寫入
randomAccessFile.write(bytes, 0, length);
//儲存下進度,做斷點 todo
mProgress = mProgress + length;
//實時去更新下進度條,將每次寫入的length傳出去
downloadCallback.onProgress(length, mCurrentLength);
}
downloadCallback.onSuccess(file);
} catch (IOException e) {
e.printStackTrace();
downloadCallback.onFailure(e);
} finally {
Utils.close(inputStream);
Utils.close(randomAccessFile);
//儲存到資料庫 怎麼存?? todo
}
}
public void stop() {
mStatus = STATUS_STOP;
}
}
DownloadRunnable負責每個執行緒的執行對應的任務,使用RandomAccessFile寫入檔案。最後看一張截圖
哈,看到最後斷點下載並沒有實現,這個不急,小編還沒寫,但是實現多執行緒下載檔案整體的思路和程式碼都已經出來了,至於斷點怎麼弄,其實具體的思路,在程式碼註解也已經寫出來了,主要是DownloadRunnable 這個類,向資料庫儲存下檔案的下載路徑url,檔案的大小currentLength,每個執行緒id,對應的每個執行緒的start位置和結束位置,以及每個執行緒的下載進度progress。使用者下次進來可以讀取資料庫的內容,有網的情況下重新發請請求,對應Range: bytes=start-end;
整個專案貼上了部分主要的程式碼。
專案完整程式碼:https://github.com/StevenYan88/MultiThreadDownload
如果小編的文章對你有幫助,或者喜歡小編的技術文章,可以關注下公眾號,大家互相學習,共同進步!