You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Retrofit下载图片成功但文件始终损坏无法打开问题求助

问题原因&解决方案

核心问题1:Retrofit转换器干扰原始响应流

如果你的Retrofit实例全局添加了GsonConverterFactory/FastJsonConverterFactory等JSON转换器,这些转换器会优先处理所有响应,哪怕你声明返回ResponseBody,也会被转换器提前读取解析为JSON结构,最终你拿到的字节流已经是被破坏的非原始图片数据,这是最常见的「下载文件大小正常但内容损坏」的原因。

解决方案:

单独为下载请求构建无JSON转换器的Retrofit实例即可:

val downloadRetrofit = Retrofit.Builder()
    .baseUrl("你的服务基础地址")
    .client(okHttpClient)
    // 不要添加任何JSON格式转换器
    .build()

如果不想单独构建实例,也可以将ScalarsConverterFactory添加到JSON转换器之前,让ResponseBody类型的响应优先被字符串转换器处理,避免被JSON解析逻辑破坏。

核心问题2:OkHttp拦截器消费了响应流

如果你的OkHttpClient配置了HttpLoggingInterceptor且日志等级设置为BODY,拦截器会完整读取响应体内容打印日志,原始流被消费后无法二次读取,导致你写入文件的是空流,最终会出现「文件大小和响应头的Content-Length一致但内容为空/乱码」的现象。

解决方案:

  • 下载专用的OkHttpClient不要配置日志拦截器,或者将日志等级调低到HEADERS/BASIC
  • 如果必须保留BODY级别的日志,需要给OkHttp配置响应缓存,或者用自定义拦截器复制响应体副本后再打印日志,避免消费原始流。

核心问题3:低版本Retrofit的协程适配缺陷

部分低于2.6.0版本的Retrofit对suspend函数+@Streaming的适配存在缺陷,会强制把整个响应体读取到内存再返回,大文件场景下可能出现OOM或者流截断问题。

解决方案:

  • 升级Retrofit到2.6.0及以上正式版
  • 下载接口强制添加@Streaming注解,避免Retrofit提前读取全部流到内存:
@Streaming
@GET
suspend fun downloadMedia(@Url url: String): Response<ResponseBody>

现有代码优化&排查手段

  1. 你当前代码中preferences.downloadDirFlow.collect会持续监听下载目录变更,每次目录变化都会重新触发下载请求,建议调整为只取一次当前下载目录即可:
override fun downloadMedia(url: String): Flow<RedditResult<DownloadState>> = flow {
    val downloadDir = preferences.downloadDirFlow.first()
    if (downloadDir.isEmpty()) {
        emit(RedditResult.Success(DownloadState.NoDefinedLocation))
        return@flow
    }
    // 原有下载逻辑
}
  1. 写入流之后不需要手动调用close(),use函数会在代码块执行完成后自动关闭流,重复关闭属于冗余操作。
  2. 可添加写入校验快速定位流是否被提前消费:
val writeBytes = response.body()?.byteStream()?.copyTo(output) ?: 0
if (writeBytes != response.body()?.contentLength()) {
    emit(RedditResult.Error(IOException("File write incomplete")))
    return@flow
}

内容的提问来源于stack exchange,提问作者Charlie Niekirk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 05:36:05