外观
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 就完成虚拟化
节点、组件、布局和资源仍然可能工作,应该根据需求禁用、回收或复用。
十、练习与答案
- Widget 和 Layout 的职责有什么区别?
- 为什么循环内强制刷新布局可能很慢?
- 10,000 条数据只显示 20 条时,为什么要做虚拟列表?
- alpha=0 和回收 Item 的性能语义有什么不同?
答案:
- Widget 处理约束定位,Layout 处理子节点排列和容器尺寸。
- 每次修改都可能重新遍历和计算,形成重复传播。
- 降低节点、组件、布局、渲染和资源工作量。
- 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 验证。先在实验场景确认视觉与生命周期,再用于业务。
对照
- 添加 20、100、500 Item。
- 固定 Item 高度与 Label 自适应分别测试。
- 每个 Item 有 Widget 与无 Widget 对比。
- 全量列表与虚拟列表对比。
- 记录打开尖峰和滚动稳定帧。
ScrollView 虚拟化的核心不是对象池
对象池减少创建/销毁;虚拟列表减少同时参与 UI 的节点数量。
text
总数据 10000 条
可见 12 条
缓冲 6 条
→ 只需要约 18~24 个 Item 节点滚动时根据 content offset 计算可见索引,再复用 Item。无需每帧遍历 10000 个节点查询世界坐标。
真实项目故障:聊天页收到消息时滚动跳动
新消息图片异步加载后,Item 高度从占位值变大,Layout 重排 Content;用户正在查看历史位置,ScrollView offset 未补偿,于是画面跳动。
修复:
- 图片区域使用稳定占位尺寸;
- 批量处理同帧尺寸变化;
- 用户在底部时自动跟随,不在底部时保持锚定消息;
- 虚拟列表缓存数据高度;
- 记录重排次数和滚动语义。
诊断与练习答案
Layout 每帧都有成本
检查是否持续写相同尺寸、Label 每帧变化、Widget 与 Layout 争夺属性、Content 在 Tween 中变化。
批量更新后视觉错误
确认最终显式刷新、组件启用顺序和依赖尺寸已准备;不要把“少刷新”变成“不刷新”。
- Widget 与 Layout 有何不同? Widget 根据父/屏幕边界对齐,Layout 根据子节点集合排列和计算容器。
- 对象池能否解决全量 Layout? 不能。池只减少创建;若 1000 个节点仍在 Content,布局仍可能遍历。
- 为什么固定尺寸常有帮助? 减少文本/图片变化向父布局传播,位置与边界更稳定。
- 虚拟列表怎样验证正确? 大数据滚动、快速跳转、动态高度、复用 reset、边界与回收后异步回调都需测试。
十一、本课总结
text
UI 性能的关键常常是刷新范围和刷新次数。
Widget、Layout、ScrollView 应避免重复遍历,并优先只处理可见区域。十二、下一课预告
text
Lesson063|JavaScript GC 与高频临时对象