4、字体与文本优化:从源头砍掉字库的“虚胖”

字库这东西,在 TouchGFX 项目里经常是内存占用的“隐形大户”。我见过不少团队,UI 做得很漂亮,一编译发现 Flash 爆了,查来查去,罪魁祸首就是字库。说白了,你把整个中文字体文件塞进去,那肯定不行。今天咱们聊聊怎么给字库“瘦身”。

4.1 为什么字库这么“吃”内存?

一个标准的 16x16 点阵中文字库,GB2312 编码下大约有 6763 个汉字。每个字按 16 位灰度算,就是 16×16×2 = 512 字节。算下来:

字库类型 字符数 单字大小 总大小(约)
16x16 点阵(1bpp) 6763 32 字节 216 KB
16x16 点阵(4bpp 抗锯齿) 6763 128 字节 864 KB
矢量字体(FreeType 渲染) 全字库 — 2~5 MB

嗯,这里要注意,这只是纯字模数据。如果用了矢量字体,还得加上 FreeType 的渲染缓存,那又是几百 KB 的 RAM 开销。我在一个工业 HMI 项目里就吃过这个亏——界面就显示 20 个数字和几个汉字,结果字库占了 80% 的 Flash。

4.2 第一刀:Unicode 范围裁剪

TouchGFX 的 Texts 和 Typographies 配置里,可以指定每个字体文件使用的 Unicode 范围。默认是全范围(0x0000~0xFFFF),但你的 UI 真的需要显示所有汉字吗?

核心思路:只包含 UI 中实际出现的字符范围。

举个例子,你的界面只用到了数字、英文字母和少量中文:

// 在 application.config 中配置
UnicodeRange:
  - start: 0x0020    # 空格
    end: 0x007E      # 基本拉丁字母
  - start: 0x4E00    # 常用汉字起始
    end: 0x4E10      # 只包含前16个汉字
  - start: 0xFF10    # 全角数字
    end: 0xFF19

我个人习惯的做法是:先用一个脚本扫描整个 UI 工程里所有用到的字符串,提取出实际出现的字符集合,然后反向生成这个 Unicode 范围列表。这样能保证一个字符都不浪费。

小技巧:TouchGFX Designer 的“Generate”阶段会输出一个 UnicodeList.txt,里面列出了所有用到的字符。你可以直接拿这个文件来裁剪字库。

4.3 第二刀:字体子集化

Unicode 范围裁剪只是第一步。更狠的做法是字体子集化——只保留 UI 中实际出现的字符,其他全部砍掉。这需要用到一些工具,比如 FontForge 或 Python 的 fonttools 库。

我曾经在一个智能家居面板项目里,UI 总共只用到了 120 个汉字。原始字体文件 3.2MB,子集化之后只有 48KB。你想想看,这差距有多大。

子集化的基本步骤:

  1. 提取 UI 中所有文本字符串,去重得到字符列表。
  2. 用 fonttools 读取原始字体文件。
  3. 只保留字符列表中的字形,删除其他所有字形。
  4. 重新生成字体文件。

Python 脚本示例:

from fontTools.ttLib import TTFont
from fontTools.subset import Subsetter

# 加载原始字体
font = TTFont('SourceHanSansSC-Regular.otf')

# 定义要保留的字符
chars = '0123456789ABCDEF温度湿度设置返回确认'

# 创建子集化器
subsetter = Subsetter()
subsetter.populate(unicodes=[ord(c) for c in chars])

# 执行子集化
subsetter.subset(font)

# 保存
font.save('subset_font.otf')

注意:子集化后的字体文件不能再用于显示未包含的字符。如果后续 UI 增加了新文本,必须重新生成字库。所以建议在 UI 文本冻结后再做这一步。

4.4 第三刀:位图字体替代矢量字体

矢量字体(TrueType/OpenType)虽然美观,但代价是巨大的。每个字符都需要 FreeType 库实时渲染,这不仅要占用 Flash 存储字体轮廓数据,还要在 RAM 中维护渲染缓存。

位图字体就不一样了。它是预渲染好的像素点阵,直接存成二进制数组。TouchGFX 原生支持位图字体,而且渲染速度极快——说白了就是内存拷贝。

什么时候该用位图字体?

  • 固定大小的文本:比如按钮标签、状态指示、数值显示。
  • 字符集很小:比如只显示数字和几个符号。
  • 对性能要求高:位图字体不需要 FreeType 渲染,CPU 开销几乎为零。

什么时候还是用矢量字体?

  • 需要动态缩放:比如用户可调整字体大小。
  • 字符集很大:位图字体每个字号都要单独存一份,字多了反而更占空间。
  • 需要复杂排版:比如多语言混排、文字环绕等。
对比项 位图字体 矢量字体
Flash 占用 低(仅存点阵) 高(存轮廓+渲染缓存)
RAM 占用 几乎为零 需要渲染缓存(几十~几百 KB)
渲染速度 极快(内存拷贝) 较慢(需要 FreeType 计算)
灵活性 差(固定字号) 好(可任意缩放)
适用场景 固定 UI 元素 动态文本、多语言

在 TouchGFX 中,位图字体是通过 Font Converter 工具生成的。你只需要提供原始字体文件和要转换的字符列表,它就会输出一个 .ttf 文件(实际上是位图数据打包成的字体文件)。

4.5 实战:三种策略的组合使用

在实际项目中,我通常不会只用一种方法。而是根据 UI 的不同区域,组合使用这三种策略。

下面这张图展示了我的优化流程:

字体优化决策流程 字体优化决策流程 UI 文本冻结 提取所有用到的字符 字符数 < 200? 是 位图字体 Flash 占用极低 否 字体子集化 生成优化后字库 提示:字符数少(<200)时位图字体更优;字符数多时子集化矢量字体更灵活。 两种方案都建议配合 Unicode 范围裁剪进一步压缩。

具体到我的一个实际案例:一个温控器界面,显示温度、湿度、设置参数。总共用了 86 个字符(数字、单位、几个汉字)。我直接用了位图字体,字库大小从原来的 1.2MB 降到了 12KB。RAM 占用更是从 200KB 降到了几乎为零。

优化效果总结:

  • Unicode 范围裁剪:减少 30%~50% 的字库体积
  • 字体子集化:减少 80%~95% 的字库体积
  • 位图字体替代:减少 90%~99% 的字库体积(字符集小的情况下)

4.6 避坑指南

我曾经在一个项目里踩过一个坑:子集化字体之后,UI 上某个按钮的文字显示成了方框。排查了半天,发现是那个字符在原始字体里存在,但子集化时漏掉了——因为字符串里用的是全角冒号,而我提取字符时只考虑了半角。

所以,提取字符时一定要注意:

  • 区分全角和半角符号
  • 包含空格、换行符等不可见字符
  • 如果 UI 支持动态文本(比如用户输入),那就不适合做子集化

另一个坑是位图字体的字号问题。位图字体是固定大小的,如果你在 TouchGFX 里设置了不同的 Typography 字号,就得为每个字号单独生成位图字体。否则字体会被拉伸,效果惨不忍睹。

我的建议:在项目初期就确定好 UI 中会用到的所有字号,统一管理。不要边开发边加字号,那样字库会越来越臃肿。

好了,字体优化这块就聊到这儿。记住一个原则:只包含你真正需要的字符。做到这一点,字库内存占用至少能砍掉一半以上。