1. 内存泄漏基础概念
各位同学好,我是老周。做嵌入式GUI移植这些年,我踩过最多的坑就是内存泄漏。今天咱们先把这个基础概念掰扯清楚。
说白了,内存泄漏就是程序向系统申请了一块内存,用完之后没还回去。你想想看,嵌入式设备的内存本来就金贵,几兆、几十兆的空间,GUI跑着跑着内存越来越少,最后系统直接崩了。
1.1 什么是内存泄漏
我习惯用一个比喻来解释:你去图书馆借书,借了不还。书架上能借的书就越来越少。内存泄漏也是这个道理——malloc 或 new 拿到的堆内存,没有对应的 free 或 delete。
看个最典型的例子:
void create_button_label(void)
{
char *label = (char *)malloc(64);
sprintf(label, "温度: %.1f°C", get_temperature());
// 这里应该 free(label),但忘了写
// 每次调用这个函数,就泄漏 64 字节
}
嗯,这段代码我见过太多次了。每次调用泄漏64字节,GUI界面每刷新一次调用一次,跑个半天,内存就耗光了。
核心定义:内存泄漏是指动态分配的内存块,在程序失去对其引用后,无法被回收再利用。泄漏的内存既不属于进程的可用堆,也不属于操作系统空闲内存。
1.2 泄漏的危害有多大
我在一个智能家居项目里吃过亏。GUI移植到某款国产芯片上,刚开机一切正常,触摸响应流畅。跑了大概6个小时,界面卡死,触摸没反应,串口打印也停了。
查到最后,就是一个弹窗动画的纹理缓存没释放。每次弹窗泄漏4KB,6小时弹了5000多次,20MB堆空间全被吃光。
具体危害我列一下:
- 系统性能下降 — 可用内存减少,malloc耗时越来越长,GUI帧率从60fps掉到十几fps
- 程序崩溃 — 堆空间耗尽,malloc返回NULL,没有判空就直接用,直接段错误
- 系统不稳定 — 内存碎片化严重,即使总空间够,也分配不出连续大块
- 设备寿命缩短 — 频繁的内存分配失败导致反复重启,Flash磨损加剧
- 安全风险 — 泄漏的内存可能包含敏感数据(密码、密钥),未被清零就暴露给后续分配
| 泄漏规模 | 运行时间 | 典型表现 |
|---|---|---|
| 少量(<1KB/次) | 数天~数周 | GUI响应变慢,偶发卡顿 |
| 中等(1~10KB/次) | 数小时~1天 | 界面刷新异常,触摸失灵 |
| 大量(>10KB/次) | 几分钟~1小时 | 系统直接死机或看门狗复位 |
我曾经踩过的坑:有个项目GUI移植到RTOS上,内存泄漏导致系统在凌晨3点准时崩溃。用户投诉说设备"半夜闹鬼"。查了三天才发现,是夜间模式的字体缓存没释放。从那以后,我养成了每个功能模块都做内存基线测试的习惯。
1.3 泄漏的常见原因
做GUI移植,泄漏原因其实有规律可循。我总结了六大类:
1.3.1 忘记释放
最直接的原因。申请了内存,但释放代码被遗漏了。尤其是在异常处理路径里:
void load_image(const char *path)
{
uint8_t *buf = (uint8_t *)malloc(1024 * 1024);
FILE *fp = fopen(path, "rb");
if (fp == NULL) {
// 这里忘记 free(buf) 就直接 return 了
return;
}
// ... 读取图片数据
fclose(fp);
free(buf); // 正常路径释放了
}
1.3.2 指针覆盖
保存内存地址的指针被重新赋值,原来的地址就找不到了。GUI里常见于链表操作:
widget_t *head = create_widget();
head = create_widget(); // 第一个widget的地址丢了,泄漏
1.3.3 异常路径未处理
函数中途return或goto,跳过了释放代码。GUI事件处理里特别多:
void on_touch_event(touch_event_t *ev)
{
char *data = (char *)malloc(256);
if (parse_event(ev, data) < 0) {
return; // 泄漏!应该先free再return
}
// ... 处理事件
free(data);
}
1.3.4 容器未正确清理
GUI框架里的链表、数组、哈希表,只释放了容器本身,没释放容器里的元素。我见过一个案例:
// 错误做法
list_t *widget_list = create_list();
// ... 往列表里添加了100个widget
free(widget_list); // 只释放了列表节点,100个widget对象全泄漏
1.3.5 循环引用
在C++的GUI框架里,父窗口持有子窗口指针,子窗口也持有父窗口指针。如果用了智能指针,引用计数永远降不到0。C语言里也有类似问题:
// 双向链表节点互相引用
typedef struct node {
struct node *prev;
struct node *next;
void *data;
} node_t;
// 释放时只free了节点,没free data,或者只遍历了单向
1.3.6 第三方库泄漏
移植GUI时,底层依赖的图形库、字体库、解码库可能有自己的内存管理问题。我遇到过某个JPEG解码库,解码大图时内部缓存没释放干净。
我的排查习惯:每移植一个第三方库,先用一个简单的测试用例跑1000次,对比malloc和free的调用次数。如果两者不相等,这个库就有泄漏嫌疑。别问我为什么知道——被坑多了自然就学会了。
1.4 知识体系总览
下面这张图把本章的核心逻辑串起来了。我建议你多看几遍,后面章节都会围绕这些概念展开。
这张图把三个核心概念串起来了。你注意看,左边"什么是泄漏"是定义,中间"危害"是后果,右边"常见原因"是根因。排查内存泄漏,本质上就是从后果反推根因的过程。
好了,这一章的基础概念就讲到这里。记住一句话:嵌入式GUI的内存泄漏,不是会不会发生的问题,而是什么时候发生、你能不能快速定位的问题。后面的章节,我会带你一步步掌握排查工具和实战技巧。