一. 前言

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 阻塞_打造简化版文件下载器相关推荐

  1. Python利用tcp_socket实现文件下载器

    Python利用tcp_socket实现简单文件下载功能 文章目录 Python利用tcp_socket实现简单文件下载功能 一.客户端 二.服务端 三.成果展示 3.1 创建测试文件 3.2 下载测 ...

  2. python高级 笔记 08 (TCP服务器增强 + 文件下载器 案例 综合应用)

    TCP服务器接受客户端多条信息 主要方式: 通过在接受信息,等待数据 recv前增加一个死循环 import sockettcp_client_socket = socket.socket(socke ...

  3. 卓大师刷机专家PC版 v4.4.0.28 官网版_安卓手机管理器

    卓大师刷机专家PC版 v4.4.0.28 官网版_安卓手机管理器 软件大小:9.78MB 软件语言:简体中文 软件性质:常用软件 软件授权:官方版 更新时间:2014-05-01 应用平台:/WinX ...

  4. 网络 #TCP #UDP #文件下载器 #网络通信

    网络 网络基础 1)IP地址 1>查看网卡信息 2>IP地址分类 3>私有IP 2)端口 1>端口的分类(部分) 1.知名端口 2.动态端口 2>查看端口号 3)sock ...

  5. QT纯代码打造音乐播放器

    QT纯代码打造音乐播放器 在.pro文件中添加 QT = prmultimedia 然后就是在.h文件中添加相关库函数 我添加的库函数 有些是不需要的,可以自定义删除修改 我是为方便以后扩展功能就留下 ...

  6. python写一个文件下载器_Python3使用TCP编写一个简易的文件下载器

    原标题:Python3使用TCP编写一个简易的文件下载器 利用Python3来实现TCP协议,和UDP类似.UDP应用于及时通信,而TCP协议用来传送文件.命令等操作,因为这些数据不允许丢失,否则会造 ...

  7. 使用IntentService给自己的Android应用写一个文件下载器。

    接着上一篇的http://www.cnblogs.com/zhengxt/p/3657833.html,当我们想给自己的APP写一个文件下载器时,可以用重写IntentService来实现. 使用In ...

  8. python写一个文件下载器_python使用tcp实现一个简单的下载器

    上一篇中介绍了tcp的流程,本篇通过写一个简单的文件下载器程序来巩固之前学的知识. 文件下载器的流程如下: 客户端: 输入目标服务器的ip和port 输入要下载文件的名称 从服务器下载文件保存到本地 ...

  9. 【多线程编程学习】java多线程基于数据分割的大文件下载器

    文章目录 代码:基于数据分割的大文件下载器 作为包装的存储对象类: 主文件下载类: 子任务下载类: 处理缓存: 启动类: 数据分割思想产生的问题 代码来自书籍<java多线程编程实战指南> ...

最新文章

  1. UVA662- Fast Food
  2. 转载通过 Docker 实现传统应用程序的现代化
  3. oracle 查看监听命令_linux下使用Oracle常用命令
  4. 基于JAVA+SpringMVC+MYSQL的在线音乐网站
  5. 类GeometricShapeFactory-JTS几何图形绘制API
  6. js本地图片预览,兼容ie[6-9]、火狐、Chrome17+、Opera11+、Maxthon3
  7. 获取本地ip地址 C#
  8. 短时傅里叶变换STFT(matlab)
  9. 读书笔记:《大型网站技术架构:核心原理与案例分析》(李智慧)(一)
  10. 新浪邮箱服务器设置,免费的新浪邮箱设置outlook怎么设置?
  11. Git初学--创建版本库
  12. 【域渗透提权】CVE-2020-1472 NetLogon 权限提升漏洞
  13. iOS Memory 内存详解
  14. Win32消息集合----方便查阅
  15. 抖音xg03算法逆向杂谈
  16. 计算机音乐技术的发展,计算机网络技术对音乐发展的影响探析
  17. C#中 out 参数的使用
  18. C语言 矩阵的上三角化
  19. 导图解书-经典(04)断舍离
  20. UAAC 访问 Predix UAA 入门

热门文章

  1. 【SpringBoot】浏览器报错Resource interpreted as Stylesheet but transferred with MIME type text/html
  2. 【Java语法】关于使用new和不使用new的数组值传递还是引用传递的问题
  3. C# 静态方法和属性 图书管理
  4. ServletRequestAttributes用法详解——Spring系列学习笔记
  5. L1-007 念数字 (10分) (13行代码AC!!)
  6. 【解题报告+思路拓展】蓝桥杯 拉马车 2017年C组真题
  7. HTTP请求报文和响应报文中的实体数据
  8. TreeMap方法源码
  9. python asyncio回调函数_python回调函数用法实例分析
  10. Python中os库的使用