Android OkHttp + Retrofit 下载文件与进度监听

举报
AnRFDev 发表于 2021/11/29 22:07:40 2021/11/29
【摘要】 下载文件是一个比较常见的需求。给定一个url,我们可以使用URLConnection下载文件。使用OkHttp也可以通过流来下载文件。网页和很多应用都有显示下载进度的功能。给OkHttp中添加拦截器,也可实现下载进度的监听功能。 使用流来实现下载文件代码可以参考:https://github.com/AnRFDev/android-Basic4/tree/master/appdowloads...

下载文件是一个比较常见的需求。给定一个url,我们可以使用URLConnection下载文件。使用OkHttp也可以通过流来下载文件。
网页和很多应用都有显示下载进度的功能。给OkHttp中添加拦截器,也可实现下载进度的监听功能。

使用流来实现下载文件

代码可以参考:https://github.com/AnRFDev/android-Basic4/tree/master/appdowloadsample

获取并使用字节流,需要注意两个要点,一个是服务接口方法的 @Streaming 注解,另一个是获取到ResponseBody

获取流(Stream),下面以ApiService为例。给方法添加上@Streaming的注解。

private interface ApiService {
    @Streaming
    @GET
    Observable<ResponseBody> download(@Url String url);
}

download方法的参数url需要@Url注解。
返回的Observable里的类型是ResponseBody

初始化OkHttp。记得填入你的baseUrl。

OkHttpClient okHttpClient = new OkHttpClient.Builder()
        .connectTimeout(8, TimeUnit.SECONDS)
        .build();

retrofit = new Retrofit.Builder()
        .client(okHttpClient)
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .baseUrl("https://rustfisher.com") // only for test
        .build();

发起网络请求。获取到ResponseBody

String downUrl = "rustfisher.com/aaa-unreal.apk";
retrofit.create(ApiService.class)
        .download(downUrl)
        .subscribeOn(Schedulers.io())
        .observeOn(Schedulers.io())
        .doOnNext(new Consumer<ResponseBody>() {
            @Override
            public void accept(ResponseBody body) throws Exception {
                // 处理 ResponseBody 中的流...
            }
        })
        .doOnError(new Consumer<Throwable>() {
            @Override
            public void accept(Throwable throwable) throws Exception {
                Log.e(TAG, "accept on error: " + downUrl, throwable);
            }
        })
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Observer<ResponseBody>() {
            @Override
            public void onSubscribe(Disposable d) {

            }

            @Override
            public void onNext(ResponseBody responseBody) {

            }

            @Override
            public void onError(Throwable e) {
                Log.e(TAG, "Download center retrofit onError: ", e);
            }

            @Override
            public void onComplete() {

            }
        });

accept方法里得到ResponseBody,拿到字节流body.byteStream()

这里会先创建一个临时文件tmpFile,把数据写到临时文件里。下载完成后再重命名成目标文件targetFile。

