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。你想想看,这差距有多大。
子集化的基本步骤:
- 提取 UI 中所有文本字符串,去重得到字符列表。
- 用 fonttools 读取原始字体文件。
- 只保留字符列表中的字形,删除其他所有字形。
- 重新生成字体文件。
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 的不同区域,组合使用这三种策略。
下面这张图展示了我的优化流程:
具体到我的一个实际案例:一个温控器界面,显示温度、湿度、设置参数。总共用了 86 个字符(数字、单位、几个汉字)。我直接用了位图字体,字库大小从原来的 1.2MB 降到了 12KB。RAM 占用更是从 200KB 降到了几乎为零。
优化效果总结:
- Unicode 范围裁剪:减少 30%~50% 的字库体积
- 字体子集化:减少 80%~95% 的字库体积
- 位图字体替代:减少 90%~99% 的字库体积(字符集小的情况下)
4.6 避坑指南
我曾经在一个项目里踩过一个坑:子集化字体之后,UI 上某个按钮的文字显示成了方框。排查了半天,发现是那个字符在原始字体里存在,但子集化时漏掉了——因为字符串里用的是全角冒号,而我提取字符时只考虑了半角。
所以,提取字符时一定要注意:
- 区分全角和半角符号
- 包含空格、换行符等不可见字符
- 如果 UI 支持动态文本(比如用户输入),那就不适合做子集化
另一个坑是位图字体的字号问题。位图字体是固定大小的,如果你在 TouchGFX 里设置了不同的 Typography 字号,就得为每个字号单独生成位图字体。否则字体会被拉伸,效果惨不忍睹。
我的建议:在项目初期就确定好 UI 中会用到的所有字号,统一管理。不要边开发边加字号,那样字库会越来越臃肿。
好了,字体优化这块就聊到这儿。记住一个原则:只包含你真正需要的字符。做到这一点,字库内存占用至少能砍掉一半以上。