renameto 阻塞_打造简化版文件下载器
一. 前言
Executors 是一種典型的生產者 - 消費者模式, java中的線程池是運用場景最多的并發框架,幾乎所有需要異步或并發執行任務的程序都可以使用線程池。線程池就是將線程進行池化,需要運行任務時從池中拿一個線程來執行,執行完畢,線程放回池中。 在開發過程中,合理地使用線程池能夠帶來3個好處。第一:降低資源消耗。通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。
有沒有一種優化后的線程池方案可以進行統一的分配、調優和監控呢?今天我就帶大家手把手寫一個簡化版的線程下載器
它有如下優點支持文件下載監聽成功,失敗,暫停,下載進度回調
支持無限制添加任務到任務隊列
支持查看文件總下載大小
支持查看已經下載的大小
支持查看下載成功后的目標文件路徑
可以根據下載錯誤碼定位下載失敗原因
支持取消,暫停,開始下載任務
二. 關鍵技術
文件下載器不是很復雜,但是前提是你要對Executor一些參數要有簡單的了解下面我列了一個表格,對每個參數都詳細的介紹了一下
2.1 ThreadPoolExecutor 核心參數介紹參數名參數作用
2.1.1 RejectedExecutionHandler (飽和策閱)
線程池的飽和策略,當阻塞隊列滿了,且沒有空閑的工作線程,如果繼續提交任務,必須采取一種策略處理該任務,線程池提供了4種策略: CallerRunsPolicy AbortPolicy DiscardPolicy DiscardOldestPolicy,API文檔也給大家列好了,不記得的可以翻一翻策閱名稱策閱作用
2.1.2 workQueue (飽和策閱)
workQueue必須是BlockingQueue阻塞隊列。當線程池中的線程數超過它的corePoolSize的時候,線程會進入阻塞隊列進行阻塞等待。通過workQueue,線程池實現了阻塞功能
那么什么是阻塞隊列呢?
阻塞隊列常用于生產者和消費者的場景,生產者是向隊列里添加元素的線程,消費者是從隊列里取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。Java提供的阻塞隊列如下:阻塞隊列名稱概念
阻塞隊列怎么實現增刪改查呢?
這里表格也給大家準備好了:
2.2 如何創建一個線程池
所有的API介紹都一一和大家介紹了,那么我們該如何傳建線程池呢,創建線程池有五種方式: 定長線程池 , 可緩存的線程池 , 單線程的線程池還有長線程池,具體應用場景如下:
2.2.1 創建一個定長線程池,可控制線程最大并發數,超出的線程會在隊列中等待。
2.2.2 根據所需的并發數來動態創建和關閉線程。能夠合理的使用CPU進行對任務進行并發操作,所以適合使用在很耗時的任務。注意返回的是ForkJoinPool對象。
2.2.3 創建一個可緩存的線程池,可靈活回收空閑線程,若無可回收,則新建線程。
2.2.4 創建一個單線程的線程池
2.2.5. 創建一個定長線程池,支持定時及周期性任務執行。
2.3 ThreadPoolExecutor 源碼結構
2.4 線程池的工作原理如果當前運行的線程少于corePoolSize,則創建新線程來執行任務(注意,執行這一步驟需要獲取全局鎖)如果運行的線程等于或多于corePoolSize,則將任務加入BlockingQueue。如果無法將任務加入BlockingQueue(隊列已滿),則創建新的線程來處理任務(注意,執行這一步驟需要獲取全局鎖)。如果創建新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕,并調用RejectedExecutionHandler.rejectedExecution()方法。
整體來說: 在 線程池 的內部,我們維護了一個阻塞隊列 workQueue 和一組工作線程,工作線程的個數由構造函數中的 poolSize 來指定。用戶通過調用 execute() 方法來提交 Runnable 任務,execute() 方法的內部實現僅僅是將任務加入到 workQueue 中。線程池 內部維護的工作線程會消費 workQueue 中的任務并執行任務,里面是一個 while 循環。
2.5 如何關閉線程池:
2.5.1 shutDown
shutdown調用的是advanceRunState(SHUTDOWN),而shutdownNow調用的是(STOP),即調用后設置的線程池狀態不同
2.5.2 shutDownNow
shutdown調用的是中斷空閑的Workers,而shutdownNow調用的是中斷所有的Workers shutdownNow會把所有任務隊列中的任務取出來,返回一個任務列表。而shutdown什么都不返回。
2.6 如何合理的配置線程池
要想合理地配置線程池,就必須首先分析任務特性要想合理地配置線程池,就必須首先分析任務特性任務的性質:CPU密集型任務、IO密集型任務和混合型任務。
任務的優先級:高、中和低。
任務的執行時間:長、中和短。
任務的依賴性:是否依賴其他系統資源,如數據庫連接。
2.6.1 確定核心線程數
混合型的任務,如果可以分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那么分解后執行的吞吐量將高于串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的CPU個數。
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心線程數量大小
private static final int corePoolSize = Math.max(2, Math.min(CPU_COUNT - 1, 4));
2.6.2 確定線程池最大容納線程數
由于IO密集型任務線程并不是一直在執行任務,則應配置盡可能多的線程,如2*Ncpu。
private static final int maximumPoolSize = CPU_COUNT * 2 + 1;
2.6.3 線程池的創建
private static ThreadPoolProxy mBackgroundPool = null;
private static final Object mBackgroundLock = new Object();
private static ThreadPoolProxy mDownloadPool = null;
private static final Object mDownloadLock = new Object();
private static Map mMap = new HashMap<>();
private static final Object mSingleLock = new Object();
2.6.3.0 線程池策閱ThreadPoolProxy
public static class ThreadPoolProxy {
private ThreadPoolExecutor mPool;
private int mCorePoolSize;
private int mMaximumPoolSize;
private long mKeepAliveTime;
private boolean mIsPriority;
/**
* @param corePoolSize 核心線程數量
* @param maximumPoolSize 最大線程數量
* @param keepAliveTime 空閑線程存活時間,秒
*/
private ThreadPoolProxy(int corePoolSize, int maximumPoolSize, long keepAliveTime, boolean isPriority) {
mCorePoolSize = corePoolSize;
mMaximumPoolSize = maximumPoolSize;
mKeepAliveTime = keepAliveTime;
mIsPriority = isPriority;
}
執行任務,當線程池處于關閉,將會重新創建新的線程池
public synchronized void execute(Runnable run) {
if (run == null) {
return;
}
if (mPool == null || mPool.isShutdown()) {
//ThreadFactory是每次創建新的線程工廠
if (mIsPriority) {//使用優先級隊列
mPool = new ThreadPoolExecutor(mCorePoolSize, mMaximumPoolSize, mKeepAliveTime, TimeUnit.SECONDS, new PriorityBlockingQueue<>(), Executors.defaultThreadFactory(), new AbortPolicy());
} else {//隊列任務
mPool = new ThreadPoolExecutor(mCorePoolSize, mMaximumPoolSize, mKeepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new AbortPolicy());
}
}
mPool.execute(run);
}
取消線程池中某個還未執行的任務
public synchronized void remove(Runnable run) {
if (mPool != null && (!mPool.isShutdown() || mPool.isTerminating())) {
mPool.getQueue().remove(run);
}
}
/**
* 是否包含某個任務
*/
public synchronized boolean contains(Runnable run) {
if (mPool != null && (!mPool.isShutdown() || mPool.isTerminating())) {
return mPool.getQueue().contains(run);
} else {
return false;
}
}
關閉線程池
/**
* 關閉線程池,
*
* @param isNow if true 立即終止線程池,并嘗試打斷正在執行的任務,清空任務緩存隊列,返回尚未執行的任務。
* if false ,確保所有已經加入的任務都將會被執行完畢才關閉,后面不接受任務
**/
public synchronized void shutdown(boolean isNow) {
if (mPool != null && (!mPool.isShutdown() || mPool.isTerminating())) {
if (isNow) {
mPool.shutdownNow();
} else {
mPool.shutdown();
}
}
}
}
}
2.6.3.1 CPU密集型獲取后臺線程池,核心線程會一直存活。
CPU密集型任務應配置盡可能小的線程,如配置Ncpu+1個線程的線程池。
public static ThreadPoolProxy getBackgroundPool() {
synchronized (mBackgroundLock) {
if (mBackgroundPool == null) {
mBackgroundPool = new ThreadPoolProxy(corePoolSize, maximumPoolSize, 60L, false);
}
return mBackgroundPool;
}
}
2.6.3.2 并發線程池
獲取一個用于文件并發下載的線程池,修改核心線程數和最大線程數
public static ThreadPoolProxy getDownloadPool() {
synchronized (mDownloadLock) {
if (mDownloadPool == null) {
mDownloadPool = new ThreadPoolProxy(4, 12, 60L, true);
}
return mDownloadPool;
}
}
2.6.3.3 單線程池
獲取一個單線程池,所有任務將會被按照加入的順序執行,免除了同步開銷的問題
public static ThreadPoolProxy getSinglePool(String name) {
synchronized (mSingleLock) {
ThreadPoolProxy singlePool = mMap.get(name);
if (singlePool == null) {
singlePool = new ThreadPoolProxy(0, 1, 60L, false);
mMap.put(name, singlePool);
}
return singlePool;
}
}
三. 簡易版下載器實現
3.0.1 定義當前下載任務枚舉狀態 DownloadStatus枚舉名意義
3.0.2 url,斷點下載開關,線程優先級,連接、讀取超時時間動態配置
public class DownloadConfig {
public String url;//下載地址,唯一標示
public String targetPath;//下載成功后的文件路徑
public int intervalTime = 1000;//默認每秒回調一次
public boolean isCallBackInUIThread = true;//默認回調在主線程
public boolean isRange = true;//是否開啟斷點下載的功能,默認開啟
public int timeOutMillisecond = 30 * 1000;//連接、讀取超時時間
public int priority;//默認線程優先級0,普通任務0,so庫下載優先級8,其他更加情況自定義
public DownloadConfig(String url, String targetPath, boolean isRange, boolean isCallBackInUIThread, int intervalTime, int timeOutMillisecond) {
this.url = url;
this.targetPath = targetPath;
this.isRange = isRange;
this.isCallBackInUIThread = isCallBackInUIThread;
this.intervalTime = intervalTime;
this.timeOutMillisecond = timeOutMillisecond;
}
3.0.3 定制化下載任務接口 BaseDownloadTask方法名作用類型
3.0.4 下載器執行文件下載接口化
public interface IDownloader{
void downloadFile(DownloadTask task);
}
3.0.5 配置下載監聽器,提供監聽成功,失敗,暫停,下載進度回調 DownloadListener方法名作用參數介紹
3.0.6 封裝單個下載任務
單任務Task實現了我們的 Runnable ,里面主要是執行我們的任務,Comparable 給我們的任務設定優先級,BaseDownloadTask 就是給任務做一些簡單的任務開始,結束,暫停,取消下載等工作了
3.0.6.1 DownloadTask 成員變量
DownloadTask 成員變量需要關注幾個點:字段名作用類型
3.0.6.2 DownloadTask 構造方法
DownloadTask 構造方法有兩個,一個是將外部自定義所有參數塞到DownloadConfig,一個是直接將參數傳到DownloadTask里面
public DownloadTask(DownloadConfig config) {
this(config.url, config.targetPath, config.isRange, config.isCallBackInUIThread, config.intervalTime, config.timeOutMillisecond, config.priority);
}
public DownloadTask(String url, String targetPath, boolean isRange, boolean isCallBackInUIThread, int intervalTime, int timeOutMillisecond, int priority) {
this.url = url;
this.targetPath = targetPath;
this.isRange = isRange;
this.isCallBackInUIThread = isCallBackInUIThread;
this.intervalTime = intervalTime;
this.timeOutMillisecond = timeOutMillisecond;
if (this.intervalTime <= 0) {
this.intervalTime = 1000;
}
if (this.timeOutMillisecond <= 0) {
this.timeOutMillisecond = 30 * 1000;
}
this.status = DownloadStatus.STATUS_NONE;
this.tempFilePath = this.targetPath + tempSuffex;
this.priority = priority;
}
3.0.6.3 DownloadTask 添加任務add()
當添加任務時候,我們給下載任務狀態置為等待,然后在這個方法里面初始化下載成功后的文件路徑以及對應的文件名
@Override
public DownloadTask add() {
status = DownloadStatus.STATUS_WAITING;
final File file = new File(targetPath);
fileName = file.getName();
return this;
}
3.0.6.4 DownloadTask run() => 任務開始start()
如果下載緩存路徑合法,將該當前本地已下載的size返回業務層
@Override
public long getDownloadedSize() {
if (!TextUtils.isEmpty(tempFilePath)) {
File file = new File(tempFilePath);
downloadedSize = file.exists() ? file.length() : 0;
} else {
downloadedSize = 0;
}
return downloadedSize;
}
3.0.6.5 設置Range斷點下載開關
如果配置了開啟斷點下載,且已經下載過,設置Range.否則關閉斷點下載
public boolean isRangeRequest() {
return isRange && getDownloadedSize() > 0;
}
3.0.6.6 compareTo 比較任務的優先級
@Override
public int compareTo(DownloadTask o) {//任務執行優先級排序
if (this.getPriority() < o.priority) {
return 1;
}
if (this.getPriority() > o.priority) {
return -1;
}
return 0;
}
3.0.6.7 DownloadTask run() => 開始下載任務start()
執行任務開始的時候,重置任務狀態,然后在run()方法里面調用下載器FileDownloader執行任務下載
@Override
public void start() {
//狀態重置
this.status = DownloadStatus.STATUS_NONE;
//開始下載
if (downloader == null) {
downloader = new FileDownloader();
}
downloader.downloadFile(this);
}
3.0.6.8 DownloadTask 暫停下載任務 pause()
在暫停下載任務方法里,將下載任務設置為暫停狀態,然后用下載監聽器將本地問價大小和總文件大小回傳給業務層
@Override
public void pause() {
status = DownloadStatus.STATUS_PAUSED;
if (listener != null) {
listener.onPaused(this, soFarBytes, totalSize);
}
}
3.0.6.9 DownloadTask 取消下載任務 pause()
取消下載任務非常簡單,將下載任務設置為取消,然后刪除所有的副本文件
@Override
public void cancel() {
status = DownloadStatus.STATUS_NONE;
//刪除臨時文件
FileUtils.deleteFile(tempFilePath);
}
3.0.7.0 DownloadTask 設置DownloadListener下載監聽器 setListener()
@Override
public DownloadTask setListener(DownloadListener listener) {
this.listener = listener;
return this;
}
還有一些常用的方法就不一一貼代碼了,詳細API文檔如下:方法名作用類型
3.0.7 封裝文件下載器FileDownloader,支持斷點下載
3.0.7.1 FileDownloader文件下載器 處理單一任務文件下載 downloadFile
downloadFile 整體看起來是比較簡單的,主線程的Looper是在這里初始化的,在這里我們默認不走下載成功回調,接著我們看看downloadByHttpURLConn是如何實現的
@Override
public void downloadFile(DownloadTask task) {
if (task == null) return;
if (handler == null) {
handler = new Handler(Looper.getMainLooper());
}
hasCallbackSuccess = false;
downloadByHttpURLConn(task);
}
3.0.7.2 downloadByHttpURLConn
downloadByHttpURLConn是 使用系統API下載,如果服務端支持206分段請求,可以斷點下載
在這里我們將下載狀態置為 STATUS_DOWNLOADING ,并且創建 HttpURLConnection 建立 Http 連接,下載我們默認使用的是 GET 請求,并且整個連接處于Keep-Alive 長連接狀態,不設置緩存,可配置化斷點下載入口
task.status = DownloadTask.DownloadStatus.STATUS_DOWNLOADING;
HttpURLConnection conn = null;
InputStream inputStream = null;
try {
URL url = new URL(task.url);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(task.timeOutMillisecond);
conn.setReadTimeout(task.timeOutMillisecond);
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Cache-Control", "no-cache");
conn.setRequestProperty("pragma", "no-cache");
conn.setRequestProperty("Accept", "*/*");
//是否開啟range請求,code=416,表示請求范圍不對
String rangStr = "";
if (task.isRangeRequest()) {//isRangeRequest()方法里面會獲取最新的本地緩存文件downloadedSize大小
rangStr = "bytes=" + task.downloadedSize + "-";
conn.setRequestProperty("Range", rangStr);//+ task.totalSize
//Logger.d(TAG, "isRangeRequest" + task.downloadedSize);
}獲取服務器返回的狀態碼
//獲取服務器返回的狀態碼
int code = conn.getResponseCode();
if (200 == code || 206 == code) { //200 請求服務器資源全部返回成功 //206 請求部分服務器資源返回成功
final String length = conn.getHeaderField("Content-Length");
if (!TextUtils.isEmpty(length)) {//總長度
task.totalSize = Long.parseLong(length);
}
if (task.totalSize == 0) {
final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
task.isChunk = (transferEncoding != null && transferEncoding.equals("chunked"));
task.totalSize = TOTAL_VALUE_IN_CHUNKED_RESOURCE;
}
Map> map = conn.getHeaderFields();
//Logger.d(TAG, "code=" + code + ",length=" + length);
inputStream = conn.getInputStream();
startWriteFile(task, code, inputStream, rangStr, map);
} else {//出錯
task.errorCode = code;
if (416 == code) {//請求分段range有誤,本地臨時文件大小已經是有問題的
//刪除臨時文件
FileUtils.deleteFile(task.tempFilePath);
}
handleError(task, new Throwable("net request error code=" + code + "|" + rangStr + "|url:" + task.url + "|tempFile:" + task.targetPath));
}200 請求服務器資源全部返回成功
206 請求部分服務器資源返回成功Content-Length 文件總大小等于0,我們取消當前文件下載任務
非正常碼416請求分段range有誤,本地臨時文件大小已經是有問題的,我們要刪除臨時文件
不要在出錯的時候暴力刪除本地緩存
private void handleError(final DownloadTask downloadTask, final Throwable e) {
if (downloadTask == null) return;
downloadTask.status = DownloadTask.DownloadStatus.STATUS_ERROR;
DownloadManager.init().removeTask(downloadTask.url);
if (downloadTask.isCallBackInUIThread) {
handler.post(() -> {
Logger.d(TAG, "download error " + e + ",isCallBackInUIThread=" + downloadTask.isCallBackInUIThread);
if (downloadTask.listener != null) {
downloadTask.listener.onError(downloadTask, e);
}
});
} else {
if (downloadTask.listener != null) {
downloadTask.listener.onError(downloadTask, e);
}
}
}
當然我們不要在最好忘記關流和取消http連接
finally {
if (conn != null) conn.disconnect();
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.0.7.3 startWriteFile 寫文件
在downloadByHttpURLConn 成功返回碼有一個寫文件的方法startWriteFile,如果服務器支持206分段請求,寫文件用RandomAccessFile
寫文件的邏輯比較簡單,首先得刪除目標文件,然后在構建一個副本文件如果是200所以不用做斷點續傳處理
if (200 == code) {//非range請求
tempFile.delete();
downloadTask.downloadedSize = 0;
fileOutputStream = new FileOutputStream(tempFile);
}如果是206,斷點續傳
(206 == code) {//斷點續傳
currentSize = tempFile.length();
downloadTask.downloadedSize = currentSize;
raf = new RandomAccessFile(tempPath, "rw");
raf.seek(currentSize);
Logger.d(TAG, "206 range downloadedSize=" + currentSize + ",totalSize=" + downloadTask.totalSize);
}
然后執行文件讀寫工作,中間有三個注意事項
while ((len = inputStream.read(buffer)) != -1 && DownloadTask.DownloadStatus.STATUS_DOWNLOADING == downloadTask.status) {
if (200 == code) {
fileOutputStream.write(buffer, 0, len);
fileOutputStream.flush();
} else if (206 == code) {
raf.write(buffer, 0, len);
}
}每intervalTime毫秒回調一次,完成了下載必須回調
soFarBytes += len;
downloadTask.downloadedSize = currentSize + soFarBytes;
downloadTask.soFarBytes = soFarBytes;
if (System.currentTimeMillis() - start >= downloadTask.intervalTime || soFarBytes == downloadTask.totalSize) {
notifyProgress(downloadTask, soFarBytes);
start = System.currentTimeMillis();
}
}本地文件大小和服務器文件大小一樣,說明文件下載完畢執行下載成功回調
if (downloadTask.soFarBytes == downloadTask.totalSize) {
handleSuccess(downloadTask);
}寫文件出錯了,停止下載,回調錯誤.如果是塊傳輸,totalSize不是實際總大小
if (downloadTask.totalSize > 0 && downloadTask.soFarBytes > downloadTask.totalSize && !downloadTask.isChunk) {
String errorMsg = ERROR_DOWNLOAD_MORE_SIZE + " localSize=" + downloadTask.downloadedSize +
",soFar=" + downloadTask.soFarBytes + ",total=" + downloadTask.totalSize;
Logger.d(TAG, errorMsg);
downloadTask.cancel();
handleError(downloadTask, new RuntimeException(errorMsg));
}如果STATUS_DOWNLOADING下載狀態的文件,沒有回調成功,則回調失敗
if (DownloadTask.DownloadStatus.STATUS_DOWNLOADING == downloadTask.status) {
if (!hasCallbackSuccess &&
(!downloadTask.isChunk && downloadTask.soFarBytes == downloadTask.totalSize)) {
//非Chunk下載,并且下載的大小和總大小一致回調成功
handleSuccess(downloadTask);
} else if (!hasCallbackSuccess && downloadTask.isChunk) {
//Chunk下載,暫時可以認為成功
handleSuccess(downloadTask);
} else {
//失敗
StringBuilder headStr = new StringBuilder();
if (null != map) {
for (String key : map.keySet()) {
headStr.append("|").append(key).append("=").append(map.get(key).toString());
}
}
handleError(downloadTask, new Throwable("Error WriteFile,hasCallSuc:" + hasCallbackSuccess + "|headerStr:" + headStr + "|bytes:" + downloadTask.soFarBytes + "-" + downloadTask.totalSize + "-" + downloadTask.downloadedSize +
"|" + rangStr + "|url:" + downloadTask.url + "|tempFile:" + tempFile.getPath() + "-" + tempFile.length()));
}
}
3.0.7.4 進度通知回調 notifyProgress
如果當前任務在主線程中運行,那么我們就把下載進度更新操作切換到UI線程
private void notifyProgress(final DownloadTask downloadTask, final long soFarBytes) {
if (DownloadTask.DownloadStatus.STATUS_DOWNLOADING == downloadTask.status) {
if (downloadTask.isCallBackInUIThread) {
handler.post(() -> {
if (downloadTask.listener != null) {
downloadTask.listener.onProgress(downloadTask.downloadedSize, soFarBytes, downloadTask.totalSize);
}
});
} else {
if (downloadTask.listener != null) {
downloadTask.listener.onProgress(downloadTask.downloadedSize, soFarBytes, downloadTask.totalSize);
}
}
}
}
3.0.7.5 下載成功回調 handleSuccess
如果是分段傳輸,我們要保證一點下載文件大小要等于總文件大小 下載成功后,重命名,去除.temp臨時文件,并且將該鏈接下載任務從隊列移除,然后將成功下載回調給業務層
private void handleSuccess(final DownloadTask downloadTask) {
if (downloadTask == null) return;
if (hasCallbackSuccess) return;
downloadTask.status = DownloadTask.DownloadStatus.STATUS_DOWNLOADED;
if (downloadTask.isChunk) {
downloadTask.totalSize = downloadTask.downloadedSize;//保證最后完成下載時的文件大小回調
}
//完整下載后,重命名,去除.temp臨時文件
final File targetFile = new File(downloadTask.targetPath);
if (targetFile.exists()) {
targetFile.delete();
}
final File tempDownloadedFile = new File(downloadTask.tempFilePath);
tempDownloadedFile.renameTo(targetFile);
//Log.d("dq-handleSuccess", targetFile.getName() + ",url=" + downloadTask.url);
DownloadManager.init().removeTask(downloadTask.url);
if (downloadTask.isCallBackInUIThread) {
handler.post(() -> {
if (downloadTask.listener != null) {
downloadTask.listener.onSuccess(downloadTask);
}
});
} else {
if (downloadTask.listener != null) {
downloadTask.listener.onSuccess(downloadTask);
}
}
hasCallbackSuccess = true;
}
3.0.7.6 下載失敗回調 handleSuccess
不要在出錯的時候暴力刪除本地緩存,這樣可能其他回調會受到影響
private void handleError(final DownloadTask downloadTask, final Throwable e) {
if (downloadTask == null) return;
downloadTask.status = DownloadTask.DownloadStatus.STATUS_ERROR;
DownloadManager.init().removeTask(downloadTask.url);
if (downloadTask.isCallBackInUIThread) {
handler.post(() -> {
Logger.d(TAG, "download error " + e + ",isCallBackInUIThread=" + downloadTask.isCallBackInUIThread);
if (downloadTask.listener != null) {
downloadTask.listener.onError(downloadTask, e);
}
});
} else {
if (downloadTask.listener != null) {
downloadTask.listener.onError(downloadTask, e);
}
}
}
3.0.8 下載管理器 DownloadManager 封裝
3.0.8.1 DownloadManager()
DownloadManager 構造器主要拿到主線程的Looper, 以及初始化普通的下載線程池
private DownloadManager() {
mTaskMap = new ConcurrentHashMap<>();
mHandler = new Handler(Looper.getMainLooper());
downloadPool = ThreadManager.getDownloadPool();//普通的下載線程池;
}
3.0.8.2 init()
DownloadManager 是一個最簡單的單例實現,我們獲取DownloadManager 引用直接調用init 即可
public static DownloadManager init() {
return SingletonHolder.INSTANCE;
}
3.0.8.3 download() 文件下載
初始化文件下載任務,加入下載的線程池,默認在子線程回調的下載任務
/**
* 方便外部自定義所有參數
*
* @param config 下載參數,可擴展
* @param listener 下載監聽,回調
* @return 返回當前下載任務
*/
public synchronized DownloadTask download(DownloadConfig config, DownloadListener listener) {
if (config == null || TextUtils.isEmpty(config.url) || TextUtils.isEmpty(config.targetPath)) {
throw new NullPointerException();
}
final DownloadTask downloadTask = new DownloadTask(config)
.setListener(listener)
.add();
addToDownloader(downloadTask);
return downloadTask;
}
public synchronized DownloadTask download(String url, String targetPath, boolean isRange, boolean isCallBackInUIThread, int intervalTime, int timeOutMillisecond, DownloadListener listener) {
if (TextUtils.isEmpty(url) || TextUtils.isEmpty(targetPath)) {
throw new NullPointerException();
}
final DownloadTask downloadTask = new DownloadTask(url, targetPath, isRange, isCallBackInUIThread, intervalTime, timeOutMillisecond, 0)
.setListener(listener)
.add();
addToDownloader(downloadTask);
return downloadTask;
}
3.0.8.4 添加下載任務
如果下載的文件有版本差異,下載前請清除緩存文件,防止出現不同時段寫入文件,導致錯誤
public synchronized void addToDownloader(DownloadTask task) {
if (task == null || TextUtils.isEmpty(task.url)) {
throw new NullPointerException();
}
DownloadTask downloadTask = mTaskMap.get(task.url);
if (downloadTask == null) { // 創建一個新的下載任務,放入線程池
mTaskMap.put(task.url, task);
downloadPool.execute(task);
//Logger.d(TAG, "dq-start download:" + task.getUrl());
} else {
downloadTask.pause();
downloadTask.setListener(task.listener);//重新設置監聽
downloadPool.remove(downloadTask);
downloadPool.execute(downloadTask);//直接執行
}
}
3.0.8.5 暫停下載 pause
public synchronized boolean pause(String url) {
if (!TextUtils.isEmpty(url)) {
DownloadTask downloadTask = mTaskMap.get(url);
if (downloadTask != null) {
downloadTask.setListener(null);
downloadTask.pause();
removeTask(url);
//Logger.d(TAG, "pause download:" + downloadTask);
return true;
}
}
return false;
}
3.0.8.6 取消下載 cancel
取消下載,將監聽器置為null,然后清除已經下載文件,最后在線程池池里面remove task 即可
public synchronized void cancel(String url) {
if (TextUtils.isEmpty(url)) {
throw new NullPointerException();
}
DownloadTask downloadTask = mTaskMap.get(url);
if (downloadTask != null) {
downloadTask.setListener(null);
downloadTask.cancel();//會清除已經下載的文件
removeTask(url);
//Logger.d(TAG, "cancel download:" + downloadTask);
}
}
3.0.8.7 清除task緩存 removeTask
清除任務比較簡單,直接在線程池里面remove task 即可
public synchronized DownloadTask removeTask(String url) {
final DownloadTask downloadTask = mTaskMap.remove(url);
if (downloadTask != null) {
downloadPool.remove(downloadTask);
}
return downloadTask;
}
3.0.8.8 程序退出時停止下載任務 onAppExit
退出應用程序前務必清除所有任務隊列暫停然后清除里面所有的任務
public synchronized void onAppExit() {
for (DownloadTask downloadTask : mTaskMap.values()) {
downloadTask.setListener(null);
downloadTask.pause();
downloadPool.remove(downloadTask);
}
mTaskMap.clear();
downloadPool.shutdown(true);
}
總結
以上是生活随笔為你收集整理的renameto 阻塞_打造简化版文件下载器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大数据治理工程师_大数据治理关键技术解析
- 下一篇: 分块的单点修改查询区间和_模版 单点修