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

原文地址:https://www.cnblogs.com/NeilZhang/p/9600859.html