聊一聊关于视频缩略图缓存策略

作者:一只修仙的猿

最近回归android业务开发,开发了如下图的视频剪辑时间轴(图源:剪映):

对于时间轴上的缩略图,需要去解码器加载获取。若每次都去解码器获取,会导致缩略图加载卡顿,无法满足性能需求,因此这里需要对缩略图进行缓存来提高加载效率。那么其中的缓存策略,就是一个值得我们思考的关键点。

这篇文章来介绍这个缩略图缓存的思考与设计的过程,希望能够对你有所帮助。

背景

视频时间轴,我使用的是RecyclerView来实现,其中,每个Item为一个ImageView,即一个缩略图。如下图所示:

缩略图使用时间戳在解码器中进行定位获取。例如一个10s的视频,需要显示10个缩略图,则每个缩略图对应的视频时间位置是0s、1s、2s...9s。

缩略图加载的时机是:

  1. 当我们滑动时间轴时,一个item从不可见到可见,该item的onBindViewHolder()方法会被调用,则对应时间的缩略图会被加载一次。
  2. 当我们调用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;
  1. 我这里创建了ThumbnailKey类表示一个缩略图:视频+时间戳。由于我们需要使用其作为HashMap的Key,所以要重写hashcode()equals()方法。
  2. 创建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,到这里我们回顾一下我们整体的缓存策略:

  1. 当发起缩略图请求时,优先判断是否有内存缓存。
  2. 没有内存缓存,则判断是否有磁盘缓存。若存在磁盘缓存则通过IO去加载缩略图,并将结果缓存到内存中。
  3. 没有磁盘缓存,则需要通过解码器去获取缩略图,并将结果缓存到内存和磁盘中。

需要注意的是,每次完成编辑后,需要同步删除所有的磁盘缓存。否则随着时间的推移,我们的app磁盘内存占用,也要向小而美app靠近了。

我们理论分析到这,那么这个缓存策略不就ok了吗?如果仅仅是局限于分析,这个大框架策略,确实没毛病。但在真正落实到实现时,会发现有一些优化的点还需要我们关注,而且会很大程度上,影响我们的性能表现。

结合具体场景优化

1. 异步请求任务去重

从我们上面整体缓存策略来说,每一张缩略图只需要通过解码器加载一次,然后存储在磁盘与内存中即可,也就是理论上,只有首次加载是非常耗时的。

但在实际开发中,RecyclerView的notifyDataChanged()很有可能频繁触发,导致解码器获取缩略图的时候,累积了很多相同的请求。

比如当前正在显示的缩略图如下:

当我们正在瞬间刷新三次的时候,就会累积12个缩略图请求,其中6个是重复的。此时解码器就会把性能,浪费在一些无意义的缩略图请求上。因此我们需要对这6个重复的缩略图进行去重,让同一个缩略图,仅会通过解码器加载一次。

而磁盘加载也是同理,毕竟也是一个耗时的异步操作,也是需要进行缩略图请求任务去重处理。

上面两种异步请求后的缩略图数据,放到内存缓存中就无需要进行去重处理了。

去重的方式很多,例如使用HashSet等。

2. 优先加载当前正在显示的缩略图、预加载缩略图

在体验上,我们还可以做一些优化。

例如解码器请求队列改为请求栈,优先处理当前屏幕上显示的缩略图请求,这样就可以更快看到缩略图显示。

例如可以将所有缩略图在进入剪辑页面的时候全部放入请求栈中,提前预加载缩略图。滑动时,再将新的缩略图请求从栈中提升到栈顶,优先加载当前显示的缩略图。

这些优化策略还有很多细节可以说,这里就不详细展开了。

最后

最后我们再来回顾一下整体的缓存策略,如下图:

相比上个流程图,增加了任务去重以及请求栈结构。

整个优化策略看下来有没有一丢丢眼熟,是不是操作系统的多级缓存很像?我们学习的一些基础知识、思想、策略等,有时候看着很高大上,但在实际应用中,还是有很大的帮助。

其次更重要的一点是,对于策略的分析到方案的落地,中间还存在很多的问题。方案思考分析仅仅只是整体的框架,将这套框架应用到具体的项目中,要完善很多的细节。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容