3. LVGL内存分配器架构

上一章我们聊了 LVGL 默认的内存分配器,说白了就是标准 C 的 malloc/free 套了个壳。但实际项目里,这种「一刀切」的方式往往不够用。

我印象很深的一个项目——一个带触屏的智能家居面板,UI 上同时跑着动画、日志刷新和弹窗。结果呢?弹窗偶尔卡顿,动画掉帧。查了半天,问题出在内存碎片上:高频分配的小块把大块内存切碎了,动画需要的大块连续内存死活分配不出来。

那怎么办?一个很自然的思路就是:给不同优先级的任务分配独立的内存区域。这就是自定义内存池要做的事。

3.1 为什么需要多个内存池?

你想想看,LVGL 里不同组件的生命周期和分配频率完全不一样:

  • 动画帧缓冲:高频分配、固定大小、要求低延迟
  • 文本/标签:低频分配、大小不定、可以容忍稍慢
  • 弹窗/对话框:偶尔分配、生命周期短、用完就释放

把它们混在一个堆里,就像把高速列车和三轮车放在同一条路上——互相干扰。高频小分配产生的碎片,会慢慢「吃掉」大块连续空间,最终导致高优先级任务分配失败。

核心思想:按任务优先级和分配特征,划分独立内存区域。高优先级任务用专用池,低优先级任务用共享池。这样碎片隔离,互不影响。

3.2 LVGL 的内存管理接口

LVGL 默认使用 lv_mem_alloc、lv_mem_free 等接口。这些接口背后默认走的是标准 C 库的堆。但 LVGL 留了一个后门——lv_mem_init_custom。

这个函数允许你传入自定义的分配器结构体,完全接管内存管理。说白了,就是你可以告诉 LVGL:「别用默认的 malloc 了,用我自己的。」

接口长这样:

typedef struct {
    void * (*alloc)(size_t size);
    void * (*alloc_aligned)(size_t size, size_t align);
    void   (*free)(void * p);
    void * (*realloc)(void * p, size_t new_size);
    void * (*realloc_aligned)(void * p, size_t new_size, size_t align);
    void * (*get_free_mem)(void);
    void * (*get_used_mem)(void);
    void * (*get_max_used_mem)(void);
    void * (*get_fragmentation)(void);
} lv_mem_custom_t;

你只需要实现这些回调,然后调用 lv_mem_init_custom(&my_custom_allocator) 即可。

小提示:alloc_aligned 和 realloc_aligned 在 LVGL 内部使用较少,但如果你用 DMA 传输帧缓冲,对齐分配就很重要。我建议至少实现 alloc、free、realloc 三个核心回调。

3.3 设计多优先级内存池

接下来我们设计一个简单的多优先级内存池系统。假设我们有三个优先级:

优先级 用途 池大小 分配策略
高 动画帧缓冲、实时反馈 32 KB 固定块分配,无碎片
中 控件、标签、按钮 64 KB 标准 malloc,带碎片整理
低 日志、缓存、非关键数据 128 KB 标准 malloc,允许慢分配

每个池独立管理自己的内存块。高优先级池用固定块分配器——没有碎片,分配和释放都是 O(1)。中低优先级池用标准 malloc,但中优先级池定期做碎片整理。

代码实现大致如下:

// 定义三个内存池
static uint8_t mem_pool_high[32 * 1024];
static uint8_t mem_pool_mid[64 * 1024];
static uint8_t mem_pool_low[128 * 1024];

// 每个池的分配器状态
static mem_pool_t pool_high;
static mem_pool_t pool_mid;
static mem_pool_t pool_low;

// 初始化所有池
void mem_pools_init(void) {
    mem_pool_fixed_init(&pool_high, mem_pool_high, sizeof(mem_pool_high), 64);
    mem_pool_std_init(&pool_mid, mem_pool_mid, sizeof(mem_pool_mid));
    mem_pool_std_init(&pool_low, mem_pool_low, sizeof(mem_pool_low));
}

// 根据优先级选择池
void * my_alloc(size_t size) {
    if (size <= 64 && is_high_priority_task()) {
        return mem_pool_fixed_alloc(&pool_high);
    } else if (is_mid_priority_task()) {
        return mem_pool_std_alloc(&pool_mid, size);
    } else {
        return mem_pool_std_alloc(&pool_low, size);
    }
}

