4. LVGL 内存监控:lv_mem_monitor、lv_mem_usage、调试日志
内存这东西,看不见摸不着,但出了问题就让你头疼。我见过不少项目,界面跑起来花里胡哨,结果运行几个小时就卡死——一查,内存泄漏了。所以这一章,咱们就聊聊怎么把 LVGL 的内存状态「看」清楚。
LVGL 提供了几个非常实用的监控工具,说白了就是三把刀:lv_mem_monitor 看全局、lv_mem_usage 查细节、调试日志抓异常。用好它们,内存问题基本无所遁形。
核心观点:不要等到内存溢出才去排查。养成监控习惯,比任何优化技巧都重要。
4.1 lv_mem_monitor:全局内存快照
这个函数是我用得最多的。它就像给内存拍了一张 X 光片,一眼就能看出当前内存池的整体状况。
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
LV_LOG_USER("总大小: %d, 已用: %d, 空闲: %d, 碎片率: %d%%",
mon.total_size, mon.used_size,
mon.free_size, mon.frag_pct);
结构体 lv_mem_monitor_t 里藏着几个关键字段:
| 字段 | 含义 | 我关注的点 |
|---|---|---|
total_size |
内存池总大小 | 确认配置是否合理 |
used_size |
当前已使用字节数 | 看有没有异常增长 |
free_size |
剩余空闲字节数 | 别等到 0 才慌 |
frag_pct |
碎片率百分比 | 超过 30% 就要警惕 |
free_biggest_size |
最大连续空闲块 | 比 free_size 更真实 |
我的习惯:在 main loop 里每 5 秒调用一次 lv_mem_monitor,把结果通过串口打印出来。这样即使界面卡死,也能从日志里看到最后的内存状态。
碎片率这个指标,很多人容易忽略。你想想看,free_size 显示还有 10KB,但 free_biggest_size 只有 1KB——这意味着你连一个 2KB 的位图都分配不出来。我曾经在一个产品上遇到过这种情况,界面切换几次后突然白屏,查了半天才发现是碎片把内存池堵死了。
4.2 lv_mem_usage:精准定位峰值
lv_mem_monitor 看的是当前状态,但有些内存问题只在特定操作时出现。比如打开某个页面、加载一张大图。这时候就需要 lv_mem_usage 来抓峰值。
uint32_t before = lv_mem_usage();
// 执行某个操作,比如加载页面
lv_obj_t *scr = lv_obj_create(NULL);
lv_obj_set_style_bg_color(scr, lv_color_hex(0x123456), 0);
// ... 其他操作
uint32_t after = lv_mem_usage();
LV_LOG_USER("该操作消耗内存: %d 字节", after - before);
用法很简单:操作前后各取一次值,差值就是这次操作的内存开销。我个人习惯在测试每个页面时,把这段代码包在宏里面:
#define TRACK_MEM_USAGE(desc, code) do { \
uint32_t __before = lv_mem_usage(); \
code; \
uint32_t __after = lv_mem_usage(); \
LV_LOG_USER("[%s] 消耗 %d 字节", desc, __after - __before); \
} while(0)
// 使用示例
TRACK_MEM_USAGE("创建主页面", {
lv_obj_t *main_scr = lv_obj_create(NULL);
lv_scr_load(main_scr);
});
注意:lv_mem_usage 返回的是当前分配的总字节数,不是增量。所以一定要在操作前后各取一次做差。我曾经见过有人只取一次值就下结论,结果闹了笑话。
4.3 调试日志:让内存问题自己开口
监控函数只能告诉你「内存出问题了」,但具体是谁干的?什么时候干的?这就需要调试日志出马了。
LVGL 的日志系统分几个等级:
- LV_LOG_LEVEL_TRACE:最详细,会打印每次内存分配和释放。调试时打开,发布时关掉。
- LV_LOG_LEVEL_INFO:常规信息,适合打印监控数据。
- LV_LOG_LEVEL_WARN:警告,比如内存不足但还能撑。
- LV_LOG_LEVEL_ERROR:错误,比如分配失败。
要开启内存相关的详细日志,需要在 lv_conf.h 里做两件事:
/* 1. 把日志等级调到最低 */
#define LV_LOG_LEVEL LV_LOG_LEVEL_TRACE
/* 2. 开启内存追踪 */
#define LV_MEM_CUSTOM 0
#define LV_MEM_BUF_MAX_NUM 16
然后你会在串口里看到类似这样的输出:
[TRACE] lv_mem.c: 分配 128 字节,地址 0x20001234
[TRACE] lv_mem.c: 释放 0x20001234,大小 128 字节
[TRACE] lv_mem.c: 分配 256 字节,地址 0x200012B0
[WARN] lv_mem.c: 碎片率 35%,建议整理
嗯,这里要注意:生产环境千万别开 TRACE 等级,否则日志刷屏能把系统拖慢。我一般只在调试阶段开,定位到问题后就关掉。
4.4 实战:用监控定位内存泄漏
光说不练假把式。咱们来看一个真实场景:
假设你的界面每隔 10 秒切换一次页面,跑了一小时后突然卡死。怎么查?
- 先加监控:在切换页面的代码前后调用
lv_mem_monitor,打印used_size。 - 观察趋势:如果每次切换后
used_size都在增长,说明有泄漏。 - 开日志:把日志等级调到 TRACE,看每次切换时分配和释放是否成对出现。
- 定位代码:找到只有分配没有释放的地方——多半是创建了对象但忘了删除。
// 错误示例:每次切换都创建新页面,但不删除旧页面
void switch_to_page_a(void) {
lv_obj_t *page = lv_obj_create(lv_scr_act());
// ... 添加控件
// 忘记保存 page 指针,下次切换无法删除
}
// 正确做法:保存页面指针,切换时先删除旧页面
static lv_obj_t *current_page = NULL;
void switch_to_page_a(void) {
if (current_page) {
lv_obj_del(current_page);
}
current_page = lv_obj_create(lv_scr_act());
// ... 添加控件
}
避坑指南:我曾经在一个项目里遇到「隐性泄漏」——每次切换页面时 used_size 只增长几十字节,但跑一天后内存就爆了。后来发现是定时器回调里创建了临时样式,但忘了释放。这种小泄漏最坑人,因为单次增量太小,不用监控根本发现不了。
4.5 监控策略总结
最后,我把这三把刀的使用场景整理了一下:
| 工具 | 最佳使用场景 | 调用频率 |
|---|---|---|
lv_mem_monitor |
全局健康检查、碎片监控 | 每 1-5 秒一次 |
lv_mem_usage |
验证单个操作的内存开销 | 按需调用 |
| 调试日志 (TRACE) | 定位泄漏点、追踪异常分配 | 调试阶段开启 |
说白了,监控不是目的,目的是让你对内存状态心里有数。你想想看,一个嵌入式设备跑在产线上,出了问题你连日志都看不到,那才叫抓瞎。所以,趁现在就把监控加进去,养成习惯,后面会省很多事。