详解Android使用OKHttp3实现下载(断点续传、显示进度)
okhttp3是如今非常流行的android网络请求框架,那么如何利用android实现断点续传呢,今天写了个demo尝试了一下,感觉还是有点意思
准备阶段
我们会用到okhttp3来做网络请求,使用rxjava来实现线程的切换,并且开启java8来启用lambda表达式,毕竟rxjava实现线程切换非常方便,而且数据流的形式也非常舒服,同时lambda和rxjava配合食用味道更佳
打开我们的app module下的build.gradle,代码如下
apply plugin: 'com.android.application' android { compilesdkversion 24 buildtoolsversion "24.0.3" defaultconfig { applicationid "com.lanou3g.downdemo" minsdkversion 15 targetsdkversion 24 versioncode 1 versionname "1.0" testinstrumentationrunner "android.support.test.runner.androidjunitrunner" //为了开启java8 jackoptions{ enabled true; } } buildtypes { release { minifyenabled false proguardfiles getdefaultproguardfile('proguard-android.txt'), 'proguard-rules.pro' } } //开启java1.8 能够使用lambda表达式 compileoptions{ sourcecompatibility javaversion.version_1_8 targetcompatibility javaversion.version_1_8 } } dependencies { compile filetree(dir: 'libs', include: ['*.jar']) androidtestcompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:24.1.1' testcompile 'junit:junit:4.12' //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' }
okhttp和rxjava,rxandroid使用的都是最新的版本,并且配置开启了java8
布局文件
接着开始书写布局文件
<?xml version="1.0" encoding="utf-8"?> <linearlayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingbottom="@dimen/activity_vertical_margin" android:paddingleft="@dimen/activity_horizontal_margin" android:paddingright="@dimen/activity_horizontal_margin" android:paddingtop="@dimen/activity_vertical_margin" android:orientation="vertical" tools:context="com.lanou3g.downdemo.mainactivity"> <linearlayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <progressbar android:id="@+id/main_progress1" android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent" style="@style/widget.appcompat.progressbar.horizontal" /> <button android:id="@+id/main_btn_down1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载1"/> <button android:id="@+id/main_btn_cancel1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消1"/> </linearlayout> <linearlayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <progressbar android:id="@+id/main_progress2" android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent" style="@style/widget.appcompat.progressbar.horizontal" /> <button android:id="@+id/main_btn_down2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载2"/> <button android:id="@+id/main_btn_cancel2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消2"/> </linearlayout> <linearlayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <progressbar android:id="@+id/main_progress3" android:layout_width="0dp" android:layout_weight="1" android:layout_height="match_parent" style="@style/widget.appcompat.progressbar.horizontal" /> <button android:id="@+id/main_btn_down3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载3"/> <button android:id="@+id/main_btn_cancel3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消3"/> </linearlayout> </linearlayout>
大概是这个样子的
3个progressbar就是为了显示进度的,每个progressbar对应2个button,一个是开始下载,一个是暂停(取消)下载,这里需要说明的是,对下载来说暂停和取消没有什么区别,除非当取消的时候,会顺带把临时文件都删除了,在本例里是不区分他俩的.
application
我们这里需要用到一些文件路径,有一个全局context会比较方便, 而application也是context的子类,使用它的是最方便的,所以我们写一个类来继承application
package com.lanou3g.downdemo; import android.app.application; import android.content.context; /** * created by 陈丰尧 on 2017/2/2. */ public class myapp extends application { public static context scontext;//全局的context对象 @override public void oncreate() { super.oncreate(); scontext = this; } }
可以看到,我们就是要获得一个全局的context对象的
我们在androidmanifest中注册一下我们的application,同时再把我们所需要的权限给上
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.lanou3g.downdemo"> <!--网络权限--> <uses-permission android:name="android.permission.internet"/> <application android:allowbackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsrtl="true" android:name=".myapp" android:theme="@style/apptheme"> <activity android:name=".mainactivity"> <intent-filter> <action android:name="android.intent.action.main" /> <category android:name="android.intent.category.launcher" /> </intent-filter> </activity> </application> </manifest>
我们只需要一个网络权限,在application标签下,添加name属性,来指向我们的application
downloadmanager
接下来是核心代码了,就是我们的downloadmanager,先上代码
package com.lanou3g.downdemo; import java.io.file; import java.io.fileoutputstream; import java.io.ioexception; import java.io.inputstream; import java.util.hashmap; import java.util.concurrent.atomic.atomicreference; import io.reactivex.observable; import io.reactivex.observableemitter; import io.reactivex.observableonsubscribe; import io.reactivex.android.schedulers.androidschedulers; import io.reactivex.schedulers.schedulers; import okhttp3.call; import okhttp3.okhttpclient; import okhttp3.request; import okhttp3.response; /** * created by 陈丰尧 on 2017/2/2. */ 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; } }
代码稍微有点长,关键部位我都加了注释了,我们挑关键地方看看
首先我们这个类是单例类,我们下载只需要一个okhttpclient就足够了,所以我们让构造方法私有,而单例类的获取实例方法就是这个getinstance();当然大家用别的方式实现单例也可以的,然后我们在构造方法里初始化我们的httpclient,并且初始化一个hashmap,用来放所有的网络请求的,这样当我们取消下载的时候,就可以找到url对应的网络请求然后把它取消掉就可以了
接下来就是核心的download方法了,首先是参数,第一个参数url不用多说,就是请求的网址,第二个参数是一个observer对象,因为我们使用的是rxjava,并且没有特别多复杂的方法,所以就没单独写接口,而是谢了一个observer对象来作为回调,接下来是downloadobserver的代码
package com.lanou3g.downdemo; import io.reactivex.observer; import io.reactivex.disposables.disposable; /** * created by 陈丰尧 on 2017/2/2. */ 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(); } }
在rxjava2中 这个observer有点变化,当注册观察者的时候,会调用onsubscribe方法,而该方法参数就是用来取消注册的,这样的改动可以更灵活的有监听者来取消监听了,我们的进度信息会一直的传送的onnext方法里,这里将下载所需要的内容封了一个类叫downloadinfo
package com.lanou3g.downdemo; /** * created by 陈丰尧 on 2017/2/2. * 下载信息 */ 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; } }
这个类就是一些基本信息,total就是需要下载的文件的总大小,而progress就是当前下载的进度了,这样就可以计算出下载的进度信息了
接着看downloadmanager的download方法,首先通过url生成一个observable对象,然后通过filter操作符过滤一下,如果当前正在下载这个url对应的内容,那么就不下载它,
接下来调用createdowninfo重新生成observable对象,这里应该用map也是可以的,createdowninfo这个方法里会调用getcontentlength来获取服务器上的文件大小,可以看一下这个方法的代码,
/** * 获取下载长度 * * @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; }
可以看到,其实就是在通过ok进行了一次网络请求,并且从返回的头信息里拿到文件的大小信息,一般这个信息都是可以拿到的,除非下载网址不是直接指向资源文件的,而是自己手写的servlet,那就得跟后台人员沟通好了.注意,这次网络请求并没有真正的去下载文件,而是请求个大小就结束了,具体原因会在后面真正请求数据的时候解释
接着download方法
获取完文件大小后,就可以去硬盘里找文件了,这里调用了getrealfilename方法
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; }
这个方法就是看本地是否有已经下载过的文件,如果有,再判断一次本地文件的大小和服务器上数据的大小,如果是一样的,证明之前下载全了,就再成一个带(1)这样的文件,而如果本地文件大小比服务器上的小的话,那么证明之前下载了一半断掉了,那么就把进度信息保存上,并把文件名也存上,看完了再回到download方法
之后就开始真正的网络请求了,这里写了一个内部类来实现observableonsubscribe接口,这个接口也是rxjava2的,东西和之前一样,好像只改了名字,看一下代码
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();//完成 } }
主要看subscribe方法
首先拿到url,当前进度信息和文件的总大小,然后开始网络请求,注意这次网络请求的时候需要添加一条头信息
.addheader("range", "bytes=" + downloadlength + "-" + contentlength)
这条头信息的意思是下载的范围是多少,downloadlength是从哪开始下载,contentlength是下载到哪,当要断点续传的话必须添加这个头,让输入流跳过多少字节的形式是不行的,所以我们要想能成功的添加这条信息那么就必须对这个url请求2次,一次拿到总长度,来方便判断本地是否有下载一半的数据,第二次才开始真正的读流进行网络请求,我还想了一种思路,当文件没有下载完成的时候添加一个自定义的后缀,当下载完成再把这个后缀取消了,应该就不需要请求两次了.
接下来就是正常的网络请求,向本地写文件了,而写文件到本地这,网上大多用的是randomaccessfile这个类,但是如果不涉及到多个部分拼接的话是没必要的,直接使用输出流就好了,在输出流的构造方法上添加一个true的参数,代表是在原文件的后面添加数据即可,而在循环里,不断的调用onnext方法发送进度信息,当写完了之后别忘了关流,同时把call对象从hashmap中移除了.这里写了一个ioutil来关流
package com.lanou3g.downdemo; import java.io.closeable; import java.io.ioexception; /** * created by 陈丰尧 on 2017/2/2. */ public class ioutil { public static void closeall(closeable... closeables){ if(closeables == null){ return; } for (closeable closeable : closeables) { if(closeable!=null){ try { closeable.close(); } catch (ioexception e) { e.printstacktrace(); } } } } }
其实就是挨一个判断是否为空,并关闭罢了
这样download方法就完成了,剩下的就是切换线程,注册观察者了
mainactivity
最后是aty的代码
package com.lanou3g.downdemo; import android.net.uri; import android.support.annotation.idres; import android.support.v7.app.appcompatactivity; import android.os.bundle; import android.view.view; import android.widget.button; import android.widget.progressbar; import android.widget.toast; public class mainactivity extends appcompatactivity implements view.onclicklistener { private button downloadbtn1, downloadbtn2, downloadbtn3; private button cancelbtn1, cancelbtn2, cancelbtn3; private progressbar progress1, progress2, progress3; private string url1 = "http://192.168.31.169:8080/out/dream.flac"; private string url2 = "http://192.168.31.169:8080/out/music.mp3"; private string url3 = "http://192.168.31.169:8080/out/code.zip"; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); downloadbtn1 = bindview(r.id.main_btn_down1); downloadbtn2 = bindview(r.id.main_btn_down2); downloadbtn3 = bindview(r.id.main_btn_down3); cancelbtn1 = bindview(r.id.main_btn_cancel1); cancelbtn2 = bindview(r.id.main_btn_cancel2); cancelbtn3 = bindview(r.id.main_btn_cancel3); progress1 = bindview(r.id.main_progress1); progress2 = bindview(r.id.main_progress2); progress3 = bindview(r.id.main_progress3); downloadbtn1.setonclicklistener(this); downloadbtn2.setonclicklistener(this); downloadbtn3.setonclicklistener(this); cancelbtn1.setonclicklistener(this); cancelbtn2.setonclicklistener(this); cancelbtn3.setonclicklistener(this); } @override public void onclick(view v) { switch (v.getid()) { case r.id.main_btn_down1: downloadmanager.getinstance().download(url1, new downloadobserver() { @override public void onnext(downloadinfo value) { super.onnext(value); progress1.setmax((int) value.gettotal()); progress1.setprogress((int) value.getprogress()); } @override public void oncomplete() { if(downloadinfo != null){ toast.maketext(mainactivity.this, downloadinfo.getfilename() + "-downloadcomplete", toast.length_short).show(); } } }); break; case r.id.main_btn_down2: downloadmanager.getinstance().download(url2, new downloadobserver() { @override public void onnext(downloadinfo value) { super.onnext(value); progress2.setmax((int) value.gettotal()); progress2.setprogress((int) value.getprogress()); } @override public void oncomplete() { if(downloadinfo != null){ toast.maketext(mainactivity.this, downloadinfo.getfilename() + uri.encode("下载完成"), toast.length_short).show(); } } }); break; case r.id.main_btn_down3: downloadmanager.getinstance().download(url3, new downloadobserver() { @override public void onnext(downloadinfo value) { super.onnext(value); progress3.setmax((int) value.gettotal()); progress3.setprogress((int) value.getprogress()); } @override public void oncomplete() { if(downloadinfo != null){ toast.maketext(mainactivity.this, downloadinfo.getfilename() + "下载完成", toast.length_short).show(); } } }); break; case r.id.main_btn_cancel1: downloadmanager.getinstance().cancel(url1); break; case r.id.main_btn_cancel2: downloadmanager.getinstance().cancel(url2); break; case r.id.main_btn_cancel3: downloadmanager.getinstance().cancel(url3); break; } } private <t extends view> t bindview(@idres int id){ view viewbyid = findviewbyid(id); return (t) viewbyid; } }
activity里没什么了,就是注册监听,开始下载,取消下载这些了,下面我们来看看效果吧
运行效果
可以看到 多个下载,断点续传什么的都已经成功了,最后我的文件网址是我自己的局域网,大家写的时候别忘了换了..
代码地址:demo
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
详解Android使用OKHttp3实现下载(断点续传、显示进度)
-
Android多线程+单线程+断点续传+进度条显示下载功能
-
Android中ProgressBar的使用-通过Handler与Message实现进度条显示
-
Android 使用Retrofit下载文件并实现进度监听
-
详解Android使用OKHttp3实现下载(断点续传、显示进度)
-
Android使用AsyncTask下载图片并显示进度条功能
-
android使用OkHttp实现下载的进度监听和断点续传
-
Android使用AsyncTask下载图片并显示进度条功能
-
android使用OkHttp实现下载的进度监听和断点续传
-
Android多线程+单线程+断点续传+进度条显示下载功能