Skip to content

Lesson 043|Label 为什么通常比 Sprite 更复杂? ​

Tags: #Creator2.x #Stage04 #Label #Font #UIDifficulty: ⭐⭐⭐☆☆


真实问题:一个倒计时为什么能拖慢整页 UI ​

活动页只有一个倒计时 Label 每帧更新:

ts
label.string = remain.toFixed(2);

但文本变化可能触发:

text
字符串解析
→ 查找/生成字形
→ 排版与换行
→ 更新节点尺寸
→ Layout/Widget 重排
→ 重建顶点和 UV
→ 字形纹理或图集更新

Sprite 通常已有固定四边形和 SpriteFrame;Label 的几何与纹理来源却由文字内容动态决定。

🎯 本课目标 ​

同样是屏幕上的一块矩形,Label 往往比 Sprite 更容易产生 CPU、内存和更新成本。本课解释原因,并建立 UI 文本优化的判断方法。


二、Sprite 和 Label 的输入不同 ​

Sprite 的输入大多是固定资源:

text
纹理区域
尺寸
颜色

Label 的输入可能是动态字符串:

text
文本内容
字体
字号
颜色
对齐
行距
换行
溢出策略

文本变化往往需要重新测量、排版和生成字形几何。


三、Label 的高层流程 ​

text
label.string 改变
    ↓
文本测量
    ↓
换行、对齐和布局
    ↓
查找或生成字形纹理
    ↓
生成字符顶点和 UV
    ↓
更新 RenderData
    ↓
进入 Renderer

因此逐字修改 Label 可能比移动一个 Sprite 贵得多。


四、字体资源和字形缓存 ​

字体系统可能使用:

  • 系统字体。
  • 位图字体。
  • BMFont 图集。
  • 动态字体。
  • 字形缓存纹理。

动态文本遇到新字符时,可能需要把字形绘制或加入缓存。首次显示大量新字符时容易出现尖峰。


五、为什么每帧更新 string 有风险 ​

ts
update(dt: number) {
    this.label.string = String(Math.floor(this.time));
}

即使显示内容没有变化,也可能触发:

text
字符串创建
文本比较
布局检查
字形或顶点更新

更好的方式是只在显示值变化时更新:

ts
const next = String(Math.floor(this.time));
if (next !== this.label.string) {
    this.label.string = next;
}

还可以把刷新频率从每帧降低到每秒或每 0.1 秒。


六、Label 的布局成本 ​

多行文本可能涉及:

text
字符宽度测量
换行计算
对齐
Content Size
父级 Layout
Widget 重新适配

如果 Label 位于 Layout 和 Widget 链中,修改一个字符串可能导致父子布局级联刷新。


七、文本优化方法 ​

  • 避免每帧无条件赋值 string。
  • 低频数据使用定时刷新。
  • 固定文本优先使用位图字体或预生成资源。
  • 统一字体和字号,减少字形缓存碎片。
  • 列表只更新可见 Item。
  • 避免在复杂 Layout 中频繁改变大段文本。
  • 对富文本、描边和阴影进行单独评估。

八、案例:倒计时 Label ​

错误写法:

ts
update(dt: number) {
    this.left -= dt;
    this.label.string = this.left.toFixed(2);
}

如果用户只需要看到整数秒:

ts
update(dt: number) {
    this.left -= dt;
    const next = Math.max(0, Math.ceil(this.left));
    if (next !== this.lastShown) {
        this.lastShown = next;
        this.label.string = String(next);
    }
}

逻辑时间仍每帧更新,但文本表现按需要刷新。


九、常见误区 ​

误区一:Label 只是另一种 Sprite ​

它可能涉及文本测量、排版、字形缓存和动态几何。

误区二:字符串内容没变,重复赋值也没成本 ​

重复赋值可能触发检查和更新,应避免无意义写入。

误区三:所有文本都应该每帧刷新 ​

显示频率应匹配用户可感知的变化频率。

误区四:字体资源只影响包体,不影响运行时 ​

字形生成、缓存和纹理更新都会影响 CPU、内存和 GPU。


十、深入推导:文本成本由哪些变量决定 ​

先把一行字符串变成像素的过程完整展开:

text
JavaScript string
→ 按字体、字号、行高和宽度约束测量
→ 决定换行和每个字符位置
→ 为每个字符寻找对应字形
→ 字形不存在时生成或加入缓存
→ 得到字形所在纹理区域
→ 为字符生成顶点、UV、颜色和索引
→ 组织字体纹理与材质批次
→ Fragment Shader 采样字形

SpriteFrame 通常已经知道固定纹理矩形;Label 的纹理区域和几何却取决于字符串内容,这就是两者复杂度差异的根源。

“字符串长度没变”为什么仍可能重排 ​

1111 换成 8888 时字符数相同,但比例字体中不同数字的字形宽度可能不同;中文、英文、Emoji 和富文本图片的度量更不一样。

因此是否重排取决于:

text
字体是否等宽;
字形 advance 是否相同;
是否自动换行;
Label 宽度是否固定;
Overflow 模式;
父 Layout 是否读取尺寸。

仅比较 string.length 不能证明布局没有变化。

字符数量 ​

更多字符通常意味着更多字形四边形、顶点和排版工作。

字符集合 ​

只变化数字 0~9 与不断出现新中文字符,对字形缓存压力不同。

排版约束 ​

自动换行、Overflow、行高、对齐、节点尺寸变化会影响布局计算。

缓存模式 ​

不同 Label Cache Mode 在生成速度、纹理占用、动态文字和合批之间取舍。具体枚举和适用字体类型以 Creator 2.4.x 当前版本为准。

更新频率 ​

一秒一次与每帧一次的视觉差距可能很小,CPU 工作量却相差几十倍。

十一、Creator 2.4.x 实验:固定显示与高频文本 ​

场景 ​

text
Canvas
├── StaticLabels(100 个固定文本)
├── DynamicLabels(100 个数字)
└── Control

所有 Label 使用相同字体、字号和材质,先避免无关变量。

LabelCostProbe.ts ​

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

@ccclass
export default class LabelCostProbe extends cc.Component {
    @property([cc.Label])
    labels: cc.Label[] = [];

    private mode = 0;
    private elapsed = 0;
    private lastSecond = -1;

    setMode(value: number) {
        this.mode = value;
    }

    update(dt: number) {
        this.elapsed += dt;

        if (this.mode === 1) {
            this.updateAll(this.elapsed.toFixed(2));
        } else if (this.mode === 2) {
            const second = Math.floor(this.elapsed);
            if (second !== this.lastSecond) {
                this.lastSecond = second;
                this.updateAll(String(second));
            }
        }
    }

    private updateAll(value: string) {
        for (let i = 0; i < this.labels.length; i++) {
            const next = value + '-' + i;
            if (this.labels[i].string !== next) {
                this.labels[i].string = next;
            }
        }
    }
}

对照 ​

  1. mode 0:完全静态。
  2. mode 1:每帧更新。
  3. mode 2:每秒更新。
  4. 固定 Label 节点宽高后再对比 Layout。
  5. 分别测试只含数字与不断引入新字符。
  6. 在相同环境比较 Creator 提供的 Cache Mode。

记录 Frame Time、DrawCall、节点/布局表现和纹理变化。不要只记录 FPS 整数。

十二、如何避免无意义写入 ​

优化前先分清三个频率:

text
业务模拟频率:数值多久计算一次
显示值变化频率:用户真正能看到的新值多久出现一次
Label 刷新频率:代码多久给 string 赋值一次

例如倒计时内部可以每帧累积 dt,但界面只显示整数秒:

text
内部剩余时间:9.93、9.91、9.90……
显示字符串:9

这一秒内重复写入 "9" 没有视觉价值。缓存上一次显示值,只在跨秒时更新,能直接减少文本下游工作。

还要避免用“所有 Label 都缓存”为统一答案。缓存模式会在更新成本、字形纹理、内存、画质和合批之间取舍;静态标题、聊天长文本和高频数字的最佳策略可能不同。

即使值没变化,这段代码也每帧赋值:

ts
this.label.string = this.score.toString();

使用值变化驱动:

ts
private shownScore = -1;

refreshScore(score: number) {
    if (score === this.shownScore) {
        return;
    }
    this.shownScore = score;
    this.label.string = String(score);
}

倒计时若只显示整数秒,也只需跨秒时更新。

十三、真实项目故障:聊天频道越聊越卡 ​

这个故障不能只归因于“Label 太多”。它通常是多条增长曲线叠加:

text
历史消息越来越多
→ Node / Label / RichText 数量增长

新字符和 Emoji 持续出现
→ 字形或图片资源集合增长

Content 越来越高
→ Layout 与滚动边界计算范围增长

所有历史节点常驻
→ 即使屏幕只显示十几条,CPU 管理对象仍不断增加

虚拟列表的关键不是把数据删掉,而是把“历史消息数据量”和“同时存在的渲染节点量”解耦:

text
上千条消息数据
→ 只为可见区域及缓冲区保留少量 Item
→ 滚动时复用 Item 并替换显示数据

这样同时缩小 Node、Label、Layout 和渲染收集范围。

现象 ​

聊天列表保留上千条 Label,包含大量不同中文与表情。滚动一段时间后内存和卡顿上升。

根因组合 ​

text
无限保留历史节点
→ 大量 Label 与顶点
→ 新字符持续进入字形缓存
→ ScrollView/Layout 全量参与
→ 表情资源与文本混排增加批次

修复 ​

  • 虚拟化可见消息;
  • 历史数据与渲染节点分离;
  • 限制富文本和表情组合;
  • 选择与内容特征匹配的字体/缓存方案;
  • 减少宽度变化引发的重复排版;
  • 在真机记录字形缓存、节点和帧时间。

十四、Label 故障诊断 ​

文本更新导致整页重排 ​

检查 Label Overflow、节点尺寸是否自适应、父 Layout 和 Widget。固定数字栏尺寸常比优化 shader 更直接。

首次出现某字符卡一下 ​

检查动态字体字形生成/上传、Cache Mode 和字符预热。预热也有启动成本,不能无上限预热所有字符。

DrawCall 增加 ​

检查字体纹理、材质、缓存页、富文本图片和渲染顺序。Label 数量不是唯一条件。

十五、练习、推导答案与版本边界 ​

Label 的缓存模式、字形图集和内部 Assembler 属于 Creator 2.4.x 具体实现。实验应固定引擎版本、字体资源和文本集合。

  1. 为什么数字倒计时不应每帧更新整数显示? 一秒内显示值不变,重复赋值只制造排版/几何检查。
  2. 固定 Label 宽度如何减少连锁? 文本宽度变化不再推动父 Layout 重排,影响范围更小。
  3. Cache Mode 是否有一个永远最优选项? 没有。静态/动态、字符集合、内存和合批需求不同,需要同场景测量。
  4. Label 卡顿为什么不能只看 DrawCall? 字符串、字形、排版、布局和顶点重建主要可能发生在 CPU,DrawCall 不一定变化。

能力验收 ​

你应该能把一次 Label.string 修改拆成字符串、排版、字形缓存、几何、布局和批次六类可能成本,并用每帧赋值、值变化时赋值、固定宽度三组实验找到真正的刷新边界。

本课总结 ​

text
Sprite 主要消费固定纹理区域。
Label 还要处理文本测量、排版和字形缓存。
动态文本更新可能带来 CPU、RenderData 和纹理成本。
文本逻辑频率和显示刷新频率可以分离。

十一、下一课预告 ​

text
Lesson044|Material、Effect、Technique、Pass、Shader 的关系