美文网首页网络面试题
Android使用Retrofit下载文件

Android使用Retrofit下载文件

作者: GoMoon | 来源:发表于2019-07-23 15:29 被阅读21次

    前言

    公司的两款App项目里都有下载文件的需求,以前都是用xUtils这个框架。很早开始,我们的网络层已经用的是Retrofit+RxJava+OKHttp,唯独下载请求还是要用xUtils,如此一来就显得有点多余了,毕竟xutils这个框架还是比较大的。于是考虑自己来封装下载文件的操作,就用Retrofit+RxJava+OKHttp,当然“进度回调”和“断点续传”都是必须支持的。

    先上个效果动图:

    image

    二. 思路

    1. 调用者需要传入一个接口(Callback),下载时实时把进度回调。
    2. 自定义ReponseBody和Interceptor,以实时获取下载的进度。
    3. 请求头加上“Range”,写文件到磁盘时用RandomAccessFile,以实现断点续传。
    4. 下载时把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只实现了下载功能,后续考虑封装各种网络请求,做成一个通用的网络框架。

    相关文章

      网友评论

        本文标题:Android使用Retrofit下载文件

        本文链接:https://www.haomeiwen.com/subject/afxalctx.html