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

Jetpack Compose中Coil图片缓存失效及API请求重复疑问

问题描述

我用OkHttp缓存API请求结果并存储到ViewModel中,之后通过AsyncImage加载ViewModel里的图片链接。但使用Coil 2.6.0做图片缓存时遇到问题:有网络时图片加载正常,断网后无法从缓存读取图片,只会显示默认图片。

另外还有个疑问:不管开启或关闭网络,API请求的override fun onResponse(call: Call, response: Response)方法总会被调用(日志打印"Coil cache image success"),想知道这是不是意味着API被重复调用?ViewModel中打印的数据始终是成功返回的列表。


代码片段

1. UI Compose部分

val imageUrl = searchViewModel.listEvent.item[index].content?.image
Log.d("TienDM", "imageUrl $imageUrl") // 日志显示有图片链接
val listener = object : ImageRequest.Listener {
    override fun onError(request: ImageRequest, result: ErrorResult) {
        Log.d("TienDM", "Coil cache image failed") // 断网时这条日志没触发
        super.onError(request, result)
    }

    override fun onSuccess(request: ImageRequest, result: SuccessResult) {
        Log.d("TienDM", "Coil cache image success")
        super.onSuccess(request, result)
    }
}
val imageRequest = ImageRequest.Builder(context)
    .data(imageUrl)
    .listener(listener)
    .memoryCacheKey(imageUrl)
    .diskCacheKey(imageUrl)
    .placeholder(R.drawable.image_faztaa_app)
    .error(R.drawable.image_faztaa_app)
    .fallback(R.drawable.image_faztaa_app)
    .networkCachePolicy(CachePolicy.ENABLED)
    .diskCachePolicy(CachePolicy.ENABLED)
    .memoryCachePolicy(CachePolicy.ENABLED)
    .crossfade(true)
    .build()

SubcomposeAsyncImage(
    model = imageRequest
)

2. ViewModel部分

private fun loadImageEvent() {
    viewModelScope.launch(Dispatchers.IO) {
        val client = OkHttpClient().newBuilder()
            .cache(Cache(File(context.cacheDir, "http-cache"), 1L * 1024L * 1024L)) // 1 MiB
            .addNetworkInterceptor(cacheInterceptor) // 仅当服务器未启用Cache-Control头时使用
            .addInterceptor(forceCacheInterceptor)
            .build()
        val request = Request.Builder()
            .url("/event/list")
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.d("TienDM", "get api failed")
            }
            override fun onResponse(call: Call, response: Response) {
                if (response.isSuccessful) {
                    val stringBody = response.body?.string()
                    Log.d("TienDM", "get api success $stringBody")
                    stringBody?.let {
                        try {
                            val events = Gson().fromJson<List<ResponseEventList>>(stringBody, object : TypeToken<List<ResponseEventList>>() {}.type)
                            _listEvent.update { events }
                        } catch (_: JsonSyntaxException) {
                        } catch (_: JsonSyntaxException) {
                        } catch (_: IllegalStateException) {
                        }
                    }
                }
            }
        })
    }
}

3. Application配置部分

override fun newImageLoader(): ImageLoader {
    return ImageLoader.Builder(this)
        .components {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                add(ImageDecoderDecoder.Factory())
            } else{
                add(GifDecoder.Factory())
            }
        }
        .crossfade(true)
        .bitmapConfig(bitmapConfig = Bitmap.Config.ARGB_8888)
        .memoryCachePolicy(CachePolicy.ENABLED)
        .memoryCache {
            MemoryCache.Builder(this)
                .maxSizePercent(0.1)
                .weakReferencesEnabled(true)
                .build()
        }
        .diskCachePolicy(CachePolicy.ENABLED)
        .diskCache {
            DiskCache.Builder()
                .directory(File(cacheDir,"image_cache").apply { mkdirs() })
                .maxSizePercent(0.001)
                .build()
        }
        .logger(DebugLogger())
        .build()
}

问题解决

一、Coil无网络无法读取缓存图片的修复方案

  1. 调整磁盘缓存大小
    你当前配置的磁盘缓存占比0.001(仅0.1%)太小,大概率导致图片缓存被系统或Coil清理。修改为合理值,比如:

    .diskCache {
        DiskCache.Builder()
            .directory(File(cacheDir,"image_cache").apply { mkdirs() })
            .maxSize(5L * 1024 * 1024) // 固定5MB缓存
            // 或者用占比:.maxSizePercent(0.05) // 占总存储5%
            .build()
    }
    
  2. 补全缓存拦截器逻辑
    你提到的cacheInterceptor和forceCacheInterceptor缺失实现,这是OkHttp缓存生效的关键。示例实现:

    // 检查网络状态的工具方法
    private fun isNetworkAvailable(context: Context): Boolean {
        val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = connectivityManager.activeNetwork ?: return false
        val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
        return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }
    
    // 强制缓存拦截器:无网络时直接用缓存
    val forceCacheInterceptor = Interceptor { chain ->
        var request = chain.request()
        if (!isNetworkAvailable(context)) {
            request = request.newBuilder()
                .cacheControl(CacheControl.FORCE_CACHE)
                .build()
        }
        chain.proceed(request)
    }
    
    // 缓存控制拦截器:设置缓存有效期
    val cacheInterceptor = Interceptor { chain ->
        val response = chain.proceed(chain.request())
        val cacheControl = CacheControl.Builder()
            .maxAge(7, TimeUnit.DAYS) // 缓存7天
            .build()
        response.newBuilder()
            .header("Cache-Control", cacheControl.toString())
            .build()
    }
    
  3. 优化Coil请求配置
    去掉手动设置的memoryCacheKey和diskCacheKey,Coil会自动根据URL生成唯一缓存键,手动设置可能导致键冲突。同时明确无网络时的缓存策略:

    val imageRequest = ImageRequest.Builder(context)
        .data(imageUrl)
        .listener(listener)
        .placeholder(R.drawable.image_faztaa_app)
        .error(R.drawable.image_faztaa_app)
        .fallback(R.drawable.image_faztaa_app)
        .networkCachePolicy(if (isNetworkAvailable(context)) CachePolicy.ENABLED else CachePolicy.READ_ONLY)
        .diskCachePolicy(CachePolicy.ENABLED)
        .memoryCachePolicy(CachePolicy.ENABLED)
        .crossfade(true)
        .build()
    
  4. 验证缓存写入状态
    打开设备的文件管理器,查看/data/data/你的包名/cache/image_cache目录下是否有缓存文件;或者通过Coil的DebugLogger查看日志,确认缓存是否命中。

二、API请求onResponse始终被调用的原因

这不代表API被重复发起网络请求,是OkHttp的缓存机制导致的:

  • 有网络时:OkHttp会先检查本地缓存是否有效,若有效直接返回缓存响应,不会发起网络请求;若缓存过期,才会发起网络请求并更新缓存。
  • 无网络时:OkHttp直接返回本地缓存的响应,不会发起网络请求。

你可以通过以下代码判断是否是缓存响应:

override fun onResponse(call: Call, response: Response) {
    if (response.cacheResponse() != null) {
        Log.d("TienDM", "从缓存读取API响应")
    } else {
        Log.d("TienDM", "从网络读取API响应")
    }
    // 原有逻辑...
}

内容的提问来源于stack exchange,提问作者Minh Tiến Đào

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 19:32:05