字体资源压缩原理:为什么需要压缩、Flash与RAM的权衡、常见压缩算法在字体中的应用

这一章,我们来聊聊压缩。很多刚接触TouchGFX的朋友会问我:“字体文件就那么点大,有必要折腾压缩吗?”

说实话,我刚开始做嵌入式UI时也是这么想的。直到有一次,项目里用了一套包含中、英、日、韩四种语言的字体,光一个16px的点阵字库就吞掉了将近2MB的Flash。那时候MCU的Flash总共才8MB,UI固件、协议栈、RTOS一瓜分,留给字体的空间连塞个完整GB2312都够呛。嗯,从那以后,我再也不敢小看字体压缩这件事了。

为什么需要压缩?三个字:空间紧

嵌入式设备的存储资源,说白了就是“螺蛳壳里做道场”。字体资源压缩的必要性,可以从三个维度来看:

  • Flash容量瓶颈:中文字库动辄几千个字符,一个24px的矢量字库未压缩时可能超过3MB。而主流MCU的Flash通常在2MB~16MB之间,UI部分能分到30%就算不错了。
  • RAM占用压力:TouchGFX在渲染时会把字体数据从Flash读到RAM中解压。如果字体不压缩,RAM的消耗会直线上升。我见过一个项目,就因为字体没压缩,导致系统在切换页面时频繁触发内存分配失败。
  • OTA升级效率:字体资源越大,固件包就越大,OTA下载时间越长。压缩后的字体能让升级包缩小40%~60%,这对量产设备来说,体验差距非常明显。

核心观点:压缩不是“锦上添花”,而是“雪中送炭”。在资源受限的嵌入式平台上,字体压缩是UI工程化的必修课。

Flash与RAM的权衡:压缩比 vs 解压速度

这里有个经典的取舍问题:压缩率越高,解压时消耗的RAM和CPU时间就越多。你想想看,Flash省下来的空间,可能被解压缓冲区吃回去。

我个人习惯把这种权衡画成一个三角关系:

Flash 与 RAM 的权衡三角 Flash 占用 RAM 占用 解压速度 高压缩率 → 低Flash占用,但解压慢、RAM缓冲区大

你看,三个角互相牵制。高压缩率的算法(比如Huffman)能省Flash,但解压时需要更大的查找表和缓冲区,RAM反而吃紧。反过来,RLE这种轻量算法解压快、RAM开销小,但压缩率一般。

我在项目中遇到过最典型的情况:某款芯片Flash只有4MB,但RAM有512KB。这时候我倾向于选择压缩率更高的算法,因为RAM相对宽裕。而另一款芯片Flash有16MB,RAM只有256KB,那就反过来,用RLE或者干脆不压缩,把解压RAM省下来给UI渲染用。

实战建议:在做方案选型时,先拉一张Flash和RAM的“预算表”。字体压缩后的Flash占用 + 解压缓冲区大小,两项加起来不能超过总资源的60%。剩下的40%要留给系统运行和UI动画。

常见压缩算法在字体中的应用

嵌入式字体压缩领域,常用的算法就那么几种。我按“从简单到复杂”的顺序给你捋一遍。

1. RLE(游程编码)—— 简单粗暴,适合点阵字

RLE的原理很简单:把连续重复的数据替换成“重复次数 + 数据值”。比如 0x00 0x00 0x00 0x00 0xFF 压缩成 0x04 0x00 0x01 0xFF。

为什么适合字体?因为点阵字库中,空白像素(0x00)和实心像素(0xFF)经常连续出现。尤其是中文字体,笔画之间的空白区域很大,RLE能轻松达到2:1~3:1的压缩率。

我早期做过一个项目,用RLE压缩16px的GB2312点阵字库,Flash占用从1.2MB降到了480KB。解压代码只有几十行,几乎不占RAM。

// RLE 解压示例(伪代码)
void rle_decompress(uint8_t *src, uint8_t *dst, int dst_size) {
    int pos = 0;
    while (pos < dst_size) {
        uint8_t count = *src++;   // 重复次数
        uint8_t value = *src++;   // 数据值
        memset(dst + pos, value, count);
        pos += count;
    }
}

注意:RLE对矢量字库(如TTF转成的位图缓存)效果很差,因为矢量渲染后的像素分布比较随机,连续重复很少。别在矢量字体上浪费RLE。

2. LZ4 —— 速度优先,适合实时渲染

LZ4是一种基于字典的压缩算法。它的核心思路是:用“偏移量+长度”来替换重复的数据块。解压速度极快,能达到每纳秒处理几个字节的水平。

TouchGFX官方其实就内置了LZ4的支持。我记得在某个项目中,我们用LZ4压缩了一套24px的矢量字体,压缩率大约在40%~50%之间,解压速度完全不影响60fps的渲染帧率。

LZ4的缺点也很明显:压缩率不如Huffman,而且压缩过程比较慢(不过压缩是PC端做的,不影响运行时)。

// LZ4 解压接口(TouchGFX 中实际使用)
// 需要包含 lz4.h
int lz4_decompress(const char *src, char *dst, int compressed_size, int dst_size) {
    return LZ4_decompress_safe(src, dst, compressed_size, dst_size);
}

我的习惯:如果项目对UI流畅度要求很高(比如60fps动画),我优先选LZ4。它的解压速度在Cortex-M4上实测能做到200MB/s以上,基本不拖后腿。

3. Huffman —— 压缩率之王,但代价不小

Huffman编码的原理是根据字符出现频率分配变长编码:高频字符用短码,低频字符用长码。在字体场景中,某些字形(比如空格、标点)出现频率极高,Huffman能把这些字符的编码压缩到几个bit。

压缩率通常能做到30%~50%,比LZ4高10~15个百分点。但代价是什么?

  • 解压需要维护Huffman树,占用额外的RAM(几百字节到几KB不等)。
  • 解压速度慢,因为要逐bit解析,不适合大块数据的实时渲染。

我曾经在一个需要存储超大字库(包含生僻字)的项目中尝试过Huffman。压缩后的字库确实小,但每次页面切换时,解压延迟导致画面卡顿了200ms。后来换回LZ4,虽然Flash多用了100KB,但体验流畅多了。

一句话总结:Huffman适合“写多读少”的场景,比如字库存储在外部Flash,只在系统启动时解压一次。不适合运行时频繁解压的UI渲染。

三种算法对比一览

算法 压缩率(典型值) 解压速度 RAM开销 适合字体类型
RLE 30%~60% 极快 几乎为0 点阵字库
LZ4 40%~60% 快 低(几KB) 矢量/点阵通用
Huffman 30%~50% 中等 中(几KB~几十KB) 静态字库、低频加载

你看,没有银弹。选哪种算法,取决于你的Flash余量、RAM余量、以及对渲染实时性的要求。

我个人习惯的做法是:先评估RAM是否紧张。如果RAM够用,优先选LZ4,平衡性好;如果RAM非常紧张,选RLE;如果Flash实在塞不下,再考虑Huffman,但要做好解压速度的测试。