问题现象
使用 Glide 图片加载框架加载 WebP 时,默认会将资源加载一份并缓存,后续引用相同资源 ID 会返回同一个缓存对象。这本身是常见的优化手段,但在 Android 原生 AnimatedImageDrawable 场景下会出现问题。因为 Glide 内部默认加载的 WebP 图片使用的是 AnimatedImageDrawable 类。
- 多个 View 通过 Glide 显示同一个 WebP 资源,会导致播放进度强制一致。
先启动上面的 ImageView 播放,再将图片设置到下面的 ImageView。结果后启动的开始时机被强制绑定到了和正在播放的一起。
- 任何一个调用了停止,其他的也会跟随停止。
调用 Glide.with(this).clear(imageView2) 将第二个 ImageView 播放停止并清除不显示,导致第一个 ImageView 也跟着停止了播放。
- 同一个 View 需要隐藏后再重头播放会导致开始时候闪现一下停止时的那一帧的问题。
将同一个资源在上面 ImageView 停止后设置给了第二个 ImageView,仔细观察会发现,第二个 ImageView 播放前会闪烁一下停止时候的那一帧。即使开始和停止在同一个 ImageView 发生也同样有这个问题。
解决问题的思路
开发中主要遇到的是重新播放闪现停止帧的问题。第一时间想到的是想办法避免这个问题:
在停止的时候释放掉这个播放进度的状态,回归为 0 帧位置。如果停止的时候做不到,那么在开始的时候想办法回归为 0 帧位置。
然而,查遍了系统 API,也没有见到 AnimatedImageDrawable 为用户开放对应的操作方法。
另外,即使使用 ASM 字节码修改框架等手段做到了控制帧,但这只能解决重复播放闪烁停止帧(即现象 3)。现象 1、现象 2 依然无法解决。因为它们是用的同一个 AnimatedImageDrawable 对象,所以会导致多个 View 视图播放进度是一个。
那么归根结底要解决的问题是要让每个 View 的播放状态不强关联的,并且重新播放不闪烁。想要做到这个只能每次使用都创建新的实例,因为旧的实例不提供清理状态的方法。而且不同的 View 也不可以复用同一个实例否则导致状态显示不能差异化。
方向明了后,我们只要按这个思路实现就能解决这个问题。下面有 2 个方案:
- 抛弃 Glide 框架,自己管理 WebP 资源的加载和执行控制。
- 还想用 Glide 框架,那么我们要想办法把 Glide 的内存缓存清掉,让 Glide 每次都重新创建。
解决方法 1 快速简洁但不推荐
每次设置 WebP 显示前直接调用 Glide.get(this.context).clearMemory() 清除所有内存缓存,这种方法简单快捷,缺点就是会误伤其他非 WebP 资源导致一起清空。
不过你也不用那么担心,他的这个整体也没那么不堪和呆板。Glide 内部的缓存会分为 active 区和普通区,active 就是当前资源绑定了 view 正在显示中的,调用 Glide.get(this.context).clearMemory() 的时候无论如何也不会清掉 active 这部分的缓存的。
如果项目需要应急,没太多时间修复的话,可以临时先用这个方法顶一顶。
解决方法 2 引入其他解码库来代替原生 AnimatedImageDrawable
引入库 com.github.zjupure:webpdecoder:2.6.4.16.0 并开启 Glide 的 skipMemoryCache 选项。
因为该库使用的自己的 api 进行存储的 WebP 内存资源。所以不存在这个问题。
那么读者可能会问,直接不引入这个 Module 库,直接使用
skipMemoryCache不行么?那这还真不行,skipMemoryCache会和AnimatedImageDrawable产生冲突,导致必然崩溃。
这个方法我认为存在的缺点就是必须在项目中指定的位置单独调用
skipMemoryCache,不然这个skipMemoryCache会干扰到其他的图片缓存。如果你是封装的 Glide 的话,改动起来比较麻烦。并且这个库也会修改解析的 Drawable 类型为 com.bumptech.glide.integration.webp.decoder.WebpDrawable。并且简单查了下资料,AnimatedImageDrawable 的性能优势比 WebpDrawable 是巨大的。再就是如果是仅仅是单 View 显示的 WebP 文件加载出现的闪烁问题,那么在不是提前调用 stop 导致停在了中间的场景,完全没必要清除内存。可以在启动的时候复用重新复用也不会有闪烁问题。用第 3 种解决方案更灵活,不像当前方案在一开始就硬编码了强制不用缓存。
基于以上种种,我实际项目中没用这个方法。
Glide.with(this).load(resourceId).skipMemoryCache(true).into(this)
解决方法 3 最终解决
我们要想办法精准的在 Glide 的缓存中找到这个资源的缓存,只清除掉这一条,这样影响可以做到最小化。
但是!又要说但是了!Glide 的 API 并没有单独清理某一个资源的方法。那怎么弄。自定义 Module 吧!
Glide 为开发者开放了自定义 Module 的模块的功能,可以自己管理缓存的存储和释放,我这里遵循最小改动原则,存储结构还使用内置的规则。我们只在存储的时候过滤是否是 WebP 资源,是的话做存到列表中。做为以后清除他的时候做准备。
可能看到以下代码会有几处疑问,我提前解答一下吧。
问: 为什么要存储下来这个 key 列表? 答: 为了清除的时候能用对应的 key 去清除掉,否则你根本没办法根据你手里的 Resource id 名称来清除对应缓存。
问: 为什么要用
resource.toString().contains("AnimatedImageDrawableResource")这种方式来判断是否是 WebP 资源? 答: 只能这么判断,系统没开放更多的 API。
问: 为什么删除类检索 key 的时候要倒着遍历? 答: 因为要边遍历,边删除元素,正着遍历不是漏掉就是数组越界异常!
自定义 GlideModule 类:
@GlideModule
class GlideCacheModule : AppGlideModule() {
companion object {
lateinit var glideMemoryCache: MemoryCache
private val glideMemoryCacheKeyList: MutableList<Key> = mutableListOf()
}
override fun applyOptions(context: Context, builder: GlideBuilder) {
super.applyOptions(context, builder)
// 步骤 1:按 Glide 默认规则计算内存缓存大小(和 Glide 原生逻辑一致)
val calculator = MemorySizeCalculator.Builder(context).build()
val defaultMemoryCacheSize = calculator.memoryCacheSize
// 步骤 2:创建 MemoryCache 实例(LruResourceCache 是 Glide 默认的内存缓存实现)
val memoryCache = LruResourceCache(defaultMemoryCacheSize.toLong())
// 步骤 3:保存实例到全局变量(供后续移除缓存使用)
glideMemoryCache = object : MemoryCache {
override fun getCurrentSize(): Long = memoryCache.currentSize
override fun getMaxSize(): Long = memoryCache.maxSize
override fun setSizeMultiplier(multiplier: Float) = memoryCache.setSizeMultiplier(multiplier)
override fun remove(key: Key): Resource<*>? {
glideMemoryCacheKeyList.remove(key)
return memoryCache.remove(key)
}
override fun put(key: Key, resource: Resource<*>?): Resource<*>? {
if (resource.toString().contains("AnimatedImageDrawableResource")) {
// 只缓存 AnimatedImageDrawableResource
glideMemoryCacheKeyList.add(key)
}
return memoryCache.put(key, resource)
}
override fun setResourceRemovedListener(listener: MemoryCache.ResourceRemovedListener) = memoryCache.setResourceRemovedListener(listener)
override fun clearMemory() = memoryCache.clearMemory()
override fun trimMemory(level: Int) = memoryCache.trimMemory(level)
}
// 步骤 4:将自定义的 MemoryCache 设置给 Glide
builder.setMemoryCache(glideMemoryCache)
// 可选:自定义内存缓存大小(比如设置为 20MB)
// val customCacheSize = 20 * 1024 * 1024L
// val customMemoryCache = LruResourceCache(customCacheSize)
// glideMemoryCache = customMemoryCache
// builder.setMemoryCache(customMemoryCache)
}
// 禁用 Manifest 解析,避免冲突(必写)
override fun isManifestParsingEnabled(): Boolean = false
}
自定义删除工具类:
object GlideMemoryCacheRemover {
/**
* 移除指定 drawable 的内存缓存
*/
fun removeDrawableMemoryCache(resId: Int) {
// 2. 使用全局持有的 MemoryCache
for (i in GlideCacheModule.glideMemoryCacheKeyList.size - 1 downTo 0) {
val key = GlideCacheModule.glideMemoryCacheKeyList[i]
if (key.toString().contains(resId.toString())) {
GlideCacheModule.glideMemoryCache.remove(key)
}
}
}
}
调用删除:
mResourcesId?.let { GlideMemoryCacheRemover.removeDrawableMemoryCache(it) }

