zoukankan      html  css  js  c++  java
  • Android通用简洁的下载器

    下载逻辑在android开发中可谓很常见,那么封装一个通用简洁的下载器时很有必要的。如果不想给工程引入一个很重的jar包那么可以直接复用下面的代码即可。

    主要对外接口

    构造函数 :     public CommonDownloader(String saveDir, int timeoutMs)

    开始下载接口: public void start(String saveFileName, String url)

    停止下载接口: public void stop()


    结构(十分简单)

    下载主要由一个Handler和一个下载线程组成,Handler统一处理结果,下载线程负责将下载并将结果发送给Handler。

    image

     

    内部实现

    public class CommonDownloader {
        /**patch save dir*/
        private String mSaveDir;
        /**http request timeout*/
        private int mTimeoutMs;
        /**download listener, see {@link OnDownloadListener}*/
        private OnDownloadListener mDownloadListener;
        private Thread mDownloadThread;
        /**download control tag*/
        private boolean isStop;
        /**UI event handler, see {@link DownloadHandler}*/
        private DownloadHandler mDownloadHandler;
    
        /**
         * download event listener
         */
        public interface OnDownloadListener {
            /**start download the callback*/
            void onStarted();
            /**download success the callback*/
            void onSuccess(String file);
            /**download failed the callback*/
            void onFailed(String errorMsg);
        }
    
        public CommonDownloader(String saveDir, int timeoutMs) {
            if (TextUtils.isEmpty(saveDir)) {
                throw new IllegalArgumentException("mSaveDir is empty! please reset.");
            } else {
                File file = new File(saveDir);
                if (!file.exists() || !file.isDirectory()) {
                    if (!file.mkdirs()) {
                        throw new IllegalArgumentException("failed to create file directory. > " + file.getAbsolutePath());
                    }
                }
                this.mSaveDir = saveDir;
            }
            this.mTimeoutMs = timeoutMs;
            mDownloadHandler = new DownloadHandler(this);
        }
    
        /**
         * start download
         * @param patchSaveFileName
         * @param url
         */
        public void start(String patchSaveFileName, String url) {
            mDownloadHandler.sendEmptyMessage(DownloadHandler.STATUS_START);
            if (TextUtils.isEmpty(patchSaveFileName)) {
                Message message = Message.obtain();
                message.what = DownloadHandler.STATUS_FAILED;
                message.obj = "patchSaveFileName is empty! please reset.";
                mDownloadHandler.sendMessage(message);
                return;
            }
            File file = new File(mSaveDir, patchSaveFileName);
            if (file.exists() && file.isFile()) {
                if (!file.delete()) {
                    Message message = Message.obtain();
                    message.what = DownloadHandler.STATUS_FAILED;
                    message.obj = "try deleted this file failed. >" + file.getAbsolutePath();
                    mDownloadHandler.sendMessage(message);
                    return;
                }
            }
            try {
                if (!file.createNewFile()) {
                    Message message = Message.obtain();
                    message.what = DownloadHandler.STATUS_FAILED;
                    message.obj = "failed to create the patch file. >" + file.getAbsolutePath();
                    mDownloadHandler.sendMessage(message);
                    return;
                }
            } catch (IOException e) {
                Message message = Message.obtain();
                message.what = DownloadHandler.STATUS_FAILED;
                message.obj = e.getMessage();
                mDownloadHandler.sendMessage(message);
                Log.e(e);
                return;
            }
    
            stop();
            mDownloadThread = new Thread(new DownloadTask(url, patchSaveFileName, file));
            mDownloadThread.start();
        }
    
        /**
         * stop download
         */
        public void stop() {
            isStop = true;
            if (mDownloadThread != null) {
                try {
                    mDownloadThread.join(3000);
                } catch (InterruptedException e) {
                    Log.w(e.getMessage());
                }
            }
        }
    
        /**
         * set the download listener
         * @param mDownloadListener
         */
        public void setmDownloadListener(OnDownloadListener mDownloadListener) {
            this.mDownloadListener = mDownloadListener;
        }
    
        /**
         * create file output stream
         * @param patchSaveFileName
         * @return
         */
        private OutputStream createOutputStream(String patchSaveFileName) {
            FileOutputStream fileOutputStream = null;
            try {
                fileOutputStream = new FileOutputStream(new File(mSaveDir, patchSaveFileName));
            } catch (FileNotFoundException e) {
                Message message = Message.obtain();
                message.what = DownloadHandler.STATUS_FAILED;
                message.obj = e.getMessage();
                mDownloadHandler.sendMessage(message);
                Log.e(e);
            }
            return fileOutputStream;
        }
    
        /**
         * download task
         */
        private class DownloadTask implements Runnable {
            private String urlAddress;
            private String patchSaveFileName;
            private File downloadFile;
            private DownloadTask(String urlAddress, String patchSaveFileName, File downloadFile) {
                this.urlAddress = urlAddress;
                this.patchSaveFileName = patchSaveFileName;
                this.downloadFile = downloadFile;
            }
    
            @Override
            public void run() {
                isStop = false;
                HttpURLConnection connection = null;
                InputStream inputStream = null;
                OutputStream outputStream = null;
                try {
                    URL url = new URL(urlAddress);
                    connection = (HttpURLConnection)url.openConnection();
                    connection.setConnectTimeout(mTimeoutMs);
                    connection.setReadTimeout(mTimeoutMs);
                    connection.setUseCaches(false);
                    connection.setDoInput(true);
                    connection.setRequestProperty("Accept-Encoding", "identity");
                    connection.setRequestMethod("GET");
                    inputStream = connection.getInputStream();
                    byte[] buffer = new byte[100 * 1024];
                    int length;
                    outputStream = createOutputStream(patchSaveFileName);
                    if(outputStream == null)    return;
                    while (!isStop && (length = inputStream.read(buffer)) != -1) {
                        outputStream.write(buffer, 0, length);
                    }
                    if (!isStop) {
                        Message message = Message.obtain();
                        message.what = DownloadHandler.STATUS_SUCCESS;
                        message.obj = downloadFile.getAbsolutePath();
                        mDownloadHandler.sendMessage(message);
                    } else {
                        Message message = Message.obtain();
                        message.what = DownloadHandler.STATUS_FAILED;
                        message.obj = "the patch download has been canceled!";
                        mDownloadHandler.sendMessage(message);
                    }
                } catch (MalformedURLException e) {
                    Message message = Message.obtain();
                    message.what = DownloadHandler.STATUS_FAILED;
                    message.obj = e.getMessage();
                    mDownloadHandler.sendMessage(message);
                    Log.e(e);
                } catch (IOException e) {
                    Message message = Message.obtain();
                    message.what = DownloadHandler.STATUS_FAILED;
                    message.obj = e.getMessage();
                    mDownloadHandler.sendMessage(message);
                    Log.e(e);
                } catch (Exception ex) {
                    Message message = Message.obtain();
                    message.what = DownloadHandler.STATUS_FAILED;
                    message.obj = ex.getMessage();
                    mDownloadHandler.sendMessage(message);
                    Log.e(ex);
                } finally {
                    if (connection != null) {
                        connection.disconnect();
                    }
                    if (inputStream != null) {
                        try {
                            inputStream.close();
                        } catch (IOException e) {
                            Log.e(e);
                        }
                    }
                    if (outputStream != null) {
                        try {
                            outputStream.close();
                        } catch (IOException e) {
                            Log.e(e);
                        }
                    }
                }
            }
        }
    
        /**
         * download event handler
         */
        private static class DownloadHandler extends Handler {
            private static final int STATUS_START = 0x01;
            private static final int STATUS_SUCCESS = 0x02;
            private static final int STATUS_FAILED = 0x03;
            private WeakReference<CommonDownloader> weakReference;
    
            private DownloadHandler(CommonDownloader patchDownloader) {
                super(Looper.getMainLooper());
                weakReference = new WeakReference<CommonDownloader>(patchDownloader);
            }
    
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                int status = msg.what;
                CommonDownloader patchDownloader = weakReference.get();
                switch (status) {
                    case STATUS_START:
                        if(patchDownloader != null && patchDownloader.mDownloadListener != null) {
                            patchDownloader.mDownloadListener.onStarted();
                        }
                        break;
                    case STATUS_SUCCESS:
                        if(patchDownloader != null && patchDownloader.mDownloadListener != null) {
                            patchDownloader.mDownloadListener.onSuccess((String)msg.obj);
                        }
                        break;
                    case STATUS_FAILED:
                        if (patchDownloader != null && patchDownloader.mDownloadListener != null) {
                            patchDownloader.mDownloadListener.onFailed((String)msg.obj);
                        }
                        break;
                    default:
                        break;
                }
            }
        }
    }


    细节分析:

    1. Hanlder中弱引用的使用:

    当下载器已经被回收时,Listener也不会再收到回调结果

    可以参考这篇关于Activity中Handler防止内存泄漏的方法:  https://blog.csdn.net/u010134087/article/details/53610654

    2. 停止下载的方法:

    首先将标记为  isStop 置为true,这样下载就不再进行(DownloadThread里面写数据时进行了判断),同时调用join方法等待线程停止。 (join方法含义可以参考https://www.cnblogs.com/NeilZhang/p/8781897.html


    断点续传

    断点续传支持从文件上次中断的地方开始传送数据,而并非是从文件开头传送。

    http协议支持: http请求头部可以带上请求文件的开始到结束字节。

    http协议首部有四种:

    • 通用首部字段
    • 请求首部字段(首部“Range”,可以设置需要下载的字节开始和结束字节,格式如下所示)

    Range: bytes=5001-10000

    • 响应首部字段
    • 实体首部字段


    下面贴出支持断点续传的下载器:

    public class DownloadInfo {
        public static final long TOTAL_ERROR = -1;//获取进度失败
        private String url;
        private long total;
        private long progress;
        private String fileName;
    
        public DownloadInfo(String url) {
            this.url = url;
        }
    
        public String getUrl() {
            return url;
        }
    
        public String getFileName() {
            return fileName;
        }
    
        public void setFileName(String fileName) {
            this.fileName = fileName;
        }
    
        public long getTotal() {
            return total;
        }
    
        public void setTotal(long total) {
            this.total = total;
        }
    
        public long getProgress() {
            return progress;
        }
    
        public void setProgress(long progress) {
            this.progress = progress;
        }
    }
    
    
    DownloadInfo


    public  abstract class DownLoadObserver implements Observer<DownloadInfo> {
        protected Disposable d;//可以用于取消注册的监听者
        protected DownloadInfo downloadInfo;
        @Override
        public void onSubscribe(Disposable d) {
            this.d = d;
        }
    
        @Override
        public void onNext(DownloadInfo downloadInfo) {
            this.downloadInfo = downloadInfo;
        }
    
        @Override
        public void onError(Throwable e) {
            e.printStackTrace();
        }
    
    
    }
    
    DownLoadObserver


    public class DownloadManager {
    
        private static final AtomicReference<DownloadManager> INSTANCE = new AtomicReference<>();
        private HashMap<String, Call> downCalls;//用来存放各个下载的请求
        private OkHttpClient mClient;//OKHttpClient;
    
        //获得一个单例类
        public static DownloadManager getInstance() {
            for (; ; ) {
                DownloadManager current = INSTANCE.get();
                if (current != null) {
                    return current;
                }
                current = new DownloadManager();
                if (INSTANCE.compareAndSet(null, current)) {
                    return current;
                }
            }
        }
    
        private DownloadManager() {
            downCalls = new HashMap<>();
            mClient = new OkHttpClient.Builder().build();
        }
    
        /**
         * 开始下载
         *
         * @param url              下载请求的网址
         * @param downLoadObserver 用来回调的接口
         */
        public void download(String url, DownLoadObserver downLoadObserver) {
            Observable.just(url)
                    .filter(s -> !downCalls.containsKey(s))//call的map已经有了,就证明正在下载,则这次不下载
                    .flatMap(s -> Observable.just(createDownInfo(s)))
                    .map(this::getRealFileName)//检测本地文件夹,生成新的文件名
                    .flatMap(downloadInfo -> Observable.create(new DownloadSubscribe(downloadInfo)))//下载
    //                .observeOn(AndroidSchedulers.mainThread())//在主线程回调
                    .subscribeOn(Schedulers.io())//在子线程执行
                    .subscribe(downLoadObserver);//添加观察者
    
        }
    
        public void cancel(String url) {
            Call call = downCalls.get(url);
            if (call != null) {
                call.cancel();//取消
            }
            downCalls.remove(url);
        }
    
        /**
         * 创建DownInfo
         *
         * @param url 请求网址
         * @return DownInfo
         */
        private DownloadInfo createDownInfo(String url) {
            DownloadInfo downloadInfo = new DownloadInfo(url);
            long contentLength = getContentLength(url);//获得文件大小
            downloadInfo.setTotal(contentLength);
            String fileName = url.substring(url.lastIndexOf("/"));
            downloadInfo.setFileName(fileName);
            return downloadInfo;
        }
    
        private DownloadInfo getRealFileName(DownloadInfo downloadInfo) {
            String fileName = downloadInfo.getFileName();
            long downloadLength = 0, contentLength = downloadInfo.getTotal();
            File file = new File(MyApp.sContext.getFilesDir(), fileName);
            if (file.exists()) {
                //找到了文件,代表已经下载过,则获取其长度
                downloadLength = file.length();
            }
            //之前下载过,需要重新来一个文件
            int i = 1;
            while (downloadLength >= contentLength) {
                int dotIndex = fileName.lastIndexOf(".");
                String fileNameOther;
                if (dotIndex == -1) {
                    fileNameOther = fileName + "(" + i + ")";
                } else {
                    fileNameOther = fileName.substring(0, dotIndex)
                            + "(" + i + ")" + fileName.substring(dotIndex);
                }
                File newFile = new File(MyApp.sContext.getFilesDir(), fileNameOther);
                file = newFile;
                downloadLength = newFile.length();
                i++;
            }
            //设置改变过的文件名/大小
            downloadInfo.setProgress(downloadLength);
            downloadInfo.setFileName(file.getName());
            return downloadInfo;
        }
    
        private class DownloadSubscribe implements ObservableOnSubscribe<DownloadInfo> {
            private DownloadInfo downloadInfo;
    
            public DownloadSubscribe(DownloadInfo downloadInfo) {
                this.downloadInfo = downloadInfo;
            }
    
            @Override
            public void subscribe(ObservableEmitter<DownloadInfo> e) throws Exception {
                String url = downloadInfo.getUrl();
                long downloadLength = downloadInfo.getProgress();//已经下载好的长度
                long contentLength = downloadInfo.getTotal();//文件的总长度
                //初始进度信息
                e.onNext(downloadInfo);
    
                Request request = new Request.Builder()
                        //确定下载的范围,添加此头,则服务器就可以跳过已经下载好的部分
                        .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength)
                        .url(url)
                        .build();
                Call call = mClient.newCall(request);
                downCalls.put(url, call);//把这个添加到call里,方便取消
                Response response = call.execute();
    
                File file = new File(MyApp.sContext.getFilesDir(), downloadInfo.getFileName());
                InputStream is = null;
                FileOutputStream fileOutputStream = null;
                try {
                    is = response.body().byteStream();
                    fileOutputStream = new FileOutputStream(file, true);
                    byte[] buffer = new byte[2048];//缓冲数组2kB
                    int len;
                    while ((len = is.read(buffer)) != -1) {
                        fileOutputStream.write(buffer, 0, len);
                        downloadLength += len;
                        downloadInfo.setProgress(downloadLength);
                        e.onNext(downloadInfo);
                    }
                    fileOutputStream.flush();
                    downCalls.remove(url);
                } finally {
                    //关闭IO流
                    IOUtil.closeAll(is, fileOutputStream);
    
                }
                e.onComplete();//完成
            }
        }
    
        /**
         * 获取下载长度
         *
         * @param downloadUrl
         * @return
         */
        private long getContentLength(String downloadUrl) {
            Request request = new Request.Builder()
                    .url(downloadUrl)
                    .build();
            try {
                Response response = mClient.newCall(request).execute();
                if (response != null && response.isSuccessful()) {
                    long contentLength = response.body().contentLength();
    //                response.close();
                    return contentLength == 0 ? DownloadInfo.TOTAL_ERROR : contentLength;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return DownloadInfo.TOTAL_ERROR;
        }
    
    
    }
    
    DownloadManager

    主要流程如下:(具体过程查看代码)

    1

    参考:

    https://www.jb51.net/article/104456.htm

  • 相关阅读:
    java&nbsp;split
    百度知道
    2014年10月27日
    2014年10月27日
    mybatis批量update,返回行数为-1
    mybatis批量插入:oracle和mysql的区别
    oracle数据库,mybatis批量insert,缺失values字段
    java后台接收json数据,报错com.alibaba.fastjson.JSONObject cannot be cast to xxx
    C++——运算符重载(上)
    C++——友元
  • 原文地址:https://www.cnblogs.com/NeilZhang/p/9600859.html
Copyright © 2011-2022 走看看