下载逻辑在android开发中可谓很常见,那么封装一个通用简洁的下载器时很有必要的。如果不想给工程引入一个很重的jar包那么可以直接复用下面的代码即可。
主要对外接口
构造函数 : public CommonDownloader(String saveDir, int timeoutMs)
开始下载接口: public void start(String saveFileName, String url)
停止下载接口: public void stop()
结构(十分简单)
下载主要由一个Handler和一个下载线程组成,Handler统一处理结果,下载线程负责将下载并将结果发送给Handler。
内部实现
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 WeakReferenceweakReference; private DownloadHandler(CommonDownloader patchDownloader) { super(Looper.getMainLooper()); weakReference = new WeakReference (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防止内存泄漏的方法:
2. 停止下载的方法:
首先将标记为 isStop 置为true,这样下载就不再进行(DownloadThread里面写数据时进行了判断),同时调用join方法等待线程停止。 (join方法含义可以参考:)
断点续传
断点续传支持从文件上次中断的地方开始传送数据,而并非是从文件开头传送。
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; }}
public abstract class DownLoadObserver implements Observer{ 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(); }}
public class DownloadManager { private static final AtomicReferenceINSTANCE = new AtomicReference<>(); private HashMap 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 { private DownloadInfo downloadInfo; public DownloadSubscribe(DownloadInfo downloadInfo) { this.downloadInfo = downloadInfo; } @Override public void subscribe(ObservableEmitter 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; }}
主要流程如下:(具体过程查看代码)
参考: