背景
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
全部一起回调。
解决方案
- 排查一圈后发现是与
HttpLoggingInterceptor
有关,开发环境下设置成了Level.Body
。 去掉HttpLoggingInterceptor
或者改成Level.Headers
之后问题解决。 - 保留
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
// ... 省略
}
}