16、RenderNode与DisplayList:View绘制缓存、RenderNode属性更新、脏区域计算
聊到Android图形系统,有个绕不开的话题——RenderNode和DisplayList。
说实话,我早年做性能优化时,对这块也是一知半解。直到有一次,一个列表滑动卡顿的问题折腾了我整整三天。最后发现,问题就出在RenderNode的脏区域计算上。嗯,从那以后,我再也不敢小看这两个概念了。
16.1 什么是RenderNode?
简单说,RenderNode是Android 4.0引入的一个核心概念。它代表了一个绘制节点的“渲染快照”。
每个View在硬件加速模式下,都会对应一个RenderNode。这个节点里存了什么?
- View的绘制指令(DisplayList)
- View的属性状态(位置、大小、透明度、旋转等)
- 脏区域标记(哪些部分需要重绘)
你想想看,如果没有RenderNode,每次View属性变化,整个View都得重新走一遍onDraw。有了它,GPU可以直接复用之前缓存的绘制指令,只更新变化的部分。
核心观点:RenderNode是硬件加速的基石。它把View的绘制逻辑和渲染逻辑解耦了。
16.2 DisplayList:绘制指令的缓存
DisplayList,说白了就是一组绘制命令的集合。比如“画一个矩形”、“画一段文字”、“贴一张图片”。
在软件绘制时代,View每次重绘都要重新生成这些命令。但在硬件加速模式下,DisplayList会被缓存起来。
我习惯把DisplayList比作“乐高说明书”。你第一次拼好一个模型(生成DisplayList),之后想换个颜色(更新属性),不需要重新拼,只需要改说明书上的颜色标注就行。
// 伪代码:DisplayList的生成过程
class View {
RenderNode renderNode;
void draw(Canvas canvas) {
// 硬件加速模式下,canvas实际是RecordingCanvas
if (canvas.isHardwareAccelerated()) {
RecordingCanvas recordingCanvas = (RecordingCanvas) canvas;
// 开始录制绘制指令
recordingCanvas.startRecording(renderNode);
// 执行onDraw,所有drawXXX调用都会被记录
onDraw(recordingCanvas);
// 结束录制,生成DisplayList
recordingCanvas.finishRecording();
}
}
}
小提示:DisplayList的缓存是分层的。每个ViewGroup都有自己的DisplayList,子View的DisplayList会被嵌套进去。这样,某个子View变化时,只需要更新它自己的DisplayList,父容器不用动。
16.3 RenderNode属性更新:轻量级操作
为什么说RenderNode属性更新是轻量级的?
因为很多属性变化,根本不需要重新生成DisplayList。比如:
| 属性 | 更新方式 | 是否触发onDraw |
|---|---|---|
| translationX/Y | 直接修改RenderNode的transform矩阵 | 否 |
| scaleX/Y | 直接修改RenderNode的transform矩阵 | 否 |
| alpha | 直接修改RenderNode的alpha值 | 否 |
| rotation | 直接修改RenderNode的transform矩阵 | 否 |
| backgroundColor | 需要更新DisplayList中的绘制命令 | 是 |
| text内容 | 需要更新DisplayList中的绘制命令 | 是 |
我曾经在项目中遇到过一个问题:一个动画每帧都在修改View的translationX,但性能却很差。后来发现,是因为我在动画监听器里调用了invalidate(),导致每次属性变化都强制重绘。去掉invalidate()后,动画流畅得像丝一样。
避坑指南:我曾经见过不少开发者,在属性动画的监听器里习惯性调用invalidate()。其实,对于translationX、alpha这类属性,系统会自动标记RenderNode为“属性已更新”,根本不需要你手动触发重绘。多此一举反而会破坏性能。
16.4 脏区域计算:只重绘需要重绘的部分
脏区域(Dirty Region)这个概念,说白了就是“哪些地方变了,需要重新渲染”。
在硬件加速模式下,脏区域的计算非常精细。系统会做以下几件事:
- 标记脏节点:当View的属性或内容变化时,系统会标记对应的RenderNode为“脏”。
- 合并脏区域:如果有多个脏节点,系统会合并它们的脏区域,避免重复渲染。
- 裁剪渲染范围:GPU只渲染脏区域内的像素,其他部分直接复用上一帧的缓存。
你想想看,如果一个列表只有第10个Item的文本变了,系统会只重绘那个Item的区域,而不是整个列表。这就是脏区域计算的威力。
关键点:脏区域计算是在CPU上完成的。它决定了GPU接下来要渲染哪些像素。如果脏区域计算本身很耗时,那性能瓶颈就从GPU转移到了CPU。
16.5 知识体系:RenderNode与DisplayList的核心逻辑
下面这张图,是我自己总结的RenderNode与DisplayList的工作流程。你看完应该能对整个机制有个直观的理解。
16.6 实战:如何利用RenderNode优化性能
知道了原理,怎么落地?我分享几个实战经验。
16.6.1 避免频繁触发DisplayList重建
DisplayList重建的成本很高。因为它要重新录制所有绘制指令。所以,能通过属性动画解决的问题,就别用自定义绘制。
// ❌ 错误做法:每次属性变化都触发onDraw
view.animate().translationX(100f).withEndAction(new Runnable() {
@Override
public void run() {
view.invalidate(); // 多此一举
}
});
// ✅ 正确做法:让系统自动处理属性更新
view.animate().translationX(100f).start();
16.6.2 合理使用clipRect减少脏区域
在自定义View中,如果你知道绘制范围有限,可以用clipRect告诉系统“我只画这个区域”。这样脏区域计算会更精确。
@Override
protected void onDraw(Canvas canvas) {
// 告诉系统:我只画这个矩形区域
canvas.clipRect(0, 0, 100, 100);
// 后续的绘制操作
canvas.drawBitmap(bitmap, 0, 0, null);
}
小技巧:我习惯在复杂的自定义View中,用canvas.quickReject()提前判断某个区域是否在脏区域内。如果不在,直接跳过绘制。这个API在硬件加速模式下非常高效。
16.6.3 监控RenderNode的脏标记
你可以通过adb shell dumpsys gfxinfo来查看每个View的RenderNode状态。重点关注dirty标记的数量。
adb shell dumpsys gfxinfo <package_name> | grep "RenderNode"
如果某个View的RenderNode频繁被标记为脏,说明它可能在不必要的时候触发了重绘。这时候就该去检查它的invalidate()调用链了。
避坑指南:我曾经遇到过一个案例,一个TextView因为设置了setText(),每次都会触发DisplayList重建。但其实文本内容根本没变。后来加了个if (!text.equals(currentText))的判断,性能直接提升了一个档次。
16.7 总结
RenderNode和DisplayList,是Android硬件加速的基石。理解它们,你才能真正掌握View绘制的性能优化。
记住三个核心点:
- RenderNode:View的渲染快照,负责管理属性和脏标记
- DisplayList:绘制指令的缓存,避免重复生成
- 脏区域计算:只重绘变化的部分,减少GPU工作量
嗯,这一章的内容就到这里。希望你能把这些知识用到实际项目中,少踩一些我当年踩过的坑。
公众号:蓝海资料掘金营,微信deep3321