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移植中,90%的内存泄漏集中在忘记释放和异常路径未处理两类

这张图把三个核心概念串起来了。你注意看,左边"什么是泄漏"是定义,中间"危害"是后果,右边"常见原因"是根因。排查内存泄漏,本质上就是从后果反推根因的过程。

好了,这一章的基础概念就讲到这里。记住一句话:嵌入式GUI的内存泄漏,不是会不会发生的问题,而是什么时候发生、你能不能快速定位的问题。后面的章节,我会带你一步步掌握排查工具和实战技巧。