21、View的缓存机制:DrawingCache与硬件缓存、LayerType的选择

说到View的缓存,很多人第一反应就是「哦,那个getDrawingCache()」。但说实话,这个API在Android 4.0之后基本就属于「历史遗留产物」了。我刚开始做Android开发那会儿,还经常用它来做ListView的优化,后来发现坑越来越多。

今天咱们就把View的缓存机制彻底聊透。从最古老的DrawingCache,到硬件加速层的缓存,再到LayerType的三种模式选择。这些东西搞明白了,你写复杂UI时心里就有底了。

21.1 DrawingCache:曾经的「性能救星」

先说说DrawingCache。它的原理其实很简单——把View的内容渲染到一个Bitmap上,下次绘制时直接贴图,不用再走一遍onDraw。

核心流程:

  • 调用 view.setDrawingCacheEnabled(true) 开启缓存
  • 系统在 dispatchDraw() 结束后,把绘制结果保存到Bitmap
  • 后续绘制时,直接drawBitmap,跳过onDraw
// 经典用法(现在不推荐了)
view.setDrawingCacheEnabled(true);
Bitmap cache = view.getDrawingCache();
// 用完后记得释放
cache.recycle();
view.setDrawingCacheEnabled(false);

但这里有个大坑——内存开销。一个1000x1000的View,缓存Bitmap就要占4MB(ARGB_8888)。我当年在一个平板项目上,就因为滥用DrawingCache,直接OOM了。嗯,那会儿还没学会用buildDrawingCache(false)来指定565格式。

注意:从Android 3.0开始,硬件加速默认开启后,DrawingCache的命中率其实很低。因为硬件加速层有自己的缓存机制,两者会互相干扰。我个人建议:新项目就别用DrawingCache了,除非你要兼容2.x的老设备。

21.2 硬件加速下的缓存:RenderNode与DisplayList

硬件加速时代,View的缓存机制完全变了。每个View对应一个RenderNode,里面存着它的DisplayList——说白了就是一系列绘制指令的列表。

你想想看,第一次绘制时,CPU把onDraw里的操作(画圆、画文字、画图片)全部记录成指令,存到DisplayList里。下次重绘时,只要View没变,直接复用这些指令就行,不用再执行onDraw。

硬件缓存的关键点:

  • DisplayList缓存的是绘制指令,不是Bitmap
  • View的invalidate()只会标记DisplayList为「脏」,不会立即重建
  • 只有onDraw()里的内容变了,才需要重建DisplayList

我在项目中遇到过一个问题:一个自定义View,每次调用invalidate()都会触发onDraw,但onDraw里只是改了个颜色值。其实这种情况下,DisplayList只需要更新颜色属性,不需要重建整个列表。但如果你在onDraw里new了Paint对象...那每次都得重建,缓存就白搭了。

21.3 LayerType:三种模式的选择

LayerType是控制View缓存行为的核心API。它有三种取值:LAYER_TYPE_NONELAYER_TYPE_SOFTWARELAYER_TYPE_HARDWARE

类型 缓存方式 适用场景 内存开销
NONE 不缓存,每次直接绘制 普通View,无特殊需求
SOFTWARE 缓存到Bitmap(软件渲染) 复杂动画、模糊效果、不支持硬件加速的操作 较高(Bitmap内存)
HARDWARE 缓存到GPU纹理(硬件加速) 频繁重绘、动画、3D变换 较低(GPU纹理)

LAYER_TYPE_NONE:默认行为

这是View的默认值。每次绘制都走完整的onDraw流程。如果你的View很简单,比如就画个纯色背景,那用NONE完全没问题。但如果你在onDraw里做了大量计算...那每次重绘都是灾难。

LAYER_TYPE_SOFTWARE:软件层缓存

这个模式会把View渲染到一个离屏Bitmap上。我经常用它来做复杂静态内容的缓存。比如一个包含大量文字和图形的自定义View,内容不变但需要频繁重绘(比如被上层View遮挡后恢复)。

// 开启软件层缓存
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

// 也可以指定一个Paint,用于控制缓存Bitmap的绘制参数
Paint paint = new Paint();
paint.setFilterBitmap(true);
view.setLayerType(View.LAYER_TYPE_SOFTWARE, paint);

我的经验:软件层缓存有个隐藏问题——它不支持invalidate()的局部更新。一旦你调用了invalidate,整个Bitmap都会重建。所以如果你只是改了一小块区域,用SOFTWARE反而更慢。我曾经在一个地图控件上踩过这个坑,后来改成HARDWARE才解决。

LAYER_TYPE_HARDWARE:硬件层缓存

这是我最常用的模式。它会创建一个GPU纹理来缓存View的渲染结果。好处是:

  • 重绘时直接贴纹理,速度极快
  • 支持View.setAlpha()setTranslationX()等属性动画,不会触发重绘
  • 内存开销比SOFTWARE小(GPU纹理压缩)
// 开启硬件层缓存
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);

// 配合属性动画使用,性能最佳
view.animate()
    .alpha(0.5f)
    .translationX(100f)
    .setDuration(300)
    .start();

注意:硬件层缓存不是万能的。如果你的View在onDraw里使用了Canvas.clipPath()drawPicture()等不支持硬件加速的API,系统会自动降级为软件渲染。这时候你设HARDWARE也没用,反而多了一层纹理拷贝的开销。

21.4 缓存失效与更新:什么时候重建?

缓存不是永久的。系统有一套完整的失效机制:

  1. invalidate():标记View为「脏」,下次绘制时重建DisplayList或Bitmap
  2. invalidate(Rect):局部失效,只重建指定区域(仅对NONE模式有效)
  3. requestLayout():布局变化,整个View树重新测量布局,缓存全部失效
  4. 属性变化:比如setBackground()、setPadding()等,会触发对应属性的缓存失效

这里有个细节很多人不知道——硬件层缓存不会因为invalidate()立即重建。系统会等到下一个Vsync信号到来时,统一处理所有脏标记。这样做的好处是:如果你在一帧里连续调了10次invalidate,最终只会重建一次。

最佳实践总结:

  • 静态复杂内容 + 频繁重绘 → 用HARDWARE层缓存
  • 不支持硬件加速的API + 需要缓存 → 用SOFTWARE层缓存
  • 简单View或内容频繁变化 → 用NONE,别缓存
  • 属性动画 → 配合HARDWARE层缓存,性能翻倍

我曾经接手过一个项目,里面有个自定义的「仪表盘」View,每秒钟要重绘60次。原来的写法是每次onDraw里重新计算所有刻度线和数字,CPU占用率直接飙到80%。后来我加了setLayerType(HARDWARE),然后把刻度线的计算放到onSizeChanged()里只算一次。结果CPU占用降到了15%,帧率从40fps直接拉满到60fps。

嗯,这就是缓存的魅力。用对了地方,效果立竿见影。

View缓存机制决策流程 View需要缓存吗? 是否频繁重绘(如动画、滚动)? 否 → 使用 NONE 是 → 选择缓存类型 LAYER_TYPE_NONE 每次重绘走完整onDraw 适合简单、变化频繁的View 是否使用硬件加速API? LAYER_TYPE_SOFTWARE 缓存到Bitmap,兼容所有API LAYER_TYPE_HARDWARE 缓存到GPU纹理,性能最佳 注:如果View使用了clipPath等不支持硬件加速的API,HARDWARE会自动降级

最后说一句:缓存不是银弹。用错了地方,反而会浪费内存、降低性能。我的建议是——先用Profile工具(比如Systrace、GPU渲染分析)找到真正的性能瓶颈,再决定要不要加缓存。别一上来就setLayerType,那样跟「头痛医头」没啥区别。


公众号:蓝海资料掘金营,微信deep3321