4. LVGL 内存监控:lv_mem_monitor、lv_mem_usage、调试日志

内存这东西,看不见摸不着,但出了问题就让你头疼。我见过不少项目,界面跑起来花里胡哨,结果运行几个小时就卡死——一查,内存泄漏了。所以这一章,咱们就聊聊怎么把 LVGL 的内存状态「看」清楚。

LVGL 提供了几个非常实用的监控工具,说白了就是三把刀:lv_mem_monitor 看全局、lv_mem_usage 查细节、调试日志抓异常。用好它们,内存问题基本无所遁形。

核心观点:不要等到内存溢出才去排查。养成监控习惯,比任何优化技巧都重要。

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 秒切换一次页面,跑了一小时后突然卡死。怎么查?

  1. 先加监控:在切换页面的代码前后调用 lv_mem_monitor,打印 used_size。
  2. 观察趋势:如果每次切换后 used_size 都在增长,说明有泄漏。
  3. 开日志:把日志等级调到 TRACE,看每次切换时分配和释放是否成对出现。
  4. 定位代码:找到只有分配没有释放的地方——多半是创建了对象但忘了删除。
// 错误示例:每次切换都创建新页面,但不删除旧页面
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) 定位泄漏点、追踪异常分配 调试阶段开启

说白了,监控不是目的,目的是让你对内存状态心里有数。你想想看,一个嵌入式设备跑在产线上,出了问题你连日志都看不到,那才叫抓瞎。所以,趁现在就把监控加进去,养成习惯,后面会省很多事。