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 多优先级内存池架构图
下面这张图展示了整个系统的架构:
3.7 避坑指南
最后分享几个我踩过的坑:
- 池大小估算:别拍脑袋定大小。我建议先跑一遍完整 UI,用默认分配器统计峰值内存,然后按 1.5 倍冗余分配。我曾经把高优先级池设得太小,结果动画一复杂就分配失败。
- 跨池分配:不要在低优先级池分配内存,然后传给高优先级任务使用。这会导致高优先级任务依赖低优先级池的释放速度。我见过一个 bug,低优先级池碎片化严重,高优先级任务等释放等到超时。
- 对齐问题:如果你的 MCU 有 cache 或 DMA 控制器,内存对齐很重要。固定块分配器天然对齐,但标准 malloc 不一定。建议在
my_alloc_aligned里做对齐处理。 - 调试输出:上线前关掉所有内存统计日志。我有一回忘了关,结果日志打印本身占用了大量 CPU 时间,反而影响了性能。
总结一下:自定义内存池的核心是「隔离」。把不同优先级的任务隔离开,碎片就不会互相影响。实现上,用 lv_mem_init_custom 挂接自己的分配器,高优先级用固定块,低优先级用标准 malloc。这样既保证了实时性,又兼顾了灵活性。