4. LVGL对象内存优化:对象创建与销毁的开销、对象复用策略、使用lv_obj_clean()与lv_obj_del()的正确时机
LVGL里的对象,说白了就是一个个小控件。你创建一个按钮,它就占一块内存。你删掉它,内存才还回来。听起来简单?但实际项目中,很多人就在这上面栽跟头。
我记得刚接手一个智能家居项目时,UI界面频繁切换,内存越用越多,最后直接卡死。查了半天,发现是对象创建和销毁的时机没把握好。嗯,这节课我们就来聊聊这个。
核心观点:LVGL对象的内存管理,关键在于「少创建、多复用、及时清理」。别让对象成为内存里的「僵尸」。
4.1 对象创建与销毁的开销
每次调用 lv_obj_create(),LVGL都会做这几件事:
- 从堆里分配一块内存给对象结构体
- 初始化对象的属性(大小、位置、样式等)
- 把对象挂到父对象的子链表上
- 触发一些内部事件
销毁时呢?lv_obj_del() 会反过来:
- 从父对象链表里摘除
- 释放子对象(递归删除)
- 释放样式、动画等附加资源
- 最后释放对象本身的内存
你看,一次创建+销毁,背后是一连串操作。如果频繁做,不仅慢,还容易产生内存碎片。
| 操作 | 时间开销(相对) | 内存碎片风险 | 适用场景 |
|---|---|---|---|
| lv_obj_create() | 中 | 低 | 初始化、页面加载 |
| lv_obj_del() | 高(递归子对象) | 中 | 页面销毁、控件移除 |
| lv_obj_clean() | 中(只删子对象) | 中 | 清空容器、复用父对象 |
| 对象复用 | 低 | 低 | 列表项、弹窗、频繁切换 |
我的经验:在资源紧张的MCU上,一次 lv_obj_del() 如果带几十个子对象,耗时可能达到几毫秒。如果放在UI线程里,用户能明显感觉到卡顿。
4.2 对象复用策略
为什么要复用?你想想看,一个列表有100项,用户上下滑动时,如果每项都创建再销毁,内存会像过山车一样波动。更糟的是,碎片越来越多,最后连一个按钮都创建不出来。
我个人习惯用「对象池」的思路。说白了,就是预先创建一批对象,用的时候拿出来,不用的时候藏回去,不销毁。
4.2.1 隐藏与显示
最简单的复用:用 lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN) 隐藏,用 lv_obj_clear_flag(obj, LV_OBJ_FLAG_HIDDEN) 显示。
// 创建时
lv_obj_t * btn = lv_btn_create(lv_scr_act());
lv_obj_t * label = lv_label_create(btn);
lv_label_set_text(label, "复用按钮");
// 隐藏(不是删除)
lv_obj_add_flag(btn, LV_OBJ_FLAG_HIDDEN);
// 需要时再显示
lv_obj_clear_flag(btn, LV_OBJ_FLAG_HIDDEN);
// 更新内容
lv_label_set_text(label, "新内容");
这样做的好处是:对象还在,内存还在,只是不绘制。切换时几乎没有开销。
4.2.2 对象池模式
对于列表、网格这类重复控件,我建议用对象池:
#define POOL_SIZE 10
lv_obj_t * obj_pool[POOL_SIZE];
uint8_t pool_index = 0;
// 初始化对象池
void pool_init(lv_obj_t * parent) {
for (int i = 0; i < POOL_SIZE; i++) {
obj_pool[i] = lv_btn_create(parent);
lv_obj_add_flag(obj_pool[i], LV_OBJ_FLAG_HIDDEN);
}
}
// 从池中取一个对象
lv_obj_t * pool_get(void) {
for (int i = 0; i < POOL_SIZE; i++) {
if (lv_obj_has_flag(obj_pool[i], LV_OBJ_FLAG_HIDDEN)) {
lv_obj_clear_flag(obj_pool[i], LV_OBJ_FLAG_HIDDEN);
return obj_pool[i];
}
}
return NULL; // 池已满
}
// 归还对象
void pool_put(lv_obj_t * obj) {
lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
}
注意:对象池的大小要根据实际场景估算。我曾经在一个项目里设了50个池子,结果内存爆了——因为每个按钮都带图片和样式,占了不少空间。后来改成20个,刚好够用。
4.3 使用lv_obj_clean()与lv_obj_del()的正确时机
这两个函数很容易混淆。我刚开始用LVGL时也搞错过,结果要么内存泄漏,要么程序崩溃。
4.3.1 lv_obj_del():彻底删除
lv_obj_del() 会删除对象及其所有子对象。一旦调用,这个对象就彻底没了,不能再使用。
什么时候用?
- 页面切换时,旧页面不再需要
- 动态创建的临时控件(比如弹窗、提示框)
- 对象池里的对象不再需要,想释放内存
// 删除单个对象
lv_obj_del(btn);
// 删除父对象及其所有子对象
lv_obj_del(container);
4.3.2 lv_obj_clean():清空子对象
lv_obj_clean() 只删除指定对象的所有子对象,父对象本身保留。父对象的内存和属性都不变。
什么时候用?
- 清空一个容器,但容器本身还要用
- 刷新列表内容时,先清空再重新添加
- 复用父对象,只换子对象
// 清空容器内的所有子控件
lv_obj_clean(container);
// 容器还在,可以继续添加新控件
lv_obj_t * new_btn = lv_btn_create(container);
| 函数 | 删除自身 | 删除子对象 | 内存释放 | 复用性 |
|---|---|---|---|---|
| lv_obj_del() | 是 | 是(递归) | 完全释放 | 不可复用 |
| lv_obj_clean() | 否 | 是(递归) | 子对象释放 | 父对象可复用 |
避坑指南:我曾经在清空列表时用了 lv_obj_del(container),结果整个容器都没了,后面想再添加控件时直接报错。正确的做法是用 lv_obj_clean(container),只清子对象,保留容器。
4.4 实战:页面切换时的内存优化
假设你有两个页面:主页和设置页。用户来回切换。怎么做才省内存?
方案一:每次切换都创建和销毁
// 切换到设置页
lv_obj_del(main_page);
settings_page = create_settings_page();
// 切回主页
lv_obj_del(settings_page);
main_page = create_main_page();
缺点:每次切换都有创建和销毁的开销,内存波动大。
方案二:创建后隐藏,切换时显示
// 初始化时创建两个页面
main_page = create_main_page();
settings_page = create_settings_page();
lv_obj_add_flag(settings_page, LV_OBJ_FLAG_HIDDEN);
// 切换到设置页
lv_obj_add_flag(main_page, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(settings_page, LV_OBJ_FLAG_HIDDEN);
// 切回主页
lv_obj_add_flag(settings_page, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(main_page, LV_OBJ_FLAG_HIDDEN);
优点:切换几乎零开销,内存稳定。缺点:两个页面同时占用内存。
方案三:混合策略
我常用的做法:常用页面用隐藏/显示,不常用页面用创建/销毁。比如主页常驻,设置页按需创建。
// 主页一直保留
main_page = create_main_page();
// 设置页按需创建
void open_settings(void) {
if (settings_page == NULL) {
settings_page = create_settings_page();
}
lv_obj_add_flag(main_page, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(settings_page, LV_OBJ_FLAG_HIDDEN);
}
void close_settings(void) {
lv_obj_clear_flag(main_page, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(settings_page, LV_OBJ_FLAG_HIDDEN);
// 或者直接销毁,释放内存
// lv_obj_del(settings_page);
// settings_page = NULL;
}
我的建议:如果内存够用,优先用隐藏/显示。如果内存紧张,对不常用页面用创建/销毁,但要做好对象池或缓存,避免频繁分配。
4.5 本章知识体系
下面这张图总结了对象内存优化的核心逻辑:
4.6 小结
对象内存优化,说白了就是三个字:省着用。创建对象时想清楚是不是必须的,销毁时想清楚是不是真的不要了。复用策略能帮你省下大量内存和CPU时间。
嗯,这节课就到这里。记住:lv_obj_clean() 清子不清父,lv_obj_del() 连根拔起。用对了,内存稳如老狗;用错了,调试到天亮。