自定义下载器

自定义下载器

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.分析下载管理器的特点

  1. 下载管理器只能有一个;
  2. 下载的任务唯一,不能一个下载任务被重复执行多次(当然同一个资源可以被多次执行);
  3. 下载的任务进度和具体状态可以提供给其他调用者查看;
  4. 调用者并不唯一,可以有很多个调用者
  5. 支持断点续传;

2.观察者模式

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

先看下图:

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

图中下载任务的不同颜色对应了不同的状态。
那么根据这三点,下载管理器对应了观察者模式中的三个要点:

  1. 下载任务<—>被观察者 (唯一)
  2. 调用者们<—>观察者们 (不唯一)
  3. 下载状态<—>观察者喜好的事件 (及时的通知观察者)

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


    /**
        下载管理器
    */
    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、总结


最后的项目展示:

  1. 下载管理器中一定要保证DownloadInfo始终唯一,这样才可以保证每一个观察者观察到对应的对象;
  2. 在notify各个观察者时,观察者应当对自己手中的DownloadInfo与notify的进行对比判断,确保自己不会更新错误;
  3. 下载时的while循环应当对当前的DownloadInfo进行实时判断,否在任务停不下来。
  4. 下载进度更新时尤其注意,因为进度更新会在点击暂停后仍然进行,通过状态码来判断是否要响应进度更新。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值