Glide播放webp动画的一些坑
问题现象
使用Glide图片加载框架加载webp的时候默认会将一个资源加载一份然后缓存起来,之后引用相同资源id会始终返回这同一个缓存。这本身是一个很常见的优化手段,但是遇到Android原生的AnimatedImageDrawable就会有问题。因为Glide内部如果不做自定义Module的话,默认加载的webp图片就是使用的AnimatedImageDrawable类。
- 如果有多个view通过Glide显示同一个webp资源,会导致播放进度强制一致。
其实我是先开始start播放上面的ImageView,然后再将图片设置到了下面的ImageView。结果后start的开始时机被强制绑定到了和正在播放的一起。
- 任何一个调用了停止其他的也会跟随停止。
这里我调用Glide.with(this).clear(imageView2);将第二个ImageView播放停止并清楚不显示,导致第一个ImageView也跟着停止了播放。
- 同一个view需要隐藏后再重头播放会导致开始时候闪现一下停止时的那一帧的问题。
这里我将同一个资源在上面ImageView停止后设置给了第二个ImageView,仔细观察会发现,第二个ImageView播放前会闪烁一下停止时候的那一帧。这里只是为了方便演示,即使开始和停止在同一个ImageView发生也同样有这个问题。
如果觉得git图看不清的可以看视频,视频的帧率更高一下,对于第3个现象看的更明显
my_screen_record
解决问题的思路
我在开发中主要遇到的是重新播放闪现停止帧的问题,我第一时间是想到想办法如何避免这个问题。
在停止的时候释放掉这个播放进度的状态,回归为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类:
@com.bumptech.glide.annotation.GlideModule class GlideCacheModule :AppGlideModule(){// 全局持有MemoryCache实例(核心:替代之前的初始化器方式)companionobject{lateinitvar glideMemoryCache: MemoryCache privatesetval glideMemoryCacheKeyList: MutableList<Key>=mutableListOf()}overridefunapplyOptions(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{overridefungetCurrentSize(): Long {return memoryCache.currentSize }overridefungetMaxSize(): Long {return memoryCache.maxSize }overridefunsetSizeMultiplier(multiplier: Float){ memoryCache.setSizeMultiplier(multiplier)}overridefunremove(key: Key): Resource<*>?{ glideMemoryCacheKeyList.remove(key)return memoryCache.remove(key)}overridefunput( key: Key, resource: Resource<*>?): Resource<*>?{if(resource.toString().contains("AnimatedImageDrawableResource")){// 只缓存 AnimatedImageDrawableResource glideMemoryCacheKeyList.add(key)}return memoryCache.put(key, resource)}overridefunsetResourceRemovedListener(listener: MemoryCache.ResourceRemovedListener){ memoryCache.setResourceRemovedListener(listener)}overridefunclearMemory(){ memoryCache.clearMemory()}overridefuntrimMemory(level: Int){ memoryCache.trimMemory(level)}}// 步骤4:将自定义的MemoryCache设置给Glide(替代setMemoryCacheInitializer) builder.setMemoryCache(glideMemoryCache)// 可选:自定义内存缓存大小(比如设置为20MB)// val customCacheSize = 20 * 1024 * 1024L // 20MB// val customMemoryCache = LruResourceCache(customCacheSize)// glideMemoryCache = customMemoryCache// builder.setMemoryCache(customMemoryCache)}// 禁用Manifest解析,避免冲突(必写)overridefunisManifestParsingEnabled(): Boolean {returnfalse}}自定义删除工具类:
object GlideMemoryCacheRemover {/** * 移除指定drawable的内存缓存 */funremoveDrawableMemoryCache(resId: Int){// 2. 使用全局持有的MemoryCachefor(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)}