public void saveFile(ResponseBody body) {
    state = DownloadTaskState.DOWNLOADING; // 更新状态
    byte[] buf = new byte[2048];
    int len;
    FileOutputStream fos = null;
    try {
        Log.d(TAG, "saveFile: body content length: " + body.contentLength());
        srcInputStream = body.byteStream();
        File dir = tmpFile.getParentFile();
        if (dir == null) {
            throw new FileNotFoundException("target file has no dir.");
        }
        if (!dir.exists()) {
            boolean m = dir.mkdirs();
            onInfo("Create dir " + m + ", " + dir);
        }
        File file = tmpFile;
        if (!file.exists()) {
            boolean c = file.createNewFile();
            onInfo("Create new file " + c);
        }
        fos = new FileOutputStream(file);
        long time = System.currentTimeMillis();
        while ((len = srcInputStream.read(buf)) != -1 && !isCancel) {
            fos.write(buf, 0, len);
            int duration = (int) (System.currentTimeMillis() - time);

            int overBytes = len - downloadBytePerMs() * duration;
            if (overBytes > 0) {
                try {
                    Thread.sleep(overBytes / downloadBytePerMs());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            time = System.currentTimeMillis();
            if (isCancel) {
                state = DownloadTaskState.CLOSING;
                srcInputStream.close();
                break;
            }
        }
        if (!isCancel) {
            fos.flush();
            boolean rename = tmpFile.renameTo(targetFile);
            if (rename) {
                setState(DownloadTaskState.DONE);
                onSuccess(url);
            } else {
                setState(DownloadTaskState.ERROR);
                onError(url, new Exception("Rename file fail. " + tmpFile));
            }
        }
    } catch (FileNotFoundException e) {
        Log.e(TAG, "saveFile: FileNotFoundException ", e);
        setState(DownloadTaskState.ERROR);
        onError(url, e);
    } catch (Exception e) {
        Log.e(TAG, "saveFile: IOException ", e);
        setState(DownloadTaskState.ERROR);
        onError(url, e);
    } finally {
        try {
            if (srcInputStream != null) {
                srcInputStream.close();
            }
            if (fos != null) {
                fos.close();
            }
        } catch (IOException e) {
            Log.e(TAG, "saveFile", e);
        }
        if (isCancel) {
            onCancel(url);
        }
    }
}

每次读数据的循环,计算读了多少数据和用了多少时间。
在某些业务场景里,需要控制下载的速度。比如减轻服务器,路由器或者ap的负担。
超过限速后主动sleep一下,达到从客户端这边控制下载速度的效果。要注意不能sleep太久,以免socket关闭。
这里控制的是网络数据流与本地文件的读写速度。

下载进度监听

OkHttp实现下载进度监听,可以从字节流的读写那里入手。也可以使用拦截器,参考官方的例子
这里用拦截器的方式实现网络下载进度监听功能。

定义回调与网络拦截器

先定义回调。回调方法里尽量多传送了参数。

public interface ProgressListener {
    void update(String url, long bytesRead, long contentLength, boolean done);
}

自定义ProgressResponseBody

public class ProgressResponseBody extends ResponseBody {

    private final ResponseBody responseBody;
    private final ProgressListener progressListener;
    private BufferedSource bufferedSource;
    private final String url;

    ProgressResponseBody(String url, ResponseBody responseBody, ProgressListener progressListener) {
        this.responseBody = responseBody;
        this.progressListener = progressListener;
        this.url = url;
    }

    @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(final Source source) {
        return new ForwardingSource(source) {
            long totalBytesRead = 0L;

            @Override
            public long read(Buffer sink, long byteCount) throws IOException {
                long bytesRead = super.read(sink, byteCount);
                // read() returns the number of bytes read, or -1 if this source is exhausted.
                totalBytesRead += bytesRead != -1 ? bytesRead : 0;
                progressListener.update(url, totalBytesRead, responseBody.contentLength(), bytesRead == -1);
                return bytesRead;
            }
        };
    }
}

定义拦截器。从Response中获取信息。

public class ProgressInterceptor implements Interceptor {

    private ProgressListener progressListener;

    public ProgressInterceptor(ProgressListener progressListener) {
        this.progressListener = progressListener;
    }

    @NotNull
    @Override
    public Response intercept(@NotNull Chain chain) throws IOException {
        Response originalResponse = chain.proceed(chain.request());
        return originalResponse.newBuilder()
                .body(new ProgressResponseBody(chain.request().url().url().toString(), originalResponse.body(), progressListener))
                .build();
    }
}

添加拦截器

在创建OkHttpClient时添加ProgressInterceptor

OkHttpClient okHttpClient = new OkHttpClient.Builder()
    .connectTimeout(8, TimeUnit.SECONDS)
    .addInterceptor(new ProgressInterceptor(new ProgressListener() {
        @Override
        public void update(String url, long bytesRead, long contentLength, boolean done) {
            // tellProgress(url, bytesRead, contentLength, done);
        }
    }))
    .build();

值得注意的是这里的进度更新非常频繁。并不一定每次回调都要去更新UI。减轻主线程的压力。

小结

直接获取流可以下载文件,写到本地。使用拦截器可以实现观察下载进度的效果。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。