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 本章知识体系

下面这张图总结了对象内存优化的核心逻辑:

LVGL对象内存优化知识体系 LVGL对象内存优化核心逻辑 对象内存优化 创建与销毁开销 对象复用策略 清理时机选择 内存分配 链表操作 碎片风险 隐藏/显示 对象池模式 混合策略 lv_obj_del() lv_obj_clean() 时机选择 核心原则:少创建、多复用、及时清理 根据场景选择合适策略,平衡内存占用与运行效率

4.6 小结

对象内存优化,说白了就是三个字:省着用。创建对象时想清楚是不是必须的,销毁时想清楚是不是真的不要了。复用策略能帮你省下大量内存和CPU时间。

嗯,这节课就到这里。记住:lv_obj_clean() 清子不清父,lv_obj_del() 连根拔起。用对了,内存稳如老狗;用错了,调试到天亮。