Android通用简洁的下载器
下载逻辑在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 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
主要流程如下:(具体过程查看代码)

参考:
https://www.jb51.net/article/104456.htm
Android通用简洁的下载器的更多相关文章
- Android开发多线程断点续传下载器
使用多线程断点续传下载器在下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度,在下载过程中记录每个线程已拷贝数据的数量,如果下载中断,比如无信号断线.电量不足等情况下,这就需要使用到断点 ...
- 实现android支持多线程断点续传下载器功能
多线程断点下载流程图: 多线程断点续传下载原理介绍: 在下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度手机端下载数据时难免会出现无信号断线.电量不足等情况,所以需要断点续传功能根据下 ...
- Android开源库--Universal Image Loader通用图片加载器
如果说我比别人看得更远些,那是因为我站在了巨人的肩上. github地址:https://github.com/nostra13/Android-Universal-Image-Loader 介绍 ...
- 我的Android进阶之旅------>Android基于HTTP协议的多线程断点下载器的实现
一.首先写这篇文章之前,要了解实现该Android多线程断点下载器的几个知识点 1.多线程下载的原理,如下图所示 注意:由于Android移动设备和PC机的处理器还是不能相比,所以开辟的子线程建议不要 ...
- 星之小说下载器Android版
原本是想在酷安上架的,然而审核不通过..只能通过网页方式宣传了 一款使用Jsoup开源库网络爬虫的APP,将在线阅读的小说解析,把小说全本下载为txt文件 由于使用爬虫技术,所以下载的速度不是很理想, ...
- 60.Android通用流行框架大全
转载:https://segmentfault.com/a/1190000005073746 Android通用流行框架大全 1. 缓存 名称 描述 DiskLruCache Java实现基于LRU的 ...
- 15类Android通用流行框架
15类Android通用流行框架 Android流行框架 缓存 DiskLruCache Java实现基于LRU的磁盘缓存 图片加载 Android Universal Image Loader 一个 ...
- 我的Android进阶之旅------>Android通用流行框架大全
Android通用流行框架大全 缓存 图片加载 图片处理 网络请求 网络解析 数据库 依赖注入 图表 后台处理 事件总线 响应式编程 Log框架 测试框架 调试框架 性能优化 本文转载于lavor的博 ...
- (转载)15 个 Android 通用流行框架大全
15 个 Android 通用流行框架大全 时间:2017-03-20 11:36来源:未知 作者:admin 点击: 2089 次 15 个 Android 通用流行框架大全 1. 缓存 Dis ...
随机推荐
- HDU 1465 2045 已知结果往前推
1465 不容易系列之一 Time Limit: 1000 MS Memory Limit: 32768 KB 64-bit integer IO format: %I64d , %I64u Java ...
- Android-Java-等待唤醒机制原理
儿时的游戏:(等待 与 唤醒) 有一群小朋友一起玩一个游戏,这个游戏可能大家都玩过,大家一起划拳,划拳输得最惨的那个小朋友去抓人(这个小朋友取名为 CPU),被抓的很多人取名为线程,有很多线程,如果其 ...
- 两台linux之间建立信任关系,实现免密码ssh远程登录或scp数据上传
两台linux之间建立信任关系,实现免密码远程登录或数据上传 1.执行ssh-keygen命令,生成建立安全信任关系的证书: linux1上:执行命令 ssh-keygen -t rsa 在程序提 ...
- 【转】dlgdata.cpp line 40 断言失败
原文网址:http://blog.csdn.net/onlyou930/article/details/6384075 在VS2010 运行一个C++ 程序,出现下图错误: 一看到这个,我头都大了.关 ...
- c#中的几种Dialog
1.OpenFileDialog private void FileOpen_Click(object sender, EventArgs e) { OpenFileDialog openFile = ...
- JS中图片飞飞效果
当鼠标在界面上移动的时候,后面有一连串的图片跟随者一起飘动,效果如下: 实现的基本思想:准备五个img标签,为了方便控制都放在一个div里面,设置div的定位方式为 fixed,设置成这中定位方式主要 ...
- 浏览器环境下JavaScript脚本加载与执行探析之动态脚本与Ajax脚本注入
在<浏览器环境下JavaScript脚本加载与执行探析之defer与async特性>中,我们研究了延迟脚本(defer)和异步脚本(async)的执行时机.浏览器支持情况.浏览器bug以及 ...
- 一个docker容器中运行多个服务还是弄一堆docker容器运行?
不建议直接在单个 Docker 容器中运行多个程序. 以 2017年 10 月18 日 Docker 官方支持 Kubernetes 为分水岭计算,Kubernetes 赢得容器编排之战的最终胜利已经 ...
- Linux系统用户与属组管理(3)
好了,终于要到了管理 Linux 账号的时刻了,对于 Linux 有一定的熟悉度之后,再来就是要管理连上 Linux 的账号问题了,这个账号的问题可大可小,大到可以限制他使用 Linux 主机的各项资 ...
- Linux删除目录下的文件的10种方法
看到了一遍文章,便突发奇想的想起Linux中删除目录下的所有文件的方法:整理了几个,如有不足,还望读者不吝赐教! 删除当前目录下的文件 1.rm -f * #最经典的方法,删除当前目录下的所有类型的文 ...
