记一次 Retrofit 文件下载边下边写文件不生效

565 阅读1分钟

背景

retrofit + okhttp 实现大文件下载边下边写文件,代码如下

/**
 * 下载文件
 */
@GET
@Streaming
suspend fun download(@Url url: String, @Header("RANGE") range: String): ResponseBody

这里用到了 @Streaming 注解声明直接返回 ResponseBody对象,这是我们实现边下边写文件的必要条件。

下载

private suspend fun writeResponseBodyToDisk(
    body: ResponseBody,
    destinationFile: File,
    range: Long,
    progressCallback: suspend (Long, Long) -> Unit
) {
    val randomAccessFile = RandomAccessFile(destinationFile, "rw")
    randomAccessFile.seek(range)
    val bufferedInputStream = BufferedInputStream(body.byteStream(), WRITE_BUFFER)
    val fileSize = body.contentLength() + range
    var totalBytesRead = range
    val buffer = ByteArray(WRITE_BUFFER)
    while (coroutineContext.isActive) {
        val bytesRead = bufferedInputStream.read(buffer, 0, WRITE_BUFFER)
        if (bytesRead == -1) break
        randomAccessFile.write(buffer, 0, bytesRead)
        totalBytesRead += bytesRead
        progressCallback(totalBytesRead, fileSize)
    }
    randomAccessFile.close()
    bufferedInputStream.close()
}

预期是 progressCallback 实时回调,实际表现是等很久全部下载完成之后,progressCallback 全部一起回调。

解决方案

  1. 排查一圈后发现是与 HttpLoggingInterceptor 有关,开发环境下设置成了 Level.Body。 去掉 HttpLoggingInterceptor 或者改成 Level.Headers 之后问题解决。
  2. 保留 HttpLoggingInterceptor,下载单独创建一个 OkHttpClient 实例,但是需要复用底层线程池。

全局单例的 OkHttpClient

private val client by lazy {
    OkHttpClient.Builder()
        .pingInterval(5, TimeUnit.SECONDS)
        .build()
}

普通 http 请求的 OkHttpClient

private val repositoryImpl by lazy {
    val httpLoggingInterceptor = HttpLoggingInterceptor()
    httpLoggingInterceptor.level =
        if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE

    val retrofit = Retrofit.Builder()
        .client(
            client.newBuilder()
                .addInterceptor(httpLoggingInterceptor)
                .build()
        )
        .baseUrl("http://127.0.0.1:8888/")
        .addConverterFactory(GsonConverterFactory.create(JsonUtils.gson))
        .addCallAdapterFactory(FlowCallAdapterFactory.create())
        .build()
    RepositoryImpl(retrofit.create(APIService::class.java))
}

下载用的 OkHttpClient

val retrofit = Retrofit.Builder()
    .client(
            // 复用全局的 OkHttpClient 单例
            client.newBuilder()
            .readTimeout(30, TimeUnit.SECONDS)
            .build()
    )
    .baseUrl("http://127.0.0.1:8888/")
    .build()

原因

翻看 HttpLoggingInterceptor 源码 intercept 我们可以看到这样一段代码

if (!logBody || !response.promisesBody()) {
    logger.log("<-- END HTTP")
  } else if (bodyHasUnknownEncoding(response.headers)) {
    logger.log("<-- END HTTP (encoded body omitted)")
  } else {
    // 设置成 Level.Body 后默认会缓存整个 body,导致 @Streaming 没有起作用
    val source = responseBody.source()
    source.request(Long.MAX_VALUE) // Buffer the entire body.
    var buffer = source.buffer
    // ... 省略
  }
}