okhttp下载文件设置进度监听


okhttp下载文件监听与 上传文件监听原理类似,上传通过requestBody的writeTo将报文发出。下载就是通过responseBody的source方法将报文接收。

仿照上传写一个responseBody

public class ProgressResponseBody extends ResponseBody {
	 ...
	 @Override
    public MediaType contentType() {
        return responseBody.contentType();
    }

 	@Override
    public BufferedSource source() {
        if (bufferedSource == null) {
            bufferedSource = Okio.buffer(new ProgressBufferedSource(responseBody.source()));
        }
        return bufferedSource;
    }
    
	class ProgressBufferedSource extends ForwardingSource {

        ProgressBufferedSource(Source delegate) {
            super(delegate);
        }

        @Override
        public long read(Buffer sink, long byteCount) throws IOException {
            long readLength = super.read(sink, byteCount);
            bytesRead += readLength > 0 ? readLength : 0;
            progressListener.onProgress(bytesRead, responseBody.contentLength());
            return readLength;
        }
    }
}

现在就差把这个responseBody放到这次请求当中。

网上广为流传的写法

了解okhttp的部分执行原理,它最终执行的是一个操作链Interceptors。链每个节点都会做一部分http请求操作。
通过自定义一个Interceptor,把responseBody插入执行链中。

public class ProgressResponseInterceptor implements Interceptor {

    private ProgressResponseBody.ProgressListener progressListener;

    public ProgressResponseInterceptor(ProgressResponseBody.ProgressListener progressListener) {
        this.progressListener = progressListener;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Response responseResponse = chain.proceed(chain.request());
        return responseResponse.newBuilder()
                .body(new ProgressResponseBody(responseResponse.headers(), responseResponse.body(), progressListener))
                .build();
    }
}
OkHttpClient client = new OkHttpClient().newBuilder()
                .addInterceptor(new ProgressResponseInterceptor(new ProgressResponseBody.ProgressListener() {
                    @Override
                    public void onProgress(long currentBytes, long contentLength) {
                        callback.progress(currentBytes, contentLength);
                    }
                }))
                .build();

另一种写法

经过跟踪执行,我发现了一个现象,于是我尝试换一种写法。
我把Callback方法按照代理的方式封装了一下,然后在onResponse回调中插入responseBody

 class MyCallback implements Callback {
            Callback delegete;

            MyCallback(Callback callback){
                delegete = callback;
            }

            @Override
            public void onFailure(Call call, IOException e) {
                delegete.onFailure(call, e);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                delegete.onResponse(call,response.newBuilder()
                        .body(new ProgressResponseBody(response.headers(), response.body(), new ProgressResponseBody.ProgressListener() {
                            @Override
                            public void onProgress(long currentBytes, long contentLength) {
                                callback.progress(currentBytes, contentLength);
                            }
                        }))
                        .build());
            }
        }

这么写结果竟然也是对的。

我跟踪执行发现onResponse回调竟然在onProgress之前调用。这时我猜测有两种可能,一种是我监听了个寂寞,其实报文早已传输完成,监听的只不过是把内存中的数据存入外存的过程。这个经过测试感觉时间对不上不太可能。那就是另一种可能,onResponse回调执行时,responseBody还没有开始接收。

okhttp执行监听原理

我们从回调onResponse倒着找源码,先找到调用onResponse的地方

@Override protected void execute() {
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } 
    }

response对象是从getResponseWithInterceptorChain中获取的。我们找到链的后一步

public final class CallServerInterceptor implements Interceptor {

  @Override public Response intercept(Chain chain) throws IOException {
    httpStream.writeRequestHeaders(request);

    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();
    }

    httpStream.finishRequest();

    Response response = httpStream.readResponseHeaders()
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    if (!forWebSocket || response.code() != 101) {
      response = response.newBuilder()
          .body(httpStream.openResponseBody(response))
          .build();
    }
    return response;
  }
}

发现这里request是直接head、body全部传输到httpstream中,而response只接收了head。并没有接收body

再从response的source()方法入手
我们看BufferSource的实现类RealBufferedSource,在onResponse回调中通过is = response.body().byteStream()获取流的,这个方法获取的就是RealBufferedSource 中inputStream方法返回的流对象,这个对象中实际调用了source的read方法。

final class RealBufferedSource implements BufferedSource {
	...
  @Override public InputStream inputStream() {
    return new InputStream() {
      @Override public int read() throws IOException {
        if (closed) throw new IOException("closed");
        if (buffer.size == 0) {
          long count = source.read(buffer, Segment.SIZE);
          if (count == -1) return -1;
        }
        return buffer.readByte() & 0xff;
      }

      @Override public int read(byte[] data, int offset, int byteCount) throws IOException {
        if (closed) throw new IOException("closed");
        checkOffsetAndCount(data.length, offset, byteCount);

        if (buffer.size == 0) {
          long count = source.read(buffer, Segment.SIZE);
          if (count == -1) return -1;
        }

        return buffer.read(data, offset, byteCount);
      }
    };
  }
}

终于找到调用responseBody中read的位置了。
okhttp在进行网络请求的时候,responseBody是在onResponse后获取的,通过以下方法都可以触发获取responseBody。不调用这些方法在okhttp默认情况下responseBody都不会获取。

response.body().string();
is = response.body().byteStream();
is.read();
response.body().bytes();
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Android Kotlin 中使用 OkHttp3 下载文件并带有下载进度,可以通过以下步骤实现: 1. 添加 OkHttp3 依赖 在 app module 的 build.gradle 文件中添加以下代码: ``` dependencies { implementation 'com.squareup.okhttp3:okhttp:4.9.0' } ``` 2. 创建 OkHttp3 客户端 在代码中创建一个 OkHttpClient 客户端: ``` val client = OkHttpClient() ``` 3. 创建下载请求 使用 OkHttp3 的 Request.Builder 创建一个下载请求,并设置下载 URL 和保存文件的路径: ``` val request = Request.Builder() .url("https://example.com/file.zip") .build() ``` 4. 创建下载监听器 定义一个回调接口,用于监听下载进度: ``` interface DownloadListener { fun onDownloadProgress(progress: Int) } ``` 在代码中实现这个接口,并在其中更新下载进度,例如: ``` val listener = object : DownloadListener { override fun onDownloadProgress(progress: Int) { runOnUiThread { // 更新下载进度条 progressBar.progress = progress } } } ``` 5. 发起下载请求 使用 OkHttpClient 的 newCall 方法发起下载请求,并在 enqueue 方法中传入一个 Callback 参数,该参数将在下载完成时回调: ``` client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // 下载失败 } override fun onResponse(call: Call, response: Response) { val inputStream: InputStream = response.body?.byteStream() ?: return // 保存文件并更新下载进度 val totalSize: Long = response.body?.contentLength() ?: -1 var downloadedSize: Long = 0 val outputStream = FileOutputStream("/storage/emulated/0/Download/file.zip") val buffer = ByteArray(8192) while (true) { val bytes = inputStream.read(buffer) if (bytes == -1) break outputStream.write(buffer, 0, bytes) downloadedSize += bytes val progress = (downloadedSize * 100 / totalSize).toInt() listener.onDownloadProgress(progress) } outputStream.close() inputStream.close() // 下载完成 } }) ``` 这段代码中,我们首先从 response.body 中获取输入流并创建输出流,然后使用循环逐段读取输入流的数据,再将其写入输出流,并计算下载进度,最后调用 DownloadListener 的 onDownloadProgress 方法更新下载进度。在下载完成后,我们需要关闭输入流和输出流,以及在 onFailure 方法中处理下载失败的情况。 6. 完整代码 最终的代码应该类似于这样: ``` interface DownloadListener { fun onDownloadProgress(progress: Int) } val client = OkHttpClient() val request = Request.Builder() .url("https://example.com/file.zip") .build() val listener = object : DownloadListener { override fun onDownloadProgress(progress: Int) { runOnUiThread { // 更新下载进度条 progressBar.progress = progress } } } client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { // 下载失败 } override fun onResponse(call: Call, response: Response) { val inputStream: InputStream = response.body?.byteStream() ?: return // 保存文件并更新下载进度 val totalSize: Long = response.body?.contentLength() ?: -1 var downloadedSize: Long = 0 val outputStream = FileOutputStream("/storage/emulated/0/Download/file.zip") val buffer = ByteArray(8192) while (true) { val bytes = inputStream.read(buffer) if (bytes == -1) break outputStream.write(buffer, 0, bytes) downloadedSize += bytes val progress = (downloadedSize * 100 / totalSize).toInt() listener.onDownloadProgress(progress) } outputStream.close() inputStream.close() // 下载完成 } }) ``` 注意,这段代码中保存文件的路径是硬编码的,你需要根据实际需求修改它。另外,为了更新 UI,我们需要在 onDownloadProgress 方法中使用 runOnUiThread 方法,以确保在主线程中执行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值