注意:优先级判断不能太复杂。我见过有人用字符串比较来判断优先级,结果分配本身比分配的内容还慢。建议用任务 ID 或分配标志位,O(1) 判断。

3.4 固定块分配器的实现

高优先级池我用的是固定块分配器。原理很简单:把内存池切成固定大小的块,用位图或链表管理空闲块。

举个例子,如果块大小是 64 字节,池大小 32 KB,那就是 512 个块。分配时直接取一个空闲块,释放时标记为空闲。没有碎片,没有合并,速度极快。

typedef struct {
    uint8_t * pool_base;
    uint32_t block_size;
    uint32_t block_count;
    uint32_t free_mask[16]; // 512 位,用位图管理
} fixed_pool_t;

void * fixed_pool_alloc(fixed_pool_t * pool) {
    for (int i = 0; i < 16; i++) {
        if (pool->free_mask[i] != 0) {
            int bit = __builtin_ctz(pool->free_mask[i]);
            pool->free_mask[i] &= ~(1 << bit);
            uint32_t index = i * 32 + bit;
            return pool->pool_base + index * pool->block_size;
        }
    }
    return NULL; // 无空闲块
}

这里用了 __builtin_ctz(Count Trailing Zeros)来快速找到第一个空闲位。如果你用 ARM Cortex-M,这个指令是单周期的。嗯,细节决定性能。

3.5 把自定义分配器挂到 LVGL

实现好分配器后,挂接很简单:

static lv_mem_custom_t my_custom_allocator = {
    .alloc           = my_alloc,
    .free            = my_free,
    .realloc         = my_realloc,
    .alloc_aligned   = my_alloc_aligned,
    .realloc_aligned = my_realloc_aligned,
    .get_free_mem    = my_get_free_mem,
    .get_used_mem    = my_get_used_mem,
    .get_max_used_mem = my_get_max_used_mem,
    .get_fragmentation = my_get_fragmentation,
};

void lvgl_mem_init(void) {
    mem_pools_init();
    lv_mem_init_custom(&my_custom_allocator);
}

调用 lv_mem_init_custom 后,LVGL 内部所有的 lv_mem_alloc 都会走你的分配器。你可以在回调里加日志,监控每个池的使用情况。

调试技巧:在 my_alloc 和 my_free 里加统计计数器,记录每个池的分配次数、释放次数、当前使用量。这样你能直观看到内存压力分布。我曾经靠这个发现一个定时器泄漏——高优先级池的分配次数一直在涨,但释放次数没跟上。

3.6 多优先级内存池架构图

下面这张图展示了整个系统的架构:

多优先级内存池架构 LVGL 应用层 自定义分配器 (lv_mem_custom_t 回调) 高优先级池 32 KB / 固定块 动画帧缓冲 中优先级池 64 KB / 标准 malloc 控件 / 标签 低优先级池 128 KB / 标准 malloc 日志 / 缓存 关键设计点 高优先级池用固定块分配,零碎片 中低优先级池用标准 malloc,碎片隔离

3.7 避坑指南

最后分享几个我踩过的坑:

  • 池大小估算:别拍脑袋定大小。我建议先跑一遍完整 UI,用默认分配器统计峰值内存,然后按 1.5 倍冗余分配。我曾经把高优先级池设得太小,结果动画一复杂就分配失败。
  • 跨池分配:不要在低优先级池分配内存,然后传给高优先级任务使用。这会导致高优先级任务依赖低优先级池的释放速度。我见过一个 bug,低优先级池碎片化严重,高优先级任务等释放等到超时。
  • 对齐问题:如果你的 MCU 有 cache 或 DMA 控制器,内存对齐很重要。固定块分配器天然对齐,但标准 malloc 不一定。建议在 my_alloc_aligned 里做对齐处理。
  • 调试输出:上线前关掉所有内存统计日志。我有一回忘了关,结果日志打印本身占用了大量 CPU 时间,反而影响了性能。

总结一下:自定义内存池的核心是「隔离」。把不同优先级的任务隔离开,碎片就不会互相影响。实现上,用 lv_mem_init_custom 挂接自己的分配器,高优先级用固定块,低优先级用标准 malloc。这样既保证了实时性,又兼顾了灵活性。