前言
公司的两款App项目里都有下载文件的需求,以前都是用xUtils这个框架。很早开始,我们的网络层已经用的是Retrofit+RxJava+OKHttp,唯独下载请求还是要用xUtils,如此一来就显得有点多余了,毕竟xutils这个框架还是比较大的。于是考虑自己来封装下载文件的操作,就用Retrofit+RxJava+OKHttp,当然“进度回调”和“断点续传”都是必须支持的。
先上个效果动图:
image二. 思路
- 调用者需要传入一个接口(Callback),下载时实时把进度回调。
- 自定义ReponseBody和Interceptor,以实时获取下载的进度。
- 请求头加上“Range”,写文件到磁盘时用RandomAccessFile,以实现断点续传。
- 下载时把url的md5值做为临时文件名,下载完成后再改成调用者传入的文件名。
三. 实现
1. 先写Retrofit下载用的Service
public interface BaseApi {
/**
* 下载文件
*/
@Streaming
@GET
Observable<ResponseBody> downloadFile(@Header("Range") String range, @Url String url);
}
注意到请求里有个请求头“Range”,这个是为了实现断点续传。简单说就是可以从服务器下载文件的指定“部分”。
Range的传值规则如下:bytes=startPos-endPos,其中endPos是可以省略的,即结束位置为文件的末尾。比如从36985byte开始断点下载,则传值为:"bytes=36985-"。
2. 自定义DownloadResponBody继承ResponBody
public class DownloadResponseBody extends ResponseBody {
private final ResponseBody responseBody;
private BufferedSource bufferedSource;
public DownloadResponseBody(ResponseBody responseBody, DownloadListener listener) {
this.responseBody = responseBody;
if (null != listener) {
listener.onStart(responseBody);
}
}
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(getSource(responseBody.source()));
}
return bufferedSource;
}
private Source getSource(Source source) {
return new ForwardingSource(source) {
long downloadBytes = 0L;
@Override
public long read(@NonNull Buffer buffer, long byteCount) throws IOException {
long singleRead = super.read(buffer, byteCount);
if (-1 != singleRead) {
downloadBytes += singleRead;
}
return singleRead;
}
};
}
}
3. 自定义拦截器把响应转换成DownloadResponseBody
public class DownloadInterceptor implements Interceptor {
private DownloadListener listener;
public DownloadInterceptor(DownloadListener listener) {
this.listener = listener;
}
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new DownloadResponseBody(originalResponse.body(), listener))
.build();
}
}
4. Retrofit初始化并传入DownloadInterceptor
其中baseUrl这个值没影响,用自己的服务器或自定义即可。
if (null == mBuilder) {
mBuilder = new OkHttpClient.Builder()
.connectTimeout(TIME_OUT_SECNOD, TimeUnit.SECONDS)
.readTimeout(TIME_OUT_SECNOD, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT_SECNOD, TimeUnit.SECONDS)
.addInterceptor(headerInterceptor)
.addInterceptor(logInterceptor)
.addInterceptor(new DownloadInterceptor(downloadListener));
}
return new Retrofit.Builder()
.baseUrl("http://imtt.dd.qq.com/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(mBuilder.build())
.build();
5. 向上层提供下载请求的方法
/**
* 下载文件请求
*/
public static void downloadFile(String url, long startPos, DownloadListener downloadListener, Observer<ResponseBody> observer) {
getDownloadRetrofit(downloadListener).create(BaseApi.class).downloadFile("bytes=" + startPos + "-", url)
.subscribeOn(Schedulers.io())
.unsubscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(observer);
}
6. 封装入口类,供外部调用
public class RxNet {
public static boolean enableLog = true;
public static void download(final String url, final String filePath, final DownloadCallback callback) {
if (TextUtils.isEmpty(url) || TextUtils.isEmpty(filePath)) {
if (null != callback) {
callback.onError("url or path empty");
}
return;
}
File oldFile = new File(filePath);
if (oldFile.exists()) {
if (null != callback) {
callback.onFinish(oldFile);
}
return;
}
DownloadListener listener = new DownloadListener() {
@Override
public void onStart(ResponseBody responseBody) {
saveFile(responseBody, url, filePath, callback);
}
};
RetrofitFactory.downloadFile(url, CommonUtils.getTempFile(url, filePath).length(), listener, new Observer<ResponseBody>() {
@Override
public void onSubscribe(Disposable d) {
if (null != callback) {
callback.onStart(d);
}
}
@Override
public void onNext(final ResponseBody responseBody) {
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
LogUtils.i("onError " + e.getMessage());
if (null != callback) {
callback.onError(e.getMessage());
}
}
@Override
public void onComplete() {
LogUtils.i("download onComplete ");
}
});
}
}
7. 写文件的关键代码,用的RandomAccessFile
注意断点续传时,responseBody.contentLength()返回的不再是文件的大小,而是续传部分的大小,因此回调时要加上已下载文件的大小
private static void writeFileToDisk(ResponseBody responseBody, String filePath, final DownloadCallback callback) throws IOException {
long totalByte = responseBody.contentLength();
long downloadByte = 0;
File file = new File(filePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
byte[] buffer = new byte[1024 * 4];
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rwd");
long tempFileLen = file.length();
randomAccessFile.seek(tempFileLen);
while (true) {
int len = responseBody.byteStream().read(buffer);
if (len == -1) {
break;
}
randomAccessFile.write(buffer, 0, len);
downloadByte += len;
callbackProgress(tempFileLen + totalByte, tempFileLen + downloadByte, callback);
}
randomAccessFile.close();
}
结尾
上面只是展示了关键代码,完整代码请戳RxNet
目前RxNet只实现了下载功能,后续考虑封装各种网络请求,做成一个通用的网络框架。
网友评论