自定义下载器
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进行实时判断,否在任务停不下来。
下载进度更新时尤其注意,因为进度更新会在点击暂停后仍然进行,通过状态码来判断是否要响应进度更新。
相关文章推荐
- iOS 自定义网页内容下载器
- 实现一个Asp.net自定义Back控件
- 权限控制的自定义SharePoint文档库/列表项菜单
- 自定义MFC打开保存对话框的扩展名
- 利用 IHttpHandler 自定义 HTTP 处理程序
- mockingbird[javascript 自定义界面]
- eclipse 自定义 内容辅助
- 创建自定义的菜单与按钮
- 模块管理常规功能自定义系统的设计与实现(41--终级阶段 综合查询[8]分类汇总)
- HTTP:浏览器请求实例,自定义服务器
- 微信公众号-开发者-自定义菜单
- UITabBarController中自定义UITabBar
- Android 控件之RatingBar评分条(五星)自定义样式
- 自定义Spark Streaming的Receivers
- 带你一起瞧瞧自定义属性以及自定义View的使用
- 控件自定义左键点击消息相应函数的问题(请懂的人来解答一下)
- storm中的log4j到logback的迁移(自定义日志)
- echarts自定义tooltip绘制轨迹
- 二级滑动侧边栏(自定义DrawerLayout)
- Android中通过Typeface设置自定义字体