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