Skip to content

Stage05|Performance ​

Lesson062|Widget、Layout、ScrollView 的刷新与遍历成本 ​

Tags: #Creator2.x #Stage05 #Widget #Layout #ScrollView #UIDifficulty: ⭐⭐⭐⭐☆


真实问题:只改一条聊天消息,为什么整个列表重新排版 ​

聊天 Content 使用 Vertical Layout,消息 Label 高度自适应。新消息加入后:

text
Label 文本计算高度
→ Item 尺寸变化
→ Layout 遍历所有子节点
→ Content 高度变化
→ ScrollView 边界更新
→ Widget 对齐相关节点

一次局部数据变化可以传播成整棵 UI 的布局工作。

一、本课目标 ​

UI 卡顿经常不是 Button 或 Sprite 单独造成的,而是尺寸、位置和滚动变化触发了大量重算。本课重点:

  • Widget 和 Layout 的职责。
  • ScrollView 为什么容易在大量 Item 时掉帧。
  • 如何减少重复刷新和不可见区域工作。

二、Widget 和 Layout 的区别 ​

text
Widget:根据父节点或边界约束自身位置和尺寸
Layout:根据子节点集合计算排列和容器尺寸

它们都可能在属性改变后标记 dirty,并在后续阶段重新计算。


三、为什么一次修改会传播 ​

text
修改子节点尺寸
    ↓
父 Layout 需要重新排列
    ↓
兄弟节点位置变化
    ↓
容器尺寸变化
    ↓
上层 Widget 可能再次计算

如果循环中每改一个子节点就强制刷新,可能产生近似重复的 O(n²) 工作。


四、ScrollView 的成本 ​

一个简单 ScrollView 可能同时处理:

  • 内容节点 Transform。
  • 可视区域判断。
  • Item 激活和回收。
  • Layout 更新。
  • 触摸和滚动事件。
  • Mask 和裁剪。
  • Label 与图片刷新。

Item 数量增长后,最重要的优化通常是虚拟化或对象复用,而不是只调小图片。


五、只更新可见项 ​

text
数据总量:10,000
屏幕可见:20

理想策略是:

text
只创建 20~40 个 Item
滚动时复用 Item
根据索引更新内容

这样可以同时减少节点、组件、图片、Label、布局和事件成本。


六、批量更新 UI ​

不推荐:

ts
for (const item of items) {
    item.node.width = getWidth(item.data);
    item.forceLayout();
}

更合理的方向:

text
先写入所有数据
→ 暂停不必要的中间刷新
→ 一次性触发布局
→ 读取最终结果

具体 API 需要结合 Creator 2.x 的组件实现确认,原则是避免循环内交替写入和强制读取。


七、Widget 的使用边界 ​

Widget 适合屏幕适配和少量稳定约束,但大量节点全部挂 Widget 会增加遍历和计算。可以考虑:

  • 只在需要适配的边界节点使用。
  • 固定格式列表内部使用相对布局。
  • 避免多层 Widget 相互约束。
  • 不在每帧反复修改 Widget 依赖的父尺寸。

八、案例:聊天列表 ​

聊天列表每条消息高度不同,新增消息时可能触发:

text
Label 换行
→ Item 高度变化
→ Layout 重新排列所有消息
→ ScrollView 内容尺寸变化
→ 滚动位置修正

优化方向:

  • 估算高度并缓存。
  • 只保留可见消息节点。
  • 批量插入。
  • 避免每帧重新测量所有文本。
  • 列表底部追加时只更新受影响区域。

九、常见误区 ​

误区一:Layout 只是改变几个坐标 ​

它可能遍历子树并重新计算多个约束。

误区二:ScrollView 只要设置 active 就没有成本 ​

内容节点仍然存在时,布局、事件、资源和组件可能继续产生成本。

误区三:所有 UI 都使用 Widget 最安全 ​

约束越多,刷新传播越复杂。

误区四:只把不可见 Item alpha 设为 0 就完成虚拟化 ​

节点、组件、布局和资源仍然可能工作,应该根据需求禁用、回收或复用。


十、练习与答案 ​

  1. Widget 和 Layout 的职责有什么区别?
  2. 为什么循环内强制刷新布局可能很慢?
  3. 10,000 条数据只显示 20 条时,为什么要做虚拟列表?
  4. alpha=0 和回收 Item 的性能语义有什么不同?

答案:

  1. Widget 处理约束定位,Layout 处理子节点排列和容器尺寸。
  2. 每次修改都可能重新遍历和计算,形成重复传播。
  3. 降低节点、组件、布局、渲染和资源工作量。
  4. alpha=0 可能仍保留节点和组件成本,回收可以停止或复用更多工作。

布局系统最怕“读写反馈” ​

常见反馈链:

text
设置 Label.string
→ Label 修改 node.width/height
→ Layout 修改 child.position
→ Widget 根据父尺寸修改节点
→ 父 Layout 再次发现尺寸变化

若多个系统同时拥有同一尺寸/位置,可能一帧多轮刷新甚至抖动。UI 设计要明确每个属性的所有者。

Creator 2.4.x 实验:逐条添加与批量添加 ​

LayoutCostProbe.ts ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class LayoutCostProbe extends cc.Component {
    @property(cc.Prefab)
    itemPrefab: cc.Prefab = null;

    @property(cc.Node)
    content: cc.Node = null;

    @property(cc.Layout)
    layout: cc.Layout = null;

    addOneByOne(count: number) {
        const begin = Date.now();
        for (let i = 0; i < count; i++) {
            const node = cc.instantiate(this.itemPrefab);
            this.content.addChild(node);
            node.getComponentInChildren(cc.Label).string =
                'message ' + i;
        }
        cc.log('[layout] one-by-one submit=', Date.now() - begin);
    }

    addBatch(count: number) {
        const begin = Date.now();
        this.layout.enabled = false;
        for (let i = 0; i < count; i++) {
            const node = cc.instantiate(this.itemPrefab);
            this.content.addChild(node);
            node.getComponentInChildren(cc.Label).string =
                'message ' + i;
        }
        this.layout.enabled = true;
        this.layout.updateLayout();
        cc.log('[layout] batch submit=', Date.now() - begin);
    }
}

updateLayout 与启停行为以 Creator 2.4.x 当前组件 API 验证。先在实验场景确认视觉与生命周期,再用于业务。

对照 ​

  1. 添加 20、100、500 Item。
  2. 固定 Item 高度与 Label 自适应分别测试。
  3. 每个 Item 有 Widget 与无 Widget 对比。
  4. 全量列表与虚拟列表对比。
  5. 记录打开尖峰和滚动稳定帧。

ScrollView 虚拟化的核心不是对象池 ​

对象池减少创建/销毁;虚拟列表减少同时参与 UI 的节点数量。

text
总数据 10000 条
可见 12 条
缓冲 6 条
→ 只需要约 18~24 个 Item 节点

滚动时根据 content offset 计算可见索引,再复用 Item。无需每帧遍历 10000 个节点查询世界坐标。

真实项目故障:聊天页收到消息时滚动跳动 ​

新消息图片异步加载后,Item 高度从占位值变大,Layout 重排 Content;用户正在查看历史位置,ScrollView offset 未补偿,于是画面跳动。

修复:

  • 图片区域使用稳定占位尺寸;
  • 批量处理同帧尺寸变化;
  • 用户在底部时自动跟随,不在底部时保持锚定消息;
  • 虚拟列表缓存数据高度;
  • 记录重排次数和滚动语义。

诊断与练习答案 ​

Layout 每帧都有成本 ​

检查是否持续写相同尺寸、Label 每帧变化、Widget 与 Layout 争夺属性、Content 在 Tween 中变化。

批量更新后视觉错误 ​

确认最终显式刷新、组件启用顺序和依赖尺寸已准备;不要把“少刷新”变成“不刷新”。

  1. Widget 与 Layout 有何不同? Widget 根据父/屏幕边界对齐,Layout 根据子节点集合排列和计算容器。
  2. 对象池能否解决全量 Layout? 不能。池只减少创建;若 1000 个节点仍在 Content,布局仍可能遍历。
  3. 为什么固定尺寸常有帮助? 减少文本/图片变化向父布局传播,位置与边界更稳定。
  4. 虚拟列表怎样验证正确? 大数据滚动、快速跳转、动态高度、复用 reset、边界与回收后异步回调都需测试。

十一、本课总结 ​

text
UI 性能的关键常常是刷新范围和刷新次数。
Widget、Layout、ScrollView 应避免重复遍历,并优先只处理可见区域。

十二、下一课预告 ​

text
Lesson063|JavaScript GC 与高频临时对象