Android OkHttp + Retrofit 下載文件與進度監聽


本文鏈接

下載文件是一個比較常見的需求。給定一個url,我們可以使用URLConnection下載文件
使用OkHttp也可以通過流來下載文件。
給OkHttp中添加攔截器,即可實現下載進度的監聽功能。

使用流來實現下載文件

代碼可以參考:https://github.com/RustFisher/android-Basic4/tree/master/appdowloadsample

獲取並使用字節流,需要注意兩個要點,一個是服務接口方法的 @Streaming 注解,另一個是獲取到ResponseBody。

獲取流(Stream)。先定義一個服務ApiService。給方法添加上@Streaming的注解。

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

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

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

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

發起網絡請求。獲取到ResponseBody。

    String downUrl = "xxx.com/aaa.apk";
    retrofit.create(ApiService.class)
            .download(downUrl)
            .subscribeOn(Schedulers.io())
            .observeOn(Schedulers.io())
            .doOnNext(new Consumer<ResponseBody>() {
                @Override
                public void accept(ResponseBody responseBody) 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() {

                }
            });

通過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);
            }
        }
    }

每次讀數據的循環,計算讀了多少數據和用了多少時間。超過限速后主動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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM