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和逻辑和这个差不多,按照这种思路修改一样能够实现.