前言
很高兴见到你!
最近回归android业务开发,开发了如下图的视频剪辑时刻轴(图源:剪映):
关于时刻轴上的缩略图,需求去解码器加载获取。若每次都去解码器获取,会导致缩略图加载卡顿,无法满意功能需求,因而这儿需求对缩略图进行缓存来提高加载效率。那么其间的缓存战略,便是一个值得咱们考虑的关键点。
这篇文章来介绍这个缩略图缓存的考虑与设计的过程,希望能够对你有所帮助。
背景
视频时刻轴,我运用的是RecyclerView来完成,其间,每个Item为一个ImageView,即一个缩略图。如下图所示:
缩略图运用时刻戳在解码器中进行定位获取。例如一个10s的视频,需求显现10个缩略图,则每个缩略图对应的视频时刻方位是0s、1s、2s…9s。
缩略图加载的时机是:
- 当咱们滑动时刻轴时,一个item从不可见到可见,该item的
onBindViewHolder()
办法会被调用,则对应时刻的缩略图会被加载一次。 - 当咱们调用RecyclerView的
notifyDataChanged()
时,屏幕上显现的一切缩略图,都会被从头加载一次。
在咱们的项目中,缩略图解码器的功能比较差。举个比如,一个缩略图的加载耗时,可能是1s。假定咱们不运用缓存,那么咱们每次滑动到一个新的方位,都需求等候好几秒,缩略图才能彻底显现完毕,这个体会是非常差的。
优化的办法,其间最直接的,是下降获取缩略图接口的耗时,例如从1s下降到0.1s,但这个优化关于负责这个模块的同事来说是一个巨大的应战。且假定缩短到0.1s,其耗时仍旧是不可承受的。
第二个优化办法,便是咱们自己想办法,也便是今天我要聊的:增加缩略图缓存。增加了缓存之后,首次加载运用多媒体接口,仍旧很慢。可是加载一次之后,呼应延迟就能够到达无感了。
那么,这个缓存战略咱们该怎么设计?
内存缓存
首先最直接的方法,是增加内存缓存,说人话便是,运用一个HashMap,来缓存从解码器中获取的缩略图Bitmap。如下:
public class ThumbnailKey {
int position = 0;
String videoPath = "";
@Override
public boolean equals(Object obj) {...}
@Override
public int hashCode() {...}
}
HashMap<ThumbnailKey,Bitmap> mThumbnailMap;
- 我这儿创立了ThumbnailKey类表明一个缩略图:视频+时刻戳。由于咱们需求运用其作为HashMap的Key,所以要重写
hashcode()
和equals()
办法。 - 创立HashMap对象来存储缩略图
这样,每次加载缩略图的时分,把结果存储在HashMap中,下次恳求就直接从Map中去获取即可。Map中没有,再去解码器中加载。
但这儿咱们很容易发现一个问题:内存暴涨。
假如缓存没有上限且视频比较长,那么缓存的bitmap内存占用会非常巨大。终究导致软件功能下降、乃至可能OOM。因而咱们不能无限制地缓存缩略图,有必要设置一个上限。这儿咱们结合LRU淘汰规则,运用LRUCache来替代HashMap,就能够很好地解决这个问题。LRUCache是android供给的一个官方库,内部运用的是LinkedHashMap,咱们直接运用即可。
假如仅运用内存缓存,在长距离滑动超出内存缓存的规模时,依然需求从解码器中从头加载。且由于内存的宝贵,上限无法设置地太大。
因而,这儿咱们需求引入另一个速度稍慢,可是量管够的缓存磁盘缓存。
磁盘缓存
磁盘缓存的速度虽然比内存缓存慢许多,但关于解码器的解码速度也是降维打击了。磁盘缓存在读取一张缩略图的耗时是毫米等级的,平均耗时3ms。加载完成一屏幕的缩略图,假定6张,只需求18ms,这个耗时是彻底满意需求的。
磁盘比较内存还有另一个好处,便是量大管饱。一张缩略图对应的bitmap巨细大概是150k。一秒钟一个缩略图,一个10分钟的视频,一切缩略图的巨细大概是88Mb。这个内存占用相关于磁盘来说都是小ks。作为对比,小而美的国民APP微信,磁盘占用都是以G为单位。
因而,咱们彻底能够将一切缩略图缓存在磁盘中,完成整个视频不论怎么滑动,都能完成无感零延迟加载缩略图,且对应的内存开支,是可承受的。
这么看来,咱们几乎只需求运用磁盘缓存就满意需求了,那岂不是能够直接撤销内存缓存,还能减少内存占用?
这个逻辑没有问题,可是咱们疏忽了RecyclerView的一个改写特性。当咱们调用notifyDataChanged()
的时分,会改写当时显现的一切缩略图。那么在一些需求频频调用notifyDataChanged()
的场景,例如拖动剪辑视频的时分,会不断地去改写缩略图,频率乃至可能是毫秒级的。磁盘缓存虽然很快,可是在他仍旧是一个耗时操作,和内存缓存比较,仍旧很慢。其次,当咱们高频地左右滑动时刻轴,那么显现之外的的缩略图也会被频频加载。
磁盘读取文件自身是一个耗时的IO操作,高频地进行IO操作,也会下降咱们程序的功能。因而这儿咱们需求结合内存缓存一起来运用。
咱们内存缓存,主要解决的场景,是时刻轴频频改写、以及左右快速来回拖动导致的缩略图频频恳求。那么咱们确定内存缓存的上限,为一个屏幕上能显现的缩略图数量的三倍即可。用较少的内存占用,完成较好的体现效果。
ok,到这儿咱们回忆一下咱们全体的缓存战略:
- 当发起缩略图恳求时,优先判断是否有内存缓存。
- 没有内存缓存,则判断是否有磁盘缓存。若存在磁盘缓存则经过IO去加载缩略图,并将结果缓存到内存中。
- 没有磁盘缓存,则需求经过解码器去获取缩略图,并将结果缓存到内存和磁盘中。
需求注意的是,每次完成编辑后,需求同步删去一切的磁盘缓存。不然随着时刻的推移,咱们的app磁盘内存占用,也要向小而美app靠近了。
咱们理论剖析到这,那么这个缓存战略不就ok了吗?假如仅仅是局限于剖析,这个大框架战略,确实没缺点。但在真正落实到完成时,会发现有一些优化的点还需求咱们重视,并且会很大程度上,影响咱们的功能体现。
结合具体场景优化
1. 异步恳求使命去重
从咱们上面全体缓存战略来说,每一张缩略图只需求经过解码器加载一次,然后存储在磁盘与内存中即可,也便是理论上,只要首次加载是非常耗时的。
但在实践开发中,RecyclerView的notifyDataChanged()
很有可能频频触发,导致解码器获取缩略图的时分,累积了许多相同的恳求。
比如当时正在显现的缩略图如下:
当咱们正在瞬间改写三次的时分,就会累积12个缩略图恳求,其间6个是重复的。此时解码器就会把功能,浪费在一些无意义的缩略图恳求上。因而咱们需求对这6个重复的缩略图进行去重,让同一个缩略图,仅会经过解码器加载一次。
而磁盘加载也是同理,毕竟也是一个耗时的异步操作,也是需求进行缩略图恳求使命去重处理。
上面两种异步恳求后的缩略图数据,放到内存缓存中就无需求进行去重处理了。
去重的方法许多,例如运用HashSet等。
2. 优先加载当时正在显现的缩略图、预加载缩略图
在体会上,咱们还能够做一些优化。
例如解码器恳求队列改为恳求栈,优先处理当时屏幕上显现的缩略图恳求,这样就能够更快看到缩略图显现。
例如能够将一切缩略图在进入剪辑页面的时分全部放入恳求栈中,提前预加载缩略图。滑动时,再将新的缩略图恳求从栈中提升到栈顶,优先加载当时显现的缩略图。
这些优化战略还有许多细节能够说,这儿就不详细展开了。
最终
最终咱们再来回忆一下全体的缓存战略,如下图:
比较上个流程图,增加了使命去重以及恳求栈结构。
整个优化战略看下来有没有一丢丢眼熟,是不是操作系统的多级缓存很像?咱们学习的一些基础知识、思想、战略等,有时分看着很高大上,但在实践使用中,仍是有很大的帮助。
其次更重要的一点是,关于战略的剖析到计划的落地,中间还存在许多的问题。计划考虑剖析仅仅只是全体的框架,将这套框架使用到具体的项目中,要完善许多的细节。
全文到此,原创不易,觉得有帮助能够点赞收藏谈论转发。 有任何想法欢迎谈论区沟通指正。 如需转载请谈论区或私信沟通。 另外欢迎光临笔者的个人博客:传送门