4. 内存碎片化实战:碎片产生的原因分析,使用 lv_mem_defrag 进行碎片整理,碎片率监控指标

内存碎片化,说白了就是你的内存被切得七零八落。明明总空闲空间还够,但就是分配不出一个连续的大块。我在项目里见过太多次这种场景——界面突然卡死,或者某个控件加载不出来,查到最后都是碎片在作怪。

这一节,我们就来彻底搞懂碎片是怎么来的,怎么量化它,以及 LVGL 提供的那个“碎片整理”工具到底怎么用。

4.1 碎片是怎么产生的?

先看一个最简单的场景。你分配了 100 字节,用完了释放掉。再分配 50 字节,再释放。反复几次,内存里就会出现很多小空洞。这些小空洞就是碎片。

LVGL 默认使用动态内存分配,每次创建控件、加载图片、设置字体,都会从堆里切一块出来。释放的时候,这块内存就变成了空闲块。但空闲块的大小和位置是随机的,久而久之,大的空闲块被切碎,就形成了碎片。

我个人习惯把碎片分成两类:

碎片类型 产生原因 典型场景
内部碎片 分配器按对齐粒度切块,实际分配比请求大 请求 33 字节,实际分配 36 字节(4 字节对齐)
外部碎片 多次分配/释放后,空闲块不连续 频繁创建/销毁控件、动态加载/卸载图片

内部碎片是分配器机制决定的,你很难完全消除。外部碎片才是我们优化的重点。

核心结论:外部碎片的增长速度,跟你的分配/释放频率成正比。如果你在界面上频繁切换页面、动态加载资源,碎片就会快速累积。

4.2 碎片率监控指标

光知道有碎片还不够,你得能量化它。LVGL 提供了几个 API 来获取内存状态,我们可以用它们计算碎片率。

我常用的监控指标有三个:

  1. 总空闲率:空闲总大小 / 总堆大小。这个指标太粗糙,不能反映碎片程度。
  2. 最大连续块占比:最大连续空闲块 / 总空闲大小。这个指标很关键——占比越低,碎片越严重。
  3. 碎片率:1 - (最大连续空闲块 / 总空闲大小)。这个值越接近 1,说明内存越碎。

来看一段实际的监控代码:

void monitor_fragmentation(void)
{
    lv_mem_monitor_t mon;
    lv_mem_monitor(&mon);

    uint32_t total_free = mon.free_size;
    uint32_t max_contiguous = mon.free_biggest_size;
    float frag_ratio = 1.0f - ((float)max_contiguous / (float)total_free);

    LV_LOG_USER("总空闲: %d bytes", total_free);
    LV_LOG_USER("最大连续块: %d bytes", max_contiguous);
    LV_LOG_USER("碎片率: %.2f%%", frag_ratio * 100.0f);

    if (frag_ratio > 0.5f) {
        LV_LOG_WARN("碎片率超过 50%%,建议执行碎片整理");
    }
}

我在实际项目中,会把碎片率超过 30% 作为预警线,超过 50% 就触发整理。你想想看,如果最大连续块只有总空闲的 20%,那你要分配一个稍大的 buffer 就很可能失败。

小技巧:把监控函数放到一个定时器里,每 5 秒执行一次。这样你可以看到碎片率随时间的变化曲线,定位到是哪个操作导致了碎片激增。

4.3 使用 lv_mem_defrag 进行碎片整理

LVGL 从 v8.3 开始提供了 lv_mem_defrag() 函数。它的原理很简单:把所有已分配的内存块向低地址移动,把空闲块合并到高地址,形成一个大的连续空闲区。

但这里有个坑——移动内存块会导致所有指针失效。所以 LVGL 要求你在调用 defrag 之前,必须确保没有正在使用的动态内存指针。

我建议的调用时机:

  • 页面切换完成后,旧页面所有控件都已销毁
  • 长时间空闲时(比如用户没有操作超过 30 秒)
  • 碎片率超过阈值后,主动触发一次

来看一个安全的调用示例:

void safe_defrag(void)
{
    // 确保没有活跃的 UI 操作
    lv_anim_del_all();           // 停止所有动画
    lv_task_handler();           // 处理完剩余任务
    lv_timer_handler();          // 处理完定时器

    LV_LOG_USER("开始碎片整理...");
    lv_mem_defrag();
    LV_LOG_USER("碎片整理完成");

    // 整理后重新获取状态
    lv_mem_monitor_t mon;
    lv_mem_monitor(&mon);
    LV_LOG_USER("整理后最大连续块: %d bytes", mon.free_biggest_size);
}

警告:千万不要在控件创建/销毁的过程中调用 defrag!我曾经在一个项目中,在按钮回调里直接调了 defrag,结果所有正在创建的控件指针都变成了野指针,直接 HardFault。一定要确保内存处于“静止”状态。

4.4 碎片整理前后对比

下面这张 SVG 图展示了碎片整理前后的内存布局变化。你可以直观地看到,分散的小空闲块被合并成了一个大的连续区域。

碎片整理前后内存布局对比 整理前: 已分配 120B 40B 已分配 80B 30B 已分配 100B 50B 已分配 60B 80B 已分配 90B 45B 整理后: 已分配 120B 已分配 80B 已分配 100B 已分配 60B 已分配 90B 合并空闲块 245B 整理前:5 个空闲块共 245B,但最大连续块只有 80B 整理后:所有空闲块合并为 1 个 245B 的连续块,分配成功率大幅提升

4.5 避坑指南与最佳实践

关于碎片整理,我踩过不少坑。这里分享几个关键点:

  • 不要频繁调用 defrag:整理本身有开销,而且会暂停 UI 响应。我一般只在碎片率超过 50% 时才触发。
  • 整理后要重新获取指针:如果你在整理前保存了某个控件的指针,整理后这个指针就失效了。最好在整理前销毁所有临时控件。
  • 配合内存池使用:对于频繁创建/销毁的小对象(比如列表项、按钮),用 lv_mem_buf_get() 从内存池分配,可以大幅减少碎片。

我的经验:在一个 128KB RAM 的 MCU 上跑 LVGL,如果不做任何碎片管理,连续切换 20 次页面后碎片率就会超过 60%。加上 defrag 和内存池后,这个数字降到了 15% 以下。说白了,碎片整理不是银弹,但配合良好的分配策略,效果非常明显。