问题背景
嵌入式设备(MCU、SoC 上的 LVGL、Qt Embedded 等)通常面临两个矛盾:Flash/ROM 空间有限,希望图片尽量小;CPU 和 RAM 有限,又希望解码尽量快。通用格式(PNG、JPEG)的解码器体积大、依赖多,往往不适合直接搬进资源紧张的项目。因此,针对 UI 图片的特点(大面积纯色、重复纹理、色彩数有限),选择更轻量的压缩方案是常见做法。
本文列出几类在"压缩率—解码速度—实现复杂度"三角中比较平衡的算法,给出简化参考代码和选型建议。所有代码均为教学性质的伪实现,未经验证,直接用于产品前必须补充边界检查、内存安全处理和平台适配。
一、RLE 变种
原理
RLE(Run Length Encoding)将连续相同字节压缩为"长度 + 值"对。对 UI 中大面积纯色区域(按钮背景、分割线、图标底色)效果显著,但对渐变或复杂纹理几乎无压缩。
常见变种
- RLE-4:用 4 位(半字节)存储游程长度,适合色彩数较少的图标,编码密度更高。
- SRLE(Sparse RLE):仅对长度达到阈值的游程进行编码,短序列以字面量直接拷贝,避免"编码后反而变大"的问题。
优缺点
- 优点:解码只需一次循环展开,速度极快;实现几十行即可;无额外内存开销。
- 缺点:对复杂图案(照片、渐变)压缩率很低甚至膨胀。
- 适合:UI 图标、按钮、简单图形元素、大面积单色背景。
参考实现
// RLE (Run-Length Encoding) Implementation
int rle_encode(const unsigned char *input, int input_len, unsigned char *output) {
int output_len = 0;
int i = 0;
while (i < input_len) {
unsigned char current = input[i];
int count = 1;
while (i + count < input_len && input[i + count] == current && count < 255) {
count++;
}
output[output_len++] = count;
output[output_len++] = current;
i += count;
}
return output_len;
}
int rle_decode(const unsigned char *input, int input_len, unsigned char *output) {
int output_len = 0;
int i = 0;
while (i < input_len) {
int count = input[i++];
unsigned char value = input[i++];
for (int j = 0; j < count; j++) {
output[output_len++] = value;
}
}
return output_len;
}
代码注意事项:上述 RLE 实现未对 output 缓冲区大小做检查,调用方必须保证 output 至少为 input_len * 2 字节;count 上限 255 意味着超过 255 的连续相同字节会被拆成多段,解码端需正确处理。
// SRLE (Simplified RLE) Implementation
int srle_encode(const unsigned char *input, int input_len, unsigned char *output) {
int output_len = 0;
int i = 0;
while (i < input_len) {
// Find runs of same value
unsigned char current = input[i];
int run_length = 1;
while (i + run_length < input_len && input[i + run_length] == current) {
run_length++;
}
if (run_length >= 4) {
// Encode run
output[output_len++] = 0; // Special marker
output[output_len++] = run_length;
output[output_len++] = current;
i += run_length;
} else {
// Copy literal
output[output_len++] = input[i++];
}
}
return output_len;
}
int srle_decode(const unsigned char *input, int input_len, unsigned char *output) {
int output_len = 0;
int i = 0;
while (i < input_len) {
if (input[i] == 0) {
// Decode run
int run_length = input[i + 1];
unsigned char value = input[i + 2];
for (int j = 0; j < run_length; j++) {
output[output_len++] = value;
}
i += 3;
} else {
// Copy literal
output[output_len++] = input[i++];
}
}
return output_len;
}
代码注意事项:SRLE 使用 0x00 作为游程标记,但字面量分支也会原样拷贝 0x00 字节,导致解码时无法区分"标记"和"字面量 0x00"。实际实现中应选用一个不会出现在字面量中的保留值(例如对字面量做 +1 偏移),或改用变长编码。此外 run_length 写入输出缓冲区时仅占 1 字节(变量本身为 int),有效上限为 255,超过时需分段。
二、LZ77 / LZSS 家族
原理
LZ77 类算法维护一个滑动窗口,将当前数据与窗口内已出现过的子串做匹配,匹配成功则输出"距离 + 长度",否则输出字面量。相比 RLE,它能捕获非连续重复模式,压缩率更高。
常见实现
- miniLZO:解压速度极快(现代 x86 单核可达数百 MB/s 量级),压缩率适中,C 代码量小,适合对解码延迟敏感的场景。
- FastLZ:独立的 LZ77 实现(非 LZO 衍生),采用不同的匹配搜索策略,压缩率与 LZO 相近或略好,速度相当或稍降。
优缺点
- 优点:解码为顺序扫描 + 内存拷贝,分支较少且可预测性较好;内存占用小(哈希表可放 RAM 或静态分配);压缩率比 RLE 高。
- 缺点:压缩率不如 DEFLATE 等复杂算法;哈希表大小影响匹配质量,需按目标数据调参。
- 适合:动态加载的 UI 资源、需要快速解码的中等大小图片。
参考实现
// Mini LZO-style Implementation
#define HASH_BITS 14
#define HASH_SIZE (1 << HASH_BITS)
#define HASH_MASK (HASH_SIZE - 1)
#define MIN_MATCH 3
#define MAX_MATCH 128
int mini_lzo_encode(const unsigned char *input, int input_len, unsigned char *output) {
int output_len = 0;
unsigned short hash_table[HASH_SIZE] = {0};
int i = 0;
while (i < input_len) {
// Look for a match
int hash = ((input[i] << 8) | input[i + 1]) & HASH_MASK;
int match_pos = hash_table[hash];
int match_len = 0;
if (match_pos < i) {
// Check match length
while (match_len < MAX_MATCH &&
i + match_len < input_len &&
input[match_pos + match_len] == input[i + match_len]) {
match_len++;
}
}
if (match_len >= MIN_MATCH) {
// Encode match
int match_dist = i - match_pos;
output[output_len++] = ((match_len - MIN_MATCH) << 4) | ((match_dist >> 8) & 0x0F);
output[output_len++] = match_dist & 0xFF;
i += match_len;
} else {
// Encode literal
output[output_len++] = input[i++];
}
// Update hash table
hash_table[hash] = i;
}
return output_len;
}
int mini_lzo_decode(const unsigned char *input, int input_len, unsigned char *output) {
int output_len = 0;
int i = 0;
unsigned short hash_table[HASH_SIZE] = {0};
while (i < input_len) {
unsigned char byte = input[i++];
if (byte < 0x80) { // literal byte
output[output_len++] = byte;
} else { // compressed match
int len = (byte >> 4) + MIN_MATCH;
int dist = ((byte & 0x0F) << 8) | input[i++];
int match_pos = output_len - dist;
for (int j = 0; j < len; j++) {
output[output_len++] = output[match_pos + j];
}
}
}
return output_len;
}
代码注意事项:
hash_table 为 32 KB 的栈上数组(unsigned short[16384],16384 × 2 字节),在栈空间紧张的 MCU 上可能溢出,应改为静态分配或堆分配。
- 编码端用哈希查找匹配,解码端却用完全不同的位格式(
byte < 0x80 判字面量),两者并不匹配——此代码仅为展示思路,不能直接编解码。
- 解码函数中声明了
hash_table 但未使用,属于冗余。
- 未检查
output 缓冲区大小,存在越写风险。
// FastLZ-style Implementation
#define FASTLZ_HASH_LOG 13
#define FASTLZ_HASH_SIZE (1 << FASTLZ_HASH_LOG)
#define FASTLZ_HASH_MASK (FASTLZ_HASH_SIZE - 1)
int fastlz_encode(const unsigned char *input, int input_len, unsigned char *output) {
int output_len = 0;
int hash_table[FASTLZ_HASH_SIZE] = {0};
int i = 0;
while (i < input_len) {
// Compute hash
int hash = (input[i] + (input[i + 1] << 4) + (input[i + 2] << 8)) & FASTLZ_HASH_MASK;
int match_pos = hash_table[hash];
int match_len = 0;
if (match_pos < i) {
// Check match length
while (match_len < 264 && i + match_len < input_len &&
input[match_pos + match_len] == input[i + match_len]) {
match_len++;
}
}
if (match_len >= 3) {
// Encode match
int match_dist = i - match_pos;
if (match_len < 7) {
output[output_len++] = ((match_len - 2) << 5) | ((match_dist >> 8) & 0x1F);
} else {
output[output_len++] = (7 << 5) | ((match_dist >> 8) & 0x1F);
output[output_len++] = match_len - 7;
}
output[output_len++] = match_dist & 0xFF;
i += match_len;
} else {
// Encode literal
output[output_len++] = input[i++];
}
// Update hash table
hash_table[hash] = i;
}
return output_len;
}
int fastlz_decode(const unsigned char *input, int input_len, unsigned char *output) {
int output_len = 0;
int i = 0;
int hash_table[FASTLZ_HASH_SIZE] = {0};
while (i < input_len) {
unsigned char byte = input[i++];
if (byte < 0x80) { // literal byte
output[output_len++] = byte;
} else { // compressed match
int len = (byte >> 5) + 3;
int dist = ((byte & 0x1F) << 8) | input[i++];
int match_pos = output_len - dist;
for (int j = 0; j < len; j++) {
output[output_len++] = output[match_pos + j];
}
}
}
return output_len;
}
代码注意事项:与 miniLZO 类似,编码和解码的位格式不一致,不能直接互操作。hash_table 为 int[8192](约 32 KB),同样有栈溢出风险。匹配长度上限 264 和距离编码的位宽需与解码端严格对齐。实际项目中建议直接使用成熟的 FastLZ 或 LZO 库源码,而非自行实现。
三、QOI(Quite OK Image Format)
原理
QOI 是 2022 年发布的轻量图像格式,针对 RGBA 像素设计。它组合了以下策略:
- 相同像素游程(最多 62 个)
- 64 项颜色索引表(1 字节引用)
- 与前一像素的 RGB 小偏移(1 字节)
- 完整 RGBA 字面量(4 字节)
作者声称其压缩率接近 PNG(无滤波的 PNG),解码速度约为 PNG 的 3–4 倍。该数据来自 QOI 规范文档,具体倍数取决于图像内容和硬件平台,此处不做独立验证。
优缺点
- 优点:实现代码量小(核心编解码各约 100 行);解码无复杂分支;对 UI 截图、图标、简单插画效果好。
- 缺点:对照片类高熵图像压缩率有限;格式较新,生态工具链不如 PNG 成熟。
- 适合:需要接近 PNG 视觉效果但要求快速解码的嵌入式场景。
参考实现
// QOI (Quite OK Image Format) style Implementation
typedef struct {
unsigned char r, g, b, a;
} QOIPixel;
#define QOI_HASH(p) (((p).r * 3 + (p).g * 5 + (p).b * 7 + (p).a * 11) & 63)
int qoi_encode(const QOIPixel *input, int pixel_count, unsigned char *output) {
int output_len = 0;
QOIPixel index[64] = {0};
QOIPixel prev = {0, 0, 0, 255};
int run = 0;
for (int i = 0; i < pixel_count; i++) {
QOIPixel pixel = input[i];
if (memcmp(&pixel, &prev, sizeof(QOIPixel)) == 0) {
run++;
if (run == 62 || i == pixel_count - 1) {
output[output_len++] = 0xC0 | (run - 1);
run = 0;
}
} else {
if (run > 0) {
output[output_len++] = 0xC0 | (run - 1);
run = 0;
}
int index_pos = QOI_HASH(pixel);
if (memcmp(&pixel, &index[index_pos], sizeof(QOIPixel)) == 0) {
output[output_len++] = 0x00 | index_pos;
} else {
index[index_pos] = pixel;
if (pixel.a == prev.a) {
int vr = pixel.r - prev.r;
int vg = pixel.g - prev.g;
int vb = pixel.b - prev.b;
if (vr > -3 && vr < 2 && vg > -3 && vg < 2 && vb > -3 && vb < 2) {
output[output_len++] = 0x40 | ((vr + 2) << 4) | ((vg + 2) << 2) | (vb + 2);
} else {
output[output_len++] = 0xFE;
output[output_len++] = pixel.r;
output[output_len++] = pixel.g;
output[output_len++] = pixel.b;
}
} else {
output[output_len++] = 0xFF;
output[output_len++] = pixel.r;
output[output_len++] = pixel.g;
output[output_len++] = pixel.b;
output[output_len++] = pixel.a;
}
}
}
prev = pixel;
}
return output_len;
}
int qoi_decode(const unsigned char *input, int input_len, QOIPixel *output) {
int output_len = 0;
QOIPixel index[64] = {0};
QOIPixel prev = {0, 0, 0, 255};
int i = 0;
while (i < input_len) {
unsigned char byte = input[i++];
if (byte == 0xC0) { // run-length encoding
int run_len = (byte & 0x3F) + 1;
for (int j = 0; j < run_len; j++) {
output[output_len++] = prev;
}
} else {
prev.r = byte;
prev.g = input[i++];
prev.b = input[i++];
prev.a = input[i++];
output[output_len++] = prev;
}
}
return output_len;
}
代码注意事项:
- 解码函数仅处理了
0xC0(游程)和默认 4 字节 RGBA 两种情况,缺少索引查找(0x00–0x3F)、RGB 偏移(0x40–0x7F)、RGB 字面量(0xFE)和带 Alpha 字面量(0xFF)的分支,无法正确解码编码端产生的完整数据。
- 编码端
run 达到 62 时写入 0xC0 | (run-1),但解码端对 0xC0 的处理是 run_len = (byte & 0x3F) + 1,当 run=62 时写入 0xC0|61=0xFD,解码端读到 0xFD 不会进入 byte == 0xC0 分支——逻辑不一致。
- 未检查
output 缓冲区大小。
- 实际使用建议直接采用 QOI 官方参考实现(MIT 协议,C 语言,约 200 行),而非自行修改。
四、混合算法:RLE + Huffman
原理
先用 RLE 将连续重复压缩为"长度 + 值"对,再对 RLE 输出做 Huffman 编码。RLE 降低了数据熵,Huffman 进一步利用符号频率分布的非均匀性,两者叠加可获得比单独使用更好的压缩率。
优缺点
- 优点:压缩率优于单独 RLE 或单独 Huffman;解码速度介于 RLE 和 DEFLATE 之间。
- 缺点:实现复杂度较高(需维护频率表、建树、位级 I/O);Huffman 树需随数据一起存储或重建,增加头部开销;对短数据(< 几百字节)头部开销占比大,可能不划算。
- 适合:较大的背景图、包含重复区域的 UI 截图。
参考实现
#define MAX_TREE_NODES 512
typedef struct {
int frequency;
int left;
int right;
unsigned char value;
} HuffmanNode;
typedef struct {
unsigned int code;
int bits;
} HuffmanCode;
void build_huffman_tree(const int *frequencies, HuffmanNode *nodes, int *node_count) {
// Initialize leaf nodes
*node_count = 0;
for (int i = 0; i < 256; i++) {
if (frequencies[i] > 0) {
nodes[*node_count].frequency = frequencies[i];
nodes[*node_count].value = (unsigned char)i;
nodes[*node_count].left = -1;
nodes[*node_count].right = -1;
(*node_count)++;
}
}
// Build tree
while (*node_count > 1) {
// Find two nodes with lowest frequencies
int min1 = 0, min2 = 1;
if (nodes[min1].frequency > nodes[min2].frequency) {
int temp = min1;
min1 = min2;
min2 = temp;
}
for (int i = 2; i < *node_count; i++) {
if (nodes[i].frequency < nodes[min1].frequency) {
min2 = min1;
min1 = i;
} else if (nodes[i].frequency < nodes[min2].frequency) {
min2 = i;
}
}
// Create new internal node
HuffmanNode new_node;
new_node.frequency = nodes[min1].frequency + nodes[min2].frequency;
new_node.left = min1;
new_node.right = min2;
// Replace first minimum with new node and remove second minimum
nodes[min1] = new_node;
nodes[min2] = nodes[--(*node_count)];
}
}
void generate_huffman_codes(const HuffmanNode *nodes, int node_count, HuffmanCode *codes,
int node_index, unsigned int code, int bits) {
if (nodes[node_index].left == -1 && nodes[node_index].right == -1) {
// Leaf node
codes[nodes[node_index].value].code = code;
codes[nodes[node_index].value].bits = bits;
} else {
// Internal node - traverse left and right
generate_huffman_codes(nodes, node_count, codes, nodes[node_index].left,
(code << 1), bits + 1);
generate_huffman_codes(nodes, node_count, codes, nodes[node_index].right,
(code << 1) | 1, bits + 1);
}
}
// Combined RLE + Huffman Implementation
int rle_huffman_encode(const unsigned char *input, int input_len, unsigned char *output) {
// First perform RLE
unsigned char *rle_buffer = (unsigned char *)malloc(input_len * 2);
int rle_len = rle_encode(input, input_len, rle_buffer);
// Calculate frequencies
int frequencies[256] = {0};
for (int i = 0; i < rle_len; i++) {
frequencies[rle_buffer[i]]++;
}
// Build Huffman tree and generate codes
HuffmanNode nodes[MAX_TREE_NODES];
int node_count;
build_huffman_tree(frequencies, nodes, &node_count);
HuffmanCode codes[256];
generate_huffman_codes(nodes, node_count, codes, 0, 0, 0);
// Write header (frequencies)
int output_len = 0;
memcpy(output, frequencies, sizeof(frequencies));
output_len += sizeof(frequencies);
// Encode data
unsigned int bit_buffer = 0;
int bits_in_buffer = 0;
for (int i = 0; i < rle_len; i++) {
unsigned char symbol = rle_buffer[i];
HuffmanCode code = codes[symbol];
bit_buffer = (bit_buffer << code.bits) | code.code;
bits_in_buffer += code.bits;
while (bits_in_buffer >= 8) {
output[output_len++] = (bit_buffer >> (bits_in_buffer - 8)) & 0xFF;
bits_in_buffer -= 8;
}
}
// Flush remaining bits
if (bits_in_buffer > 0) {
output[output_len++] = (bit_buffer << (8 - bits_in_buffer)) & 0xFF;
}
free(rle_buffer);
return output_len;
}
int rle_huffman_decode(const unsigned char *input, int input_len, unsigned char *output) {
int frequencies[256];
memcpy(frequencies, input, sizeof(frequencies));
input += sizeof(frequencies);
HuffmanNode nodes[MAX_TREE_NODES];
int node_count;
build_huffman_tree(frequencies, nodes, &node_count);
HuffmanCode codes[256];
generate_huffman_codes(nodes, node_count, codes, 0, 0, 0);
int output_len = 0;
unsigned int bit_buffer = 0;
int bits_in_buffer = 0;
int rle_len = 0;
unsigned char *rle_buffer = (unsigned char *)malloc(input_len * 2);
int rle_output_len = 0;
while (rle_len < input_len) {
bit_buffer = (bit_buffer << 8) | input[rle_len++];
bits_in_buffer += 8;
while (bits_in_buffer >= 8) {
unsigned char symbol = 0;
for (symbol = 0; symbol < 256; symbol++) {
if (codes[symbol].bits <= bits_in_buffer &&
(bit_buffer >> (bits_in_buffer - codes[symbol].bits)) == codes[symbol].code) {
rle_buffer[rle_output_len++] = symbol;
bits_in_buffer -= codes[symbol].bits;
bit_buffer &= (1 << bits_in_buffer) - 1;
break;
}
}
}
}
int i = 0;
while (i < rle_output_len) {
int count = rle_buffer[i++];
unsigned char value = rle_buffer[i++];
for (int j = 0; j < count; j++) {
output[output_len++] = value;
}
}
free(rle_buffer);
return output_len;
}
代码注意事项:
rle_huffman_encode 中 malloc 未检查返回值;rle_buffer 大小为 input_len * 2,但 RLE 输出最坏情况(无重复)为 input_len * 2,刚好够用,若 RLE 实现有额外头部则不够。
- Huffman 建树使用 O(n²) 的线性扫描找最小值,对 256 个符号可接受,但代码中
min1/min2 的初始化逻辑在 node_count == 2 时未遍历 i=2,结果正确但可读性差。
- 解码函数存在严重逻辑问题:(a) 内层
while (bits_in_buffer >= 8) 的阈值意味着即使缓冲区中已有足够位解码一个短码字(如 1–7 位),也不会触发解码,导致解码停滞或位对齐错乱;(b) 对每个待解码符号遍历 256 个码字做匹配,时间复杂度为 O(n × 256),效率极低;(c) bit_buffer &= (1 << bits_in_buffer) - 1 在 bits_in_buffer 接近 32 时存在整型溢出风险。正确做法应使用解码树或查表法,并按实际码长判断是否可解码。
- 解码端
rle_len < input_len 的循环条件中,input 已跳过 1024 字节头部,但 input_len 未相应减去,会多读 1024 字节。
- 整体代码为教学示意,不可直接用于生产环境。
五、对比速查表
以下压缩率和速度为量级参考,实际数值强烈依赖图像内容、目标平台(CPU 主频、缓存、总线宽度)和具体实现优化程度,不应作为硬性指标。
| 算法 | 典型压缩率 | 解码速度(量级) | 适用场景 |
|---|
| RLE-4 | 2× – 10× | 非常快 | 图标、按钮、简单背景 |
| SRLE | 5× – 20×(大面积纯色时) | 非常快 | 大面积单色区域 |
| miniLZO | 2× – 3× | 数百 KB/s – 1 MB/s | 动态加载图像 |
| FastLZ | 3× – 5× | 数百 KB/s – 1 MB/s | 动态加载、较大 UI 元素 |
| miniz (DEFLATE) | 5× – 10× | 数百 KB/s | 存储受限、CPU 充足 |
| QOI | 2× – 3×(接近无滤波 PNG) | 1 MB/s – 3 MB/s | 快速解码 + 接近 PNG 效果 |
| RLE + Huffman | 3× – 8× | 数百 KB/s – 1 MB/s | 含重复区域的大图 |
注:"压缩率"指原始大小 / 压缩后大小。RLE 类算法在纯色区域可远超上表上限,在复杂图像中可能 < 1(即膨胀)。LZ 类算法对随机数据同样会膨胀。
六、选型建议
按典型场景给出推荐,实际选择还需结合你的 Flash 预算、RAM 余量、CPU 主频和 UI 刷新率综合判断:
简单 UI 元素(图标、按钮、小图形)
- 推荐:RLE-4 或 SRLE
- 理由:数据量小(通常 < 4 KB),解码几乎无延迟,实现简单,无需额外 RAM。
较大界面背景图
- 推荐:QOI 或 miniLZO
- 理由:平衡了压缩率和解码速度;QOI 对 RGBA 图像天然友好,miniLZO 对任意字节流通用。
存储空间极其受限
- 推荐:miniz(DEFLATE)或 RLE + Huffman
- 理由:追求最高压缩率,牺牲一定解码速度;前提是 CPU 有足够余量完成解码。
动态加载大量图片(如滚动列表、多页面切换)
- 推荐:FastLZ 或 miniLZO
- 理由:解码速度优先,确保 UI 响应流畅;压缩率适中即可,因为图片会频繁读写。
七、风险与边界提醒
- 缓冲区安全:本文所有参考代码均未对输出缓冲区做边界检查。实际实现中必须传入
output_capacity 参数并在每次写入前校验,否则存在栈/堆溢出风险。
- 输入校验:解码端应验证输入长度、魔数(如有)、以及游程/匹配参数是否在合法范围内,防止恶意或损坏数据导致越界。
- 平台依赖:速度数据与 CPU 主频、缓存大小、总线位宽强相关。在 8 MHz MCU 和 1 GHz SoC 上,同一算法的绝对速度可差两个数量级。建议在自己的目标硬件上实测。
- 压缩率不可预测:上表为经验量级。对具体图片,务必在目标数据上实测压缩率,避免"压缩后反而更大"的情况(RLE 对无重复数据、LZ 对高熵数据均可能膨胀)。
- 代码成熟度:本文代码为简化示意,存在逻辑不一致、缺少错误处理等问题。生产环境请使用经过审计的开源库(如 LZO、miniz、QOI 官方实现),或自行实现后做 fuzz 测试。
- 并发与线程安全:若 UI 线程和渲染线程同时访问解码缓冲区,需加锁或使用双缓冲。本文代码未考虑并发。
八、验证建议
- 用目标 UI 图片集(覆盖图标、按钮、背景、渐变)分别测试各算法的压缩率和解码耗时。
- 对解码器做边界测试:空输入、单字节、全相同字节、最大游程、损坏数据。
- 在目标 MCU 上测量峰值 RAM 占用(哈希表、临时缓冲区)。
- 若使用 Huffman,验证码字前缀无冲突(Kraft 不等式)。
小结
嵌入式 GUI 图片压缩没有"万能最优解"。纯色多的图标用 RLE 最省事;通用字节流用 LZO/FastLZ 兼顾速度和压缩率;需要接近 PNG 效果且解码要快时 QOI 是轻量选择;Flash 极度紧张时再考虑 DEFLATE 或混合方案。关键是先明确自己的约束(Flash 预算、RAM 余量、CPU 余量、刷新率),再在对应维度上选最合适的算法,并在目标硬件上实测验证。
<!-- csdn-article-id: 144375263 -->
本文最初于 2024/12/10 发布在 CSDN。