您的位置:首页 > 其它

自定义下载器

2016-10-05 13:29 78 查看

自定义下载器

1、线程池的使用

在项目中我们使用单例模式,构建了自己的线程池,线程池部分的代码如下:

private ThreadPool() {
//      int corePoolSize, 核心线程数,必定工作的线程数
//      int maximumPoolSize, 最大线程数,核心和对垒不够时可增加的线程个数
//      long keepAliveTime, 线程接待下一个任务前的休息时间
//      TimeUnit unit, 休息时间的单位
//      BlockingQueue<Runnable> workQueue, 当前的队列
//      ThreadFactory threadFactory, 线程制造工厂
//      RejectedExecutionHandler handler 异常处理者
//      availableProcessors 向 Java 虚拟机返回可用处理器的数目。

int cpuCount = Runtime.getRuntime().availableProcessors();
LogUtils.i("cpuCount "+cpuCount);
mCorePoolSize = cpuCount * 2 + 1;
mMaximumPoolSize = mCorePoolSize;
mKeepAliveTime = 0;
mExecutor = new ThreadPoolExecutor(mCorePoolSize, mMaximumPoolSize,
mKeepAliveTime, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
}

/**
* 执行任务,事实上如果非常频繁,线程池就会抛出异常,导致无法继续下载
*/
public void execute(Runnable runnable) {
if (mExecutor != null) {
try{
mExecutor.execute(runnable);
}catch(RejectedExecutionException e){
}
}
}

/**
* 取消线程队列的任务,但没有取消已经执行的任务
*/
public void cancel(Runnable runnable){
if (mExecutor!=null){
mExecutor.getQueue().remove(runnable);
}
}


然而java已经提供给我们一些可以适用于不同场景的线程池,例如:

Executors.newFixedThreadPool(mCorePoolSize);//固定大小的线程池

Executors.newCachedThreadPool();//可缓存的线程池

Executors.newScheduledThreadPool(mCorePoolSize);//支持定时以及周期性任务的线程池

Executors.newSingleThreadExecutor();//单线程的线程池

那么他们底层与我们手写的ThreadPoolExecutor又有什么区别呢?

下面为newFixedThreadPool(nThreads)的源码:

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}


下面为newCachedThreadPool()的源码:

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}


下面为newSingleThreadExecutor()的源码:

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}


下面为newScheduledThreadPool(corePoolSize)的源码:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

// ScheduledThreadPoolExecutor 的继承关系
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService

// ScheduledThreadPoolExecutor 的构造方法
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}


可以看到newFixedThreadPool的实现方式似乎与我们手写的一样,其他的只是与我们的参数略有不同,也就是说他们是应用于不同场景的线程池。我们可以根据需要,来使用这些提供好的线程池。

更多java线程池详情见:http://www.oschina.net/question/565065_86540

2、下载管理器的实现

1.分析下载管理器的特点

下载管理器只能有一个;

下载的任务唯一,不能一个下载任务被重复执行多次(当然同一个资源可以被多次执行);

下载的任务进度和具体状态可以提供给其他调用者查看;

调用者并不唯一,可以有很多个调用者

支持断点续传;

2.观察者模式

对应下载管理器的第一个特点我们可以使用单例模式,这一点很好解决。对应的2、3、4几点看起来也是可以成一种模式,那么在这里我们使用观察者的模式去注册监听下载任务。

先看下图:



该图为对234三点的分析图。

图中下载任务的不同颜色对应了不同的状态。

那么根据这三点,下载管理器对应了观察者模式中的三个要点:

下载任务<—>被观察者 (唯一)

调用者们<—>观察者们 (不唯一)

下载状态<—>观察者喜好的事件 (及时的通知观察者)

有了这三个要点看一下代码:

/**
下载管理器
*/
public class DownLoadManager {
// 下载管理器采用单例模式
private DownLoadManager() {}

private static DownLoadManager sDownLoadManager = null;

public static DownLoadManager getManager() {
if (sDownLoadManager == null) {
synchronized (DownLoadManager.class) {
if (sDownLoadManager == null) {
sDownLoadManager = new DownLoadManager();
}
}
}
return sDownLoadManager;
}

/**
* 存放 下载id 和 观察者对象(采用线程安全的vector)
*/
private Vector<DownloadObserver> mObserverVector;

/**
* 存放 下载id 和 下载任务(采用线程安全的ConcurrentHashMap)
*/
private Map<Long, DownloadRunnable> mDownloadRunnableMap;

/**
* 存放所有的 下载id 和其 对应的下载信息
*/
private Map<Long, DownloadInfo> mDownloadInfoMap;

/**
* 注册观察者,在注册观察者时我们可以根据其提供的信息,先查找是否已经存在对应的
* 下载信息,这样可以及时的将要观察的信息提供给观察者,保证消息的唯一和及时性。
*/
public DownloadInfo registerObserver(AppDetail info, DownloadObserver observer) {...}

/**
* 取消观察者
*/
public void unRegisterObserver(AppDetail info, DownloadObserver observer) {...}

/**
* 提醒状态发生改变,采用循环遍历来提醒保存的观察者们,
* 观察者根据与自己的DownloadInfo进行对比判断是否是自己的事件被更新了。
*/
public void notifyObserverStateChange(DownloadInfo info) {...}

/**
* 提醒进度发生改变,在此处需要参数 DownloadInfo,是因为下载进度时常刷新,
* 那么应该告诉观察者哪个DownloadInfo进度改变了,
* 观察者可以和自己手中的DownloadInfo进行对比,
* 从而避免了进度更新造成其他观察者进行不必要刷新,
* 同时也listview中复用关系造成的错乱。
*/
public void notifyObserverProgressUpdate(DownloadInfo info) {...}

/**
* 注册的观察者接口,观察者通过实现该接口成为可被回调的对象
*/
public interface DownloadObserver {
// 状态改变时的回调
public void onStateChange();
// 进度改变时的回调
public void onProgressUpdate(DownloadInfo info);
}

/**
* 下载事件,当一个资源信息穿进来时,
* 可以对其id进行判断是否已经存在对应的DownloadInfo没有才创建,
* 有的话那么将返回已经存在的DownloadInfo
*/
public synchronized DownloadInfo download(AppDetail detail) {
// 将每个下载信息都存起来
if (!mDownloadInfoMap.containsKey(detail.id)) {
mDownloadInfoMap.put(detail.id, new DownloadInfo(detail));
}
DownloadInfo downloadInfo = mDownloadInfoMap.get(detail.id);
DownloadRunnable downloadRunnable = null;

if (mDownloadRunnableMap == null)
mDownloadRunnableMap = new ConcurrentHashMap<>();

// 保存下载信息和对应的下载线程
if (!mDownloadRunnableMap.containsKey(downloadInfo.id)) {
downloadRunnable = new DownloadRunnable(downloadInfo);
mDownloadRunnableMap.put(downloadInfo.id, downloadRunnable);
}
if (downloadInfo.currentState == DownloadInfo.STATE_ERROR ||
downloadInfo.currentState == DownloadInfo.STATE_WAITING ||
downloadInfo.currentState == DownloadInfo.STATE_START) {
downloadRunnable = mDownloadRunnableMap.get(downloadInfo.id);
ThreadManager.getInstance().execute(downloadRunnable);
}
return downloadInfo;
}

/**
* 下载暂停事件,取消线程池队列中的任务,将DownloadInfo的状态改变为暂停,
* 那么下载进程发现DownloadInfo的状态暂停时也随之退出进程
*/
public void pause(DownloadInfo downloadInfo) {
if (downloadInfo == null)
return;
ThreadManager.getInstance().cancel(mDownloadRunnableMap.get(downloadInfo.id));
downloadInfo.currentState = DownloadInfo.STATE_WAITING;
notifyObserverStateChange();
}

/**
* 下载完成事件,用于统一的安装
*/
public void install(DownloadInfo downloadInfo) {
if (downloadInfo == null)
return;
Intent intent = new Intent("android.intent.action.VIEW");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory("android.intent.category.DEFAULT");
intent.setDataAndType(Uri.fromFile(new File (downloadInfo.savePath)), "application/vnd.android.package- archive");
MyApplication.getContext().startActivity(intent);
}
}


上述管理器的代码中提及的DownloadInfo为下载信息对应的Bean

/**
* 下载信息Bean
*/
public class DownloadInfo {

private final AppDetail detail; // 用于Bean参数的copy
public String downloadUrl; // 下载的链接
public long id; // 下载资源对应的id可以保证,用于鉴别相同的资源
public String name; // 资源的名称
public String packageName; // 资源的包名(这里是apk所以存在包名)
public long size; // 下载后的大小(下载完成后校验,服务器提供)
public long currentPos; // 当前的下载数据大小(下载了多少数据)
public String savePath; // 存放的路径,方便安装

/**
* 下载的五种状态
*/
public static final int STATE_START = 0;
public static final int STATE_ERROR = -1;
public static final int STATE_WAITING = 1;
public static final int STATE_LOADING = 2;
public static final int STATE_SUCCESS = 3;

/**
* 保存当前的下载状态,观察者可以通过收到通着来响应这个状态
*/
public int currentState = STATE_START;

public DownloadInfo(AppDetail detail) {...}

/**
* 获取对应的下载路径
*/
public String setSavePath() {...}

/**
* 计算当前状态
*/
public float getProgress() {
return size != 0 ? (float) currentPos / size : 0;
}
}


3.下载逻辑,断点续传

下载管理器使用观察者模式配置后,我们可以进行下载逻辑的书写了,代码如下:

/**
* 下载线程,因为我们已经封装了线程池所以可以直接将下载逻辑封装到Runnable中
*/
public class DownloadRunnable implements Runnable {

private final DownloadInfo downloadInfo;

public DownloadRunnable(DownloadInfo downloadInfo) {
this.downloadInfo = downloadInfo;
}

@Override
public void run() {
LogUtils.i("线程 " + Thread.currentThread().getId() + " 开始下载了.....");

String downloadUrl = downloadInfo.downloadUrl;
File file = new File(downloadInfo.savePath);

HttpHelper.HttpResult download = null;
InputStream inputStream = null;
FileOutputStream fileOutputStream = null;
/**
* 下载分多种情况,为了支持断点续传
*
* 第一种,已经存在完整的文件
* 第二种,断点续传,存在不完整的文件
*        1)manager有记录信息,如果文件大小和信息相符那么继续下载,否则删掉
*        2)manager没有记录信息,则根据文件的大小下载
* 第三种,不存在文件,那么就直接下载
*/

try {
// 1.已经存在完整的文件
if (file.exists() && downloadInfo.size == file.length()) {
downloadInfo.currentState = DownloadInfo.STATE_SUCCESS;
notifyObserverStateChange();
return;
}

// 如果DownloadInfo初始状态不等0,但是又不等于文件已经下载的大小,说明可能下载错误
if (file.exists() && downloadInfo.currentPos != 0 && downloadInfo.currentPos != file.length()) {
file.delete();
}
// range支持断点续传,(服务器需要支持)
download = HttpHelper.download(HttpHelper.URL + "download?name=" + downloadUrl + "&range=" + file.length());
// 网络正常时
if (download != null) {
inputStream = download.getInputStream();
fileOutputStream = new FileOutputStream(file, true);
int len = 0;
byte data[] = new byte[1024 * 2];
// 状态变为正在下载
downloadInfo.currentState = DownloadInfo.STATE_LOADING;
downloadInfo.currentPos = file.length();
notifyObserverStateChange();

// 下载的循环加上判断,只有在当前状态为下载时才开始下载
while ((len = inputStream.read(data)) != -1 && downloadInfo.currentState == DownloadInfo.STATE_LOADING) {
fileOutputStream.write(data, 0, len);
downloadInfo.currentPos += len;
notifyObserverProgressUpdate(downloadInfo);
fileOutputStream.flush();
}

// 循环结束后对下载的大小进行判断
if (downloadInfo.currentPos == downloadInfo.size) {
// 下载完成
downloadInfo.currentState = DownloadInfo.STATE_SUCCESS;
notifyObserverStateChange();
} else {
// 如果非暂停结束时
if (downloadInfo.currentState != DownloadInfo.STATE_WAITING) {
downloadInfo.currentState = DownloadInfo.STATE_ERROR;
file.delete();
notifyObserverStateChange();
}
}

} else {
// 网络不正常
downloadInfo.currentState = DownloadInfo.STATE_ERROR;
file.delete();
notifyObserverStateChange();
}

} catch (IOException e) {
e.printStackTrace();
} finally {
IOUtils.close(inputStream);
IOUtils.close(fileOutputStream);
}

}
}


3、总结

最后的项目展示:



下载管理器中一定要保证DownloadInfo始终唯一,这样才可以保证每一个观察者观察到对应的对象;

在notify各个观察者时,观察者应当对自己手中的DownloadInfo与notify的进行对比判断,确保自己不会更新错误;

下载时的while循环应当对当前的DownloadInfo进行实时判断,否在任务停不下来。

下载进度更新时尤其注意,因为进度更新会在点击暂停后仍然进行,通过状态码来判断是否要响应进度更新。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息