16、RenderNode与DisplayList:View绘制缓存、RenderNode属性更新、脏区域计算

聊到Android图形系统,有个绕不开的话题——RenderNodeDisplayList

说实话,我早年做性能优化时,对这块也是一知半解。直到有一次,一个列表滑动卡顿的问题折腾了我整整三天。最后发现,问题就出在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)这个概念,说白了就是“哪些地方变了,需要重新渲染”。

在硬件加速模式下,脏区域的计算非常精细。系统会做以下几件事:

  1. 标记脏节点:当View的属性或内容变化时,系统会标记对应的RenderNode为“脏”。
  2. 合并脏区域:如果有多个脏节点,系统会合并它们的脏区域,避免重复渲染。
  3. 裁剪渲染范围:GPU只渲染脏区域内的像素,其他部分直接复用上一帧的缓存。

你想想看,如果一个列表只有第10个Item的文本变了,系统会只重绘那个Item的区域,而不是整个列表。这就是脏区域计算的威力。

关键点:脏区域计算是在CPU上完成的。它决定了GPU接下来要渲染哪些像素。如果脏区域计算本身很耗时,那性能瓶颈就从GPU转移到了CPU。

16.5 知识体系:RenderNode与DisplayList的核心逻辑

下面这张图,是我自己总结的RenderNode与DisplayList的工作流程。你看完应该能对整个机制有个直观的理解。

RenderNode与DisplayList工作流程 View onDraw() / 属性变化 属性更新 RenderNode 属性矩阵 / 脏标记 脏区域 DisplayList 绘制指令缓存 属性变化(translationX等) 直接修改RenderNode属性 内容变化(text等) 重新生成DisplayList GPU渲染 只渲染脏区域内的像素 View RenderNode DisplayList GPU渲染

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