Android(Coil,Glide)大量图片加载缓存清理问题(二 Coil处理)

Android(Coil,Glide)大量图片加载缓存清理问题

Glide/Coil 加载大量图片后--->磁盘缓存(已满)-->请求网络资源重新下载.

App加载加载大量图片资源以后导致磁盘缓存已满,从而引发频繁的清理磁盘缓存和下载网络资源存入磁盘缓存,进而导致指定资源频繁请求.

1:处理方案

  • 扩大磁盘缓存,类似HashMap动态扩容(DiskLruCache,是固定容量,处理复杂)
  • 分类型分磁盘存储

我们选择了第二种方案,将图片资源分为日常存储资源和固定存储的资源.

2:功能实现

自定义缓存存储,创建固定缓存的对象,然后加载和存储的时候调用固定存储的方法,日常的调用日常方法

2.1 自定义缓存存储

kotlin 复制代码
package com.wkq.util.coil

import android.content.Context
import android.graphics.Bitmap
import android.util.LruCache
import android.widget.ImageView
import coil3.ImageLoader
import coil3.disk.DiskCache
import coil3.disk.directory
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.request.crossfade
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.security.MessageDigest

/**
 * CacheManager
 *
 * 功能:
 * 1. 管理 Coil 的 ImageLoader 缓存(内存/磁盘)
 * 2. 提供永久缓存机制(内存 + 磁盘),适合头像、礼物等频繁使用资源
 * 3. 支持 URL / 本地 File / resId
 * 4. 提供 ImageView 测量完成后生成 Bitmap 的工具,避免第一次加载模糊
 */
object CacheManager {

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    // 永久内存缓存
    private var pinnedMemoryCache: LruCache<String, Bitmap>? = null

    // 永久磁盘缓存 (使用 Coil DiskCache 管理大小)
    private var pinnedDiskCache: DiskCache? = null

    // Coil ImageLoader 实例
    private var imageLoader: ImageLoader? = null

    private var isInitialized = false

    /**
     * 初始化缓存
     * @param context Context
     * @param pinnedMemoryMB 永久内存缓存大小 (MB)
     * @param memoryCachePercent Coil 内存缓存占比
     * @param pinnedDiskMB 永久磁盘缓存大小 (MB)
     * @param diskCacheMB Coil 磁盘缓存大小 (MB)
     */
    fun init(context: Context, pinnedMemoryMB: Int = 20, memoryCachePercent: Double = 0.25, pinnedDiskMB: Int = 200, diskCacheMB: Int = 200) {
        if (isInitialized) return

        val appContext = context.applicationContext
        // -------------------------------
        // 永久内存缓存,LruCache 按 Bitmap.byteCount 计算大小
        pinnedMemoryCache = object : LruCache<String, Bitmap>(pinnedMemoryMB * 1024 * 1024) {
            override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
        }
        // -------------------------------
        // 永久磁盘缓存 (LRU 管理)
        pinnedDiskCache = DiskCache.Builder()
            .directory(File(appContext.cacheDir, "pinned_disk_cache"))
            .maxSizeBytes(pinnedDiskMB * 1024L * 1024L)
            .build()
        // -------------------------------
        // Coil 缓存
        val coilDiskCache = DiskCache.Builder()
            .directory(File(appContext.cacheDir, "coil_image_cache"))
            .maxSizeBytes(diskCacheMB * 1024L * 1024L)
            .build()
        val loader = ImageLoader.Builder(appContext)
            .crossfade(true) // 渐入动画
            .components {
                add(OkHttpNetworkFetcherFactory()) // 注册 OkHttp 网络加载器
                add(coil3.gif.GifDecoder.Factory()) // 注册 GIF 解码器
            }
            .memoryCache {
                MemoryCache.Builder()
                    .maxSizePercent(appContext, memoryCachePercent)
                    .strongReferencesEnabled(true) // 启用强引用,提高缓存稳定性
                    .build()
            }
            .diskCache { coilDiskCache }
            .build()
        imageLoader = loader
        // 集成到 Coil 单例加载器
        coil3.SingletonImageLoader.setSafe { loader }
        isInitialized = true
    }

    /** 获取 Coil ImageLoader */
    fun getImageLoader(): ImageLoader = imageLoader ?: throw IllegalStateException("CacheManager not initialized. Call init(context) first.")

    // ===============================
    // 永久内存缓存操作
    fun putPinnedMemory(key: String, bitmap: Bitmap) {
        pinnedMemoryCache?.put(key, bitmap)
    }

    fun getPinnedMemory(key: String): Bitmap? = pinnedMemoryCache?.get(key)
    
    fun clearPinnedMemory() {
        pinnedMemoryCache?.evictAll()
    }
    // 存储到磁盘
    fun putPinnedDisk(key: String, bitmap: Bitmap) {
        val diskCache = pinnedDiskCache ?: return
        scope.launch {
            val editor = diskCache.openEditor(key.md5()) ?: return@launch
            try {
                FileOutputStream(editor.data.toFile()).use { 
                    bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) 
                }
                editor.commit()
            } catch (e: Exception) {
                editor.abort()
                e.printStackTrace()
            }
        }
    }
    /** 获取磁盘缓存 */
    fun getPinnedDisk(key: String): File? {
        val diskCache = pinnedDiskCache ?: return null
        val snapshot = diskCache.openSnapshot(key.md5()) ?: return null
        return try {
            snapshot.data.toFile()
        } catch (e: Exception) {
            null
        } finally {
            snapshot.close()
        }
    }
    /** 清空磁盘缓存 */
    fun clearPinnedDisk() {
        scope.launch {
            pinnedDiskCache?.clear()
        }
    }

    // ===============================
    // Coil 内存/磁盘缓存清理
    fun clearMemoryCache() {
        imageLoader?.memoryCache?.clear()
    }

    fun clearDiskCache() {
        scope.launch {
            imageLoader?.diskCache?.clear()
        }
    }

    /** 清理所有缓存:永久内存/磁盘 + Coil 内存/磁盘 */
    fun clearAllCache() {
        clearPinnedMemory()
        clearPinnedDisk()
        clearMemoryCache()
        clearDiskCache()
    }

    // ===============================
    // 工具方法
    /** String -> MD5,用于生成唯一文件名 */
    private fun String.md5(): String {
        return try {
            val digest = MessageDigest.getInstance("MD5")
            val result = digest.digest(toByteArray())
            result.joinToString("") { "%02x".format(it) }
        } catch (e: Exception) {
            this.hashCode().toString()
        }
    }

    /**
     * 等待 ImageView 测量完成后生成 Bitmap
     * 避免第一次加载模糊
     */
    fun ImageView.postBitmap(drawableBitmap: Bitmap, onReady: (Bitmap) -> Unit) {
        if (width > 0 && height > 0) {
            onReady(drawableBitmap.resize(width, height))
        } else {
            post {
                onReady(drawableBitmap.resize(width.coerceAtLeast(1), height.coerceAtLeast(1)))
            }
        }
    }

    /** 按 ImageView 尺寸缩放 Bitmap */
    private fun Bitmap.resize(targetWidth: Int, targetHeight: Int): Bitmap {
        if (width == targetWidth && height == targetHeight) return this
        return Bitmap.createScaledBitmap(this, targetWidth, targetHeight, true)
    }
}

2.2 资源加载

kotlin 复制代码
package com.wkq.util.coil

import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.drawable.toDrawable
import coil3.asDrawable
import coil3.request.CachePolicy
import coil3.request.ImageRequest
import coil3.request.crossfade
import coil3.request.error
import coil3.request.placeholder
import coil3.request.target
import coil3.request.transformations
import coil3.size.Scale
import coil3.load
import coil3.request.Disposable
import coil3.transform.CircleCropTransformation
import coil3.transform.RoundedCornersTransformation
import coil3.transform.Transformation
import com.wkq.util.coil.CacheManager.postBitmap
import java.io.File

/**
 * ImageLoaderUtil
 *
 * 功能:
 * 1. 图片加载工具类,支持 URL / File / resId
 * 2. 支持普通加载和永久缓存加载
 * 3. 支持 GIF 动图加载,避免 OOM
 * 4. 自动按 ImageView 尺寸生成 Bitmap,避免第一次加载模糊
 * 5. 支持圆角、圆形、灰度等变换
 * 6. 提供 ImageView 扩展函数,使用更便捷
 */
object ImageLoaderUtil {

    /**
     * 基础加载方法
     * @return Disposable 可用于取消请求
     */
    fun load(
        imageView: ImageView,
        data: Any?,
        placeholder: Any? = null,
        error: Any? = null,
        isCircle: Boolean = false,
        isGrayscale: Boolean = false,
        radius: Float = 0f,
        scale: Scale = Scale.FILL,
        memoryCachePolicy: CachePolicy = CachePolicy.ENABLED,
        diskCachePolicy: CachePolicy = CachePolicy.ENABLED,
        onSuccess: ((Drawable) -> Unit)? = null,
        onError: ((Throwable) -> Unit)? = null,
        builder: ImageRequest.Builder.() -> Unit = {}
    ): Disposable? {
        if (data == null) return null
        
        // 使用 Coil 的单例 ImageLoader,它带有了正确的解码组件(GIF 等)
        val imageLoader = CacheManager.getImageLoader()
        
        // 直接使用扩展函数,它会自动处理 ImageView 的生命周期并设置加载结果
        return imageView.load(data, imageLoader) {
            (placeholder as? Int)?.let { placeholder(it) }
            (placeholder as? Drawable)?.let { placeholder(it) }
            (error as? Int)?.let { error(it) }
            (error as? Drawable)?.let { error(it) }
            
            scale(scale)
            crossfade(true)
            memoryCachePolicy(memoryCachePolicy)
            diskCachePolicy(diskCachePolicy)
            // 圆形圆角处理
            val transformationsList = mutableListOf<Transformation>()
            if (isCircle) {
                transformationsList.add(CircleCropTransformation())
            } else if (radius > 0) {
                transformationsList.add(RoundedCornersTransformation(radius))
            }
            // 判断灰度处理
            if (isGrayscale) {
                transformationsList.add(GrayscaleTransformation())
            }
            if (transformationsList.isNotEmpty()) {
                transformations(transformationsList)
            }

            // 这里监听加载结果,用于回调扩展
            listener(
                onSuccess = { _, _ ->
                    // 获取 ImageView 当前显示的 Drawable(Coil 已经设置进去的)
                    val currentDrawable = imageView.drawable
                    if (currentDrawable is android.graphics.drawable.Animatable) {
                        currentDrawable.start()
                    }
                    onSuccess?.invoke(currentDrawable!!)
                },
                onError = { _, result ->
                    onError?.invoke(result.throwable)
                }
            )
            
            apply(builder)
        }
    }

    /**
     * 永久缓存加载图片(内存 + 磁盘)
     * 适合头像、礼物图标等小尺寸但高频显示的资源
     */
    fun loadPinned(
        imageView: ImageView,
        data: Any,
        placeholder: Any? = null,
        error: Any? = null,
        isCircle: Boolean = false,
        isGrayscale: Boolean = false,
        radius: Float = 0f,
        builder: ImageRequest.Builder.() -> Unit = {}
    ): Disposable? {
        val key = when (data) {
            is String -> data
            is File -> data.absolutePath
            is Int -> "res_$data"
            else -> data.toString()
        } + "_c${isCircle}_r${radius}_g${isGrayscale}"
        // 先检查内存缓存
        CacheManager.getPinnedMemory(key)?.let { bitmap ->
            if (!isCircle && radius <= 0f && !isGrayscale) {
                imageView.setImageBitmap(bitmap)
                return null
            }
        }

        // 检查磁盘缓存或网络加载
        val dataSource = CacheManager.getPinnedDisk(key) ?: data
        
        return load(
            imageView = imageView,
            data = dataSource,
            placeholder = placeholder,
            error = error,
            isCircle = isCircle,
            isGrayscale = isGrayscale,
            radius = radius,
            onSuccess = { drawable ->
                val bmp = drawable.toBitmap()
                CacheManager.putPinnedMemory(key, bmp)
                if (data is String || data is File) {
                    CacheManager.putPinnedDisk(key, bmp)
                }
            },
            builder = builder
        )
    }

    /**
     * GIF 动图加载
     */
    fun loadGif(
        imageView: ImageView,
        data: Any,
        placeholder: Any? = null,
        error: Any? = null,
        builder: ImageRequest.Builder.() -> Unit = {}
    ): Disposable? {
        return load(
            imageView = imageView,
            data = data,
            placeholder = placeholder,
            error = error,
            memoryCachePolicy = CachePolicy.DISABLED, // GIF 默认不存内存,防 OOM
            builder = builder
        )
    }

    /**
     * 预加载图片
     */
    fun preload(
        context: android.content.Context,
        data: Any?,
        builder: ImageRequest.Builder.() -> Unit = {}
    ): Disposable? {
        if (data == null) return null
        val request = ImageRequest.Builder(context)
            .data(data)
            .apply(builder)
            .build()
        return CacheManager.getImageLoader().enqueue(request)
    }

    /**
     * 灰度变换支持
     */
    private class GrayscaleTransformation : Transformation() {
        override val cacheKey: String = "GrayscaleTransformation"
        override suspend fun transform(input: android.graphics.Bitmap, size: coil3.size.Size): android.graphics.Bitmap {
            val paint = android.graphics.Paint(android.graphics.Paint.ANTI_ALIAS_FLAG or android.graphics.Paint.FILTER_BITMAP_FLAG)
            val colorMatrix = android.graphics.ColorMatrix()
            colorMatrix.setSaturation(0f)
            paint.colorFilter = android.graphics.ColorMatrixColorFilter(colorMatrix)

            val output = android.graphics.Bitmap.createBitmap(input.width, input.height, input.config ?: android.graphics.Bitmap.Config.ARGB_8888)
            val canvas = android.graphics.Canvas(output)
            canvas.drawBitmap(input, 0f, 0f, paint)
            return output
        }
    }
}

/**
 * ImageView 扩展函数
 */
fun ImageView.loadUrl(
    url: String?,
    placeholder: Any? = null,
    error: Any? = null,
    isCircle: Boolean = false,
    isGrayscale: Boolean = false,
    radius: Float = 0f,
    memoryCachePolicy: CachePolicy = CachePolicy.ENABLED,
    diskCachePolicy: CachePolicy = CachePolicy.ENABLED,
    onSuccess: ((Drawable) -> Unit)? = null,
    onError: ((Throwable) -> Unit)? = null,
    builder: ImageRequest.Builder.() -> Unit = {}
) = ImageLoaderUtil.load(
    this, url, placeholder, error, isCircle, isGrayscale, radius, 
    memoryCachePolicy = memoryCachePolicy, 
    diskCachePolicy = diskCachePolicy, 
    onSuccess = onSuccess, onError = onError, builder = builder
)

fun ImageView.loadRes(
    resId: Int,
    placeholder: Any? = null,
    error: Any? = null,
    isCircle: Boolean = false,
    isGrayscale: Boolean = false,
    radius: Float = 0f,
    memoryCachePolicy: CachePolicy = CachePolicy.ENABLED,
    diskCachePolicy: CachePolicy = CachePolicy.ENABLED,
    onSuccess: ((Drawable) -> Unit)? = null,
    onError: ((Throwable) -> Unit)? = null,
    builder: ImageRequest.Builder.() -> Unit = {}
) = ImageLoaderUtil.load(
    this, resId, placeholder, error, isCircle, isGrayscale, radius, 
    memoryCachePolicy = memoryCachePolicy, 
    diskCachePolicy = diskCachePolicy, 
    onSuccess = onSuccess, onError = onError, builder = builder
)

fun ImageView.loadFile(
    file: File?,
    placeholder: Any? = null,
    error: Any? = null,
    isCircle: Boolean = false,
    isGrayscale: Boolean = false,
    radius: Float = 0f,
    memoryCachePolicy: CachePolicy = CachePolicy.ENABLED,
    diskCachePolicy: CachePolicy = CachePolicy.ENABLED,
    onSuccess: ((Drawable) -> Unit)? = null,
    onError: ((Throwable) -> Unit)? = null,
    builder: ImageRequest.Builder.() -> Unit = {}
) = ImageLoaderUtil.load(
    this, file, placeholder, error, isCircle, isGrayscale, radius, 
    memoryCachePolicy = memoryCachePolicy, 
    diskCachePolicy = diskCachePolicy, 
    onSuccess = onSuccess, onError = onError, builder = builder
)

3.调用

kotlin 复制代码
fun test() {
    // 1. 普通加载 (使用扩展函数)
    binding.ivNormal.loadUrl(testImg)

    // 2. 圆形裁剪
    binding.ivCircle.loadUrl(avatarImg, isCircle = true)

    // 3. 圆角加载 (30dp)
    binding.ivRounded.loadUrl(testImg, radius = 60f) // 这里的单位是 px,demo 使用 60 示意

    // 4. 灰度模式
    binding.ivGrayscale.loadUrl(testImg, isGrayscale = true)

    // 5. GIF 动图 (自动支持,也可以显式调用以禁用内存缓存防 OOM)
    ImageLoaderUtil.loadGif(binding.ivGif, gifUrl)

    // 6. 永久缓存加载 (Pinned Cache)
    // 适合高频显示的头像、小图标,即使应用清理缓存也不会丢失(除非手动清空永久缓存)
   val load= ImageLoaderUtil.loadPinned(binding.ivPinned, avatarImg, isCircle = true)
    load?.dispose()
}

总结

Coil处理磁盘已满情况下,高频情况下资源频繁请求网络问题.Glide和逻辑和这个差不多,按照这种思路修改一样能够实现.

相关推荐
jzlhll12338 分钟前
kotlin flow去重distinctUntilChanged vs distinctUntilChangedBy
android·开发语言·kotlin
渡我白衣1 小时前
【MySQL基础】(3):MySQL库与表的操作
android·数据库·人工智能·深度学习·神经网络·mysql·adb
huwuhang1 小时前
植物大战僵尸版本所有版本合集下载含杂交版 融合版 火影版 二战版 无双版 抽卡版 β版等等
android·游戏·电脑·游戏机
尤老师FPGA9 小时前
petalinux修改设备树添加vdma生成linux系统
android·linux·运维
月山知了10 小时前
linux kernel component子系统:基于rk3588 Android 14 kernel-6.1 display-subsystem代码分析
android·linux·运维
leo_messi9412 小时前
多线程(五) -- 并发工具(二) -- J.U.C并发包(八) -- CompletableFuture组合式异步编程
android·java·c语言
Deryck_德瑞克15 小时前
【已解决】MySQL连接出错 1045 - Access denied for user ‘root‘@‘::1‘
android·mysql·adb
2501_9159184115 小时前
iOS性能测试工具 Instruments、Keymob的使用方法 不局限 FPS
android·ios·小程序·https·uni-app·iphone·webview
.豆鲨包16 小时前
【Android】组件化搭建的一般流程
android
心有—林夕17 小时前
MySQL 误操作恢复完全指南
android·数据库·mysql