首先先感谢丰神,本文源于他的这篇微博http://blog.csdn.net/cfy137000/article/details/54838608,思路很棒,然后自己跟着代码撸了一遍,然后为了加深理解就上传到博客上来。
首先说下主要都到了什么开发技术吧,网络请求是使用okhttp,然后涉及多线程的部分是使用rxjava,之前自己只是简单的看了一下rxjava,然后实际运用了下,感觉熟悉了很多;然后还涉及到了lambda表达式以及文件的流的读写,开发要求的jdk的版本的要求是在1.8以上,不然没办法支持lambda表达式。
首先是配置gradle,导入okhttp以及rxjava,//OKHttp
compile 'com.squareup.okhttp3:okhttp:3.6.0'
//RxJava和RxAndroid 用来做线程切换的
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.0.1'
然后,//开启Java1.8 能够使用lambda表达式
compileOptions{
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
//为了开启Java8
jackOptions{
enabled true;
}
然后大概思路捋了下:
1,使用单例模式初始化okhttpclient,同时使用hashmap来存放请求;
2.使用rxjava进行下载的一些操作;
3.点击暂停的时候,通过hashmap移除请求;
4.断点续传主要是包括:移除请求,判断本地是否有下载的文件,比较本地文件和下载文件的大小,重新开始请求;
5.需要提到的一点是我们通过添加请求头来实现接着之前的进度继续下载。
为了开发方便新建了一个包括下载信息的实体类(downloadInfo),一个数据接受(downloadObserver),
还有一个关闭流的工具类(IOUtil),新建了一个application(myApp).
1.跟着思路开始理解代码,首先来说的是单例模式的实现,这里涉及到了一个之前没有接触的新东西(AtomicReference),百度了一下,介绍说这是java的原子性引用,在多线程的情况下更新对象可以保持一致性;然后这里用来实现单例
//获得一个单例类
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();
}
其中的comareAndSet方法的意思是如果此方法的调用者和参数一相等,那么就将参数二的值赋值给调用者;然后构造方法私有化。
2.
然后就是下载文件的方法,先看代码:
/**
* 开始下载
*
* @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);//添加观察者
}
参数一是我们下载文件的url,参数二就是我们的数据接收源,里面的代码很简单,就是用来更新ui:
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();
}
}
然后说下我们的downloadfile方法,里面的(s->)即lambda表达式的形式,这里的s代表的是url;第一个是过滤器(判断当前的网络请求下载状态),关于flatmap操作符的意思可以参考这篇博客http://blog.csdn.net/johnny901114/article/details/51532776;然后map操作检测本地文件夹,如果已经存在的话,就更新文件名(在原来文件名的名字后加上(1)方法介绍详见后面方法介绍);然后再次执行flatmap操作,接着就是rxjava的一系列操作,关于rxjava的操作,不是太熟练,所以这块现在只是跟着
写了。
3.取消网络请求的操作,取消网络请求的操作即通过每条url来操作hashmap中的call,执行call的cancel操作。
public void cancel(String url) {
Call call = downCalls.get(url);
if (call != null) {
call.cancel();//取消
}
downCalls.remove(url);
}
4.根据网址创建downloadInfo;
/**
* 创建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;
}
5.
获取下载文件的大小,进行网络请求,文件的大小在请求头中可以得到;
/**
* 获取下载长度
*
* @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;
}
6.获取本地下载文件的大小。通过文件名来判断是否存在同名下载文件,如果有,将大小赋值给实体类,然后比较已存在文件的大小和下载文件的大小;如果等于或者大于的情况,则添加小标,区别于之前下载过的文件。代码如下:
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;
}
7.创建DownloadSubscribe,在subscribe中进行断点续传,主要就是通过设置
请求头,将文件的读写范围跳跃至已下载文件大小处,然后通过流的形式,将文件衔接上去:
private class DownloadSubscribe implements ObservableOnSubscribe<DownloadInfo> {
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();
新建请求,然后添加请求头,同时,将本次请求也加入到hashmap中,方便取消;通过 call.execute()拿到我们的返回结果。
然后,文件的io流将文件写入到已下载文件中;
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();//完成
}
至此,就完成了多个文件的同时下载以及暂停和断点续传,如果要实现取消的话,就是将我们的进度条清零,然后删除本次的已下载文件。
附上源代码:http://download.csdn.net/detail/cfy137000/9746583,源码的下载地址是原博主。