4、内存使用分析:LVGL内存池机制,如何监控动态内存使用情况
内存,说白了就是嵌入式图形系统的命根子。我见过太多项目,界面画得挺漂亮,一跑起来就卡死、花屏、甚至直接复位。十有八九,问题都出在内存上。
LVGL 为了在资源受限的 MCU 上跑起来,设计了一套自己的内存池机制。今天我们就把它扒开看看,再聊聊怎么监控动态内存的使用情况。
4.1 LVGL 的内存池机制
LVGL 没有直接用标准库的 malloc 和 free。为什么?因为标准库的分配器在频繁申请释放小块内存时,会产生大量碎片。你想想看,一个嵌入式设备可能连续运行几个月,碎片一多,内存就慢慢“漏”光了。
LVGL 的做法是:自己维护一个内存池。它有两种模式:
- LV_MEM_CUSTOM = 0:使用 LVGL 内置的内存分配器。默认情况下,它会从一个大数组中切内存。
- LV_MEM_CUSTOM = 1:使用你系统自带的
malloc/free。比如跑在 Linux 或 RTOS 上时,可以这样配置。
我个人习惯用内置分配器。它采用了一种叫“首次适应”的算法,说白了就是从头扫描空闲链表,找到第一个足够大的块就分配出去。释放时会把相邻的空闲块合并,减少碎片。
核心配置参数(在 lv_conf.h 中):
/* 内存池大小,单位字节 */
#define LV_MEM_SIZE (64U * 1024U) /* 64KB */
/* 使用内置分配器 */
#define LV_MEM_CUSTOM 0
/* 内存对齐,一般 4 字节或 8 字节 */
#define LV_MEM_ATTR
#define LV_MEM_POOL_INCLUDE
#define LV_MEM_POOL_EXTERN
嗯,这里要注意:LV_MEM_SIZE 设多大合适?我见过有人直接拍脑袋设个 32KB,结果界面稍微复杂点就崩了。我的经验是:先设大一点(比如 128KB),跑起来后用监控工具看实际峰值,再往回缩。
4.2 如何监控动态内存使用情况
LVGL 提供了一套非常实用的监控 API。你不用猜,直接看数字就行。
4.2.1 基础监控函数
这几个函数我几乎每个项目都会用:
| 函数 | 作用 |
|---|---|
lv_mem_monitor_t |
内存监控数据结构体 |
lv_mem_monitor(&mon) |
获取当前内存使用快照 |
lv_mem_get_free() |
获取剩余空闲字节数 |
lv_mem_get_used() |
获取已使用字节数 |
lv_mem_get_peak() |
获取历史峰值使用量 |
使用示例:
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
LV_LOG_USER("总大小: %d bytes", mon.total_size);
LV_LOG_USER("已使用: %d bytes", mon.used_size);
LV_LOG_USER("空闲: %d bytes", mon.free_size);
LV_LOG_USER("碎片率: %d%%", mon.frag_pct);
LV_LOG_USER("最大空闲块: %d bytes", mon.free_biggest_size);
这里有个关键指标:碎片率。如果碎片率超过 30%,你就要警惕了。我曾经在一个项目里发现碎片率飙到 45%,界面切换几次后就申请不到内存了。后来排查发现是频繁创建和销毁小控件导致的。
我的小技巧:在调试阶段,我会用一个定时器每 5 秒打印一次内存状态。这样能直观看到内存的“呼吸”情况——正常应用应该是有升有降,但总体平稳。如果只升不降,那就是内存泄漏了。
4.2.2 内存泄漏检测
LVGL 内置了一个很实用的功能:内存泄漏检测。开启方法:
/* 在 lv_conf.h 中 */
#define LV_USE_PERF_MONITOR 1
#define LV_USE_MEM_MONITOR 1
然后你可以调用:
/* 打印所有未释放的内存块信息 */
lv_mem_dump();
这个函数会列出所有当前仍被占用的内存块,包括它们的地址、大小和调用位置。我一般会在退出某个界面时调用一次,看看有没有忘记释放的对象。
注意:lv_mem_dump() 只在调试模式下有效。发布版本一定要关掉,否则会泄露调试信息。
4.3 实战:内存监控示例
下面是一个完整的监控示例。我习惯把它做成一个调试界面,按一个按键就能看到当前内存状态:
static void mem_monitor_task(lv_timer_t *timer)
{
lv_mem_monitor_t mon;
lv_mem_monitor(&mon);
static char buf[128];
lv_snprintf(buf, sizeof(buf),
"总大小: %d KB\n"
"已使用: %d KB (%d%%)\n"
"空闲: %d KB\n"
"碎片率: %d%%\n"
"最大空闲块: %d KB",
mon.total_size / 1024,
mon.used_size / 1024,
mon.used_size * 100 / mon.total_size,
mon.free_size / 1024,
mon.frag_pct,
mon.free_biggest_size / 1024);
/* 假设有一个 label 叫 mem_label */
lv_label_set_text(mem_label, buf);
}
/* 创建定时器,每秒更新一次 */
lv_timer_create(mem_monitor_task, 1000, NULL);
你看,这样你就能实时看到内存的变化了。我曾在调试一个动画时发现,每次动画循环都会多占用 200 字节,而且不释放。后来发现是动画回调里创建了一个临时样式,忘记删除了。
4.4 常见内存问题与避坑指南
- 问题一:内存碎片化严重
我曾经在一个需要频繁更新列表的项目中遇到这个问题。每次刷新列表都重新创建所有项,碎片率很快就上去了。
解决方案:改用对象池。预先创建一批控件,需要时隐藏/显示,而不是销毁/创建。
- 问题二:忘记释放对象
LVGL 的对象删除后,关联的样式、图片等资源不会自动释放。你需要手动调用
lv_obj_del()或lv_obj_clean()。避坑指南:我习惯在删除对象前,先调用
lv_mem_dump()看看当前内存状态,删除后再看一次。两次对比,就能知道有没有漏掉。 - 问题三:图片解码占用大量内存
一张 320x240 的 32 位真彩色图片,解码后需要 300KB 内存。如果你的 MCU 只有 64KB 内存池,那肯定爆了。
解决方案:使用 LVGL 的图片缓存机制,或者用压缩格式(如 PNG、JPEG)。我一般用
lv_img_cache_set_size()限制缓存大小。
核心总结:
- LVGL 使用内存池管理动态内存,避免标准库的碎片问题。
- 用
lv_mem_monitor()实时监控内存使用情况。 - 碎片率超过 30% 就要警惕,考虑改用对象池。
- 调试阶段开启
lv_mem_dump()检查内存泄漏。 - 图片解码是内存大户,务必控制缓存大小。
嗯,内存这块其实不难,关键是要养成监控的习惯。别等到跑起来出问题了再回头查,那时候就晚了。下一章我们会聊聊 CPU 占用率的分析,看看怎么让界面跑得更流畅。