3. 常见泄漏场景分析:未释放的 malloc/calloc、事件循环中的累积泄漏、图片缓存泄漏
内存泄漏这东西,说白了就是「借了不还」。在嵌入式 GUI 里,资源本来就紧巴巴的,泄漏一点可能当时看不出来,跑个几天就崩了。我这些年排查过的泄漏,十有八九都逃不出下面这三类。咱们一个一个掰开看。
核心观点:嵌入式 GUI 的内存泄漏,90% 集中在动态分配未释放、循环累积、缓存失控这三个场景。抓住它们,就抓住了排查的主线。
3.1 未释放的 malloc / calloc
这是最基础、也最容易被忽视的一类。你想想看,调了 malloc 或者 calloc,用完之后忘了 free,这块内存就永远回不来了。在桌面程序里可能还能撑一阵,但在嵌入式环境里,几次泄漏就能把堆耗光。
我遇到过最典型的例子:一个按键弹窗的绘制函数,每次弹窗都 malloc 一块缓冲区来存位图数据,弹窗关闭时却忘了 free。用户按几次按键,内存就爆了。
// 错误示例:只分配,不释放
void show_popup(const char *text) {
char *buf = (char *)malloc(256);
if (buf) {
snprintf(buf, 256, "Popup: %s", text);
draw_text(buf); // 绘制后忘记 free(buf)
}
}
// 正确做法:谁分配,谁释放
void show_popup(const char *text) {
char *buf = (char *)malloc(256);
if (buf) {
snprintf(buf, 256, "Popup: %s", text);
draw_text(buf);
free(buf); // 释放
buf = NULL; // 置空,防止野指针
}
}
我的习惯:每次写 malloc 的时候,我习惯先写 free 再写中间逻辑。这样就不会忘了。你可以试试这个「先写释放」的编码习惯。
还有一种情况更隐蔽——条件分支里的泄漏。比如函数中间提前 return 了,后面的 free 就没执行到。
// 条件分支泄漏
char *load_config(const char *path) {
char *data = (char *)malloc(1024);
if (!data) return NULL;
FILE *fp = fopen(path, "r");
if (!fp) {
return NULL; // 泄漏!data 没释放
}
// ... 读取数据
fclose(fp);
return data; // 调用方负责释放
}
避坑指南:我曾经在一个项目里排查了整整两天,最后发现是一个 realloc 失败后,原指针没释放,导致内存泄漏。记住:realloc 返回 NULL 时,原内存块仍然存在,必须手动 free。
3.2 事件循环中的累积泄漏
GUI 程序的核心是事件循环。每次触屏、每次按键、每次定时器触发,都可能产生一次内存分配。如果这些分配在循环结束时没有清理干净,就会像水龙头没关紧一样——一滴一滴地漏,一天下来就是一大桶。
我见过一个血淋淋的例子:一个智能家居面板,每次触摸事件都会创建一个 EventData 结构体,用 malloc 分配,然后丢进消息队列。消息处理完后,处理函数忘了 free 这个结构体。设备运行 8 小时后,触摸完全失灵。
// 事件循环中的累积泄漏
void event_loop() {
while (1) {
Event *ev = get_event(); // 从队列取事件
if (ev) {
EventData *data = (EventData *)malloc(sizeof(EventData));
data->type = ev->type;
data->timestamp = get_tick();
// 处理事件...
process_event(data);
// 忘记 free(data) —— 每次循环泄漏 sizeof(EventData) 字节
}
}
}
这种泄漏的特点是:单次泄漏量很小,但频率很高。你很难在短时间测试里发现它,必须用长时间压力测试才能暴露。
| 泄漏类型 | 单次泄漏量 | 泄漏频率 | 暴露时间 |
|---|---|---|---|
| malloc 未释放 | 大(KB ~ MB) | 低(几次操作) | 几分钟 |
| 事件循环累积 | 小(几十 ~ 几百字节) | 高(每秒多次) | 几小时 ~ 几天 |
| 图片缓存泄漏 | 中等(几 KB ~ 几百 KB) | 中(页面切换时) | 几小时 |
排查技巧:对于事件循环泄漏,我建议在每次循环的入口和出口打印堆使用量。如果出口比入口多几个字节,那就是泄漏了。用 mallinfo() 或者自己维护一个分配计数器,很管用。
3.3 图片缓存泄漏
嵌入式 GUI 里,图片是内存消耗大户。一张 800x480 的 32 位 RGBA 位图,就要 1.5MB 左右。如果缓存管理不当,泄漏起来非常快。
常见的场景是这样的:页面切换时,新页面加载图片到缓存,旧页面释放缓存。但如果某个页面忘记释放,或者缓存替换策略有 bug,图片就会一直占用内存。
// 图片缓存泄漏示例
typedef struct {
char name[32];
uint8_t *data;
uint32_t size;
} ImageCache;
ImageCache cache[10]; // 固定大小缓存
void load_image(const char *name) {
// 查找空闲槽位
for (int i = 0; i < 10; i++) {
if (cache[i].data == NULL) {
cache[i].data = (uint8_t *)malloc(image_size(name));
// 加载图片数据...
strncpy(cache[i].name, name, 32);
return;
}
}
// 缓存满了,但没有替换策略 —— 旧图片永远不会被释放
// 新图片加载失败,旧图片占着内存不放
}
我遇到过更坑的:一个图片解码库,每次解码都 malloc 输出缓冲区,但解码失败时直接返回错误码,不释放已分配的缓冲区。调用方以为失败了就没数据,结果内存被悄悄吃掉了。
避坑指南:我曾经在一个项目里,图片缓存用了 LRU 算法,但实现时把「最近使用」和「最近加载」搞混了。结果最常用的图片反而被优先淘汰,每次都要重新加载和分配,内存碎片越来越严重。最后堆上全是小碎片,连一块完整的 1MB 都分配不出来。
图片缓存泄漏的另一个重灾区是缩略图生成。很多 GUI 框架会在后台生成缩略图,如果缩略图生成后没有正确关联到缓存生命周期,就会产生大量孤儿内存块。
// 缩略图泄漏
Thumbnail *create_thumbnail(const char *path) {
Thumbnail *thumb = (Thumbnail *)malloc(sizeof(Thumbnail));
thumb->data = decode_thumbnail(path); // 内部 malloc
// 如果 decode_thumbnail 失败,thumb 本身泄漏
// 如果成功但后续插入缓存失败,thumb->data 泄漏
return thumb;
}
3.4 三类泄漏的对比与排查思路
这三类泄漏,说白了就是三个不同的「漏点」:一次性忘记释放、高频小量累积、缓存管理失控。它们的排查方法也不一样。
- malloc/calloc 未释放:用静态代码分析工具扫一遍,或者手动 review 每个 malloc 对应的 free 路径。重点关注条件分支和错误处理路径。
- 事件循环累积:用压力测试 + 堆监控。让设备跑一晚上,每小时记录一次堆使用量。如果曲线一直往上走,就是累积泄漏。
- 图片缓存泄漏:检查缓存的插入、淘汰、清空三个操作。重点看缓存满时的处理逻辑,以及页面销毁时是否释放了关联的图片内存。