4. 内存碎片化实战:碎片产生的原因分析,使用 lv_mem_defrag 进行碎片整理,碎片率监控指标
内存碎片化,说白了就是你的内存被切得七零八落。明明总空闲空间还够,但就是分配不出一个连续的大块。我在项目里见过太多次这种场景——界面突然卡死,或者某个控件加载不出来,查到最后都是碎片在作怪。
这一节,我们就来彻底搞懂碎片是怎么来的,怎么量化它,以及 LVGL 提供的那个“碎片整理”工具到底怎么用。
4.1 碎片是怎么产生的?
先看一个最简单的场景。你分配了 100 字节,用完了释放掉。再分配 50 字节,再释放。反复几次,内存里就会出现很多小空洞。这些小空洞就是碎片。
LVGL 默认使用动态内存分配,每次创建控件、加载图片、设置字体,都会从堆里切一块出来。释放的时候,这块内存就变成了空闲块。但空闲块的大小和位置是随机的,久而久之,大的空闲块被切碎,就形成了碎片。
我个人习惯把碎片分成两类:
| 碎片类型 | 产生原因 | 典型场景 |
|---|---|---|
| 内部碎片 | 分配器按对齐粒度切块,实际分配比请求大 | 请求 33 字节,实际分配 36 字节(4 字节对齐) |
| 外部碎片 | 多次分配/释放后,空闲块不连续 | 频繁创建/销毁控件、动态加载/卸载图片 |
内部碎片是分配器机制决定的,你很难完全消除。外部碎片才是我们优化的重点。
核心结论:外部碎片的增长速度,跟你的分配/释放频率成正比。如果你在界面上频繁切换页面、动态加载资源,碎片就会快速累积。
4.2 碎片率监控指标
光知道有碎片还不够,你得能量化它。LVGL 提供了几个 API 来获取内存状态,我们可以用它们计算碎片率。
我常用的监控指标有三个:
- 总空闲率:空闲总大小 / 总堆大小。这个指标太粗糙,不能反映碎片程度。
- 最大连续块占比:最大连续空闲块 / 总空闲大小。这个指标很关键——占比越低,碎片越严重。
- 碎片率: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 图展示了碎片整理前后的内存布局变化。你可以直观地看到,分散的小空闲块被合并成了一个大的连续区域。
4.5 避坑指南与最佳实践
关于碎片整理,我踩过不少坑。这里分享几个关键点:
- 不要频繁调用 defrag:整理本身有开销,而且会暂停 UI 响应。我一般只在碎片率超过 50% 时才触发。
- 整理后要重新获取指针:如果你在整理前保存了某个控件的指针,整理后这个指针就失效了。最好在整理前销毁所有临时控件。
- 配合内存池使用:对于频繁创建/销毁的小对象(比如列表项、按钮),用
lv_mem_buf_get()从内存池分配,可以大幅减少碎片。
我的经验:在一个 128KB RAM 的 MCU 上跑 LVGL,如果不做任何碎片管理,连续切换 20 次页面后碎片率就会超过 60%。加上 defrag 和内存池后,这个数字降到了 15% 以下。说白了,碎片整理不是银弹,但配合良好的分配策略,效果非常明显。