版权申明】非商业目的注明出处可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/82428733
出自:shusheng007
Retrofit
这个Okhttp
的封装库俨然已经成为Android开发中网络请求的标配,使用其处理Http
请求非常方便,但是涉及到下载功能时,特别是需要实时获得下载进度的场景时就比较棘手了,而下载时需要获得下载进度又属于刚需,所以本文总结一下这部分的知识。
我们知道Retrofit
是Okhttp
的封装库,真正发起网络请求的是OkHttp
,而OkHttp
拥有强大的拦截器机制。如果不了解什么事拦截器机制,就简单看下下面的示例,否则直接跳过:
例如王二狗和李胖子都喜欢牛翠花,王二狗与牛翠花中间隔着李胖子,那王二狗给牛翠花写了个求爱的小纸条,在扔给牛翠花的时候被中间的李胖子给拦截住了,那李胖子就可以修改这个纸条上的内容,甚至把原来的纸条丢掉换个自己的纸条过去,或者干脆不把纸条给牛翠花,等等操作。。。在此例中李胖子就好比是一个拦截器。
其实实时向外报告下载进度就是通过OkHttp
的拦截器实现的,在OkHttp
的下载过程中使用拦截器拦截到下载的内容,算出进度,然后暴露给使用者就可以了。
dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.2.0'
}
public interface DownloadService {
@Streaming
@GET
Call downloadWithDynamicUrl(@Url String fileUrl);
}
这个下载的接口是一个Get
请求,但是需要使用@Streaming
注解标记,如果不使用这个注解,那么Okhttp
就会将下载的内容一次性载入内存中才返回,这在下载稍微大点的文件时候就会发生OOM
。值得注意的是我们使用了@Url
标记了我们的参数,表示可以传入动态Url
部分。
ResponseBody
由于我们会使用拦截器拦截下载的内容,其是一个ResponseBody
类型的数据,我们就是在这里面获取到下载进度的。
public class DownloadResponseBody extends ResponseBody {
private ResponseBody responseBody;
private DownloadListener downloadListener;
private BufferedSource bufferedSource;
private Executor executor;
public DownloadResponseBody(ResponseBody responseBody, Executor executor, DownloadListener downloadListener) {
this.responseBody = responseBody;
this.downloadListener = downloadListener;
this.executor = executor;
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source()));
}
return bufferedSource;
}
private Source source(Source source) {
return new ForwardingSource(source) {
long totalBytesRead = 0L;
@Override
public long read(Buffer sink, long byteCount) throws IOException {
final long bytesRead = super.read(sink, byteCount);
// read() returns the number of bytes read, or -1 if this source is exhausted.
if (null != downloadListener) {
totalBytesRead += bytesRead != -1 ? bytesRead : 0;
Logger.t("DownloadUtil").d("已经下载的:" + totalBytesRead + "共有:" + responseBody.contentLength());
final int progress = (int) (totalBytesRead * 100 / responseBody.contentLength());
if (executor != null) {
executor.execute(() -> downloadListener.onProgress(progress));
} else {
downloadListener.onProgress(progress);
}
}
return bytesRead;
}
};
}
}
主要看private Source source(Source source)
这个方法,其为Okio
提供了数据源(okhttp内部使用Okio来处理IO).我们可以看到在source方法中,有一个可以读取到字节流的函数read()
,我们就是在这个函数中计算下载进度的。此处值得注意的是我们使用了一个Executor
,这个是用来将暴露进度的方法切换到UI
线程的。
拦截器就比较简单了,构造函数中传入了一个用于切换线程的executor
和一个进度监听的listener
,通过设置这个拦截器,下载过程就会在我们自定义的DownloadResponseBody
中拦截到。
public class DownloadInterceptor implements Interceptor {
private DownloadListener listener;
private Executor executor;
public DownloadInterceptor(Executor executor, DownloadListener listener) {
this.listener = listener;
this.executor = executor;
}
@Override
public Response intercept(Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new DownloadResponseBody(originalResponse.body(), executor, listener))
.build();
}
}
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.retryOnConnectionFailure(true)
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.build();
final DownloadService api = new Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.build()
.create(DownloadService.class);
new Thread(() -> {
try {
Response result = api.downloadWithDynamicUrl(rUrl).execute();
File file = writeFile(filePath, result.body().byteStream());
if (listener != null){
executor.execute(()->{
listener.onFinish(file);
});
}
} catch (IOException e) {
if (listener != null){
executor.execute(()->{
listener.onFailed(e.getMessage());
});
}
e.printStackTrace();
}
}) .start();
使用方法非常简单,获取下载工具类实例后调用下载函数即可,出入要下载的文件网络地址以及本地保存地址,传入进度监听,在监听回调中完成自己的业务逻辑。
DownloadUtil.getInstance()
.downloadFile(baseUrl, url, desFilePath, new DownloadListener() {
@Override
public void onFinish(final File file) {
tvFileLocation.setText("下载的文件地址为:" + file.getAbsolutePath());
installAPK(file, MainActivity.this);
}
@Override
public void onProgress(int progress) {
tvProgress.setText(String.format("下载进度为:%s", progress));
}
@Override
public void onFailed(String errMsg) {
}
});
显然与其他的OkHttp封装类库相比,Retrofit在下载这方面显得较为繁琐,但是却给了开发者更大的灵活性。OkHttp
的拦截器机制值得我们每个人学习。
源码下载地址