第三章 绘制优化策略:脏矩形机制详解
做嵌入式 UI 最头疼的是什么?
不是动画逻辑写不出来,而是动画跑起来卡成幻灯片。我早期做智能手表项目时就踩过这个坑——一个简单的翻页动画,CPU 占用直接飙到 80%,帧率掉到 15fps。后来才发现,问题出在绘制策略上。
LVGL 的脏矩形机制,说白了就是「只画变化的部分」。这招用好了,复杂动画也能跑得丝滑。今天我就把这套东西掰开揉碎了讲给你听。
脏矩形机制:LVGL 绘制的核心
先问一个问题:屏幕上 100 个控件,只有 1 个按钮改变了颜色,你需要重绘整个屏幕吗?
当然不。LVGL 的做法是——只标记那个按钮所在的区域为「脏」,然后只重绘这一小块。这就是脏矩形机制。
核心概念:脏矩形(Invalid Area)是指屏幕上需要重新绘制的矩形区域。LVGL 会收集所有脏矩形,合并优化后,一次性刷新到屏幕。
我习惯把脏矩形机制比作「贴膏药」——哪里痛贴哪里,而不是全身都贴一遍。这样做的好处很明显:
- 减少绘制像素数——只画需要变化的部分
- 降低 CPU 负载——绘制计算量大幅减少
- 提升帧率——同样的时间能刷更多帧
局部刷新原理:脏矩形的生命周期
脏矩形从产生到消失,经历四个阶段。我画了张图,你看完就明白了。
你看,整个过程是流水线式的。我重点讲一下第三步——脏矩形合并,这是性能优化的关键。
脏矩形合并:少画一笔是一笔
假设你的动画同时改变了 5 个按钮的位置,LVGL 会生成 5 个脏矩形。如果这些矩形分散在屏幕各处,直接刷新 5 次,每次都要切换绘制上下文,开销很大。
LVGL 的做法是:先合并,再刷新。
| 合并策略 | 触发条件 | 性能影响 |
|---|---|---|
| 不合并 | 脏矩形间距 > 50px | 绘制次数多,但单次面积小 |
| 相邻合并 | 间距 < 10px | 减少绘制次数,面积略增 |
| 完全合并 | 所有脏矩形打包成一个 | 绘制次数最少,但面积可能很大 |
我个人习惯用「相邻合并」策略。为什么?因为完全合并虽然减少了绘制次数,但可能把整个屏幕都包进去——那就等于全屏刷新了,得不偿失。
实战技巧:如果你的动画涉及多个分散的控件,可以手动调用 lv_obj_invalidate() 只标记真正变化的区域,避免 LVGL 自动合并时扩大范围。
减少重绘区域的实战技巧
理论说完了,来点干货。下面这几个技巧,是我在多个项目里验证过的,能显著减少脏矩形面积。
技巧一:分层绘制
把静态背景和动态控件分开。背景只画一次,存到缓冲区。动画只刷新动态层。
// 创建两个容器,一个放背景,一个放动态控件
lv_obj_t *bg_layer = lv_obj_create(lv_scr_act());
lv_obj_set_size(bg_layer, 240, 240);
lv_obj_set_style_bg_color(bg_layer, lv_color_hex(0x333333), 0);
lv_obj_t *anim_layer = lv_obj_create(lv_scr_act());
lv_obj_set_size(anim_layer, 240, 240);
lv_obj_set_style_bg_opa(anim_layer, LV_OPA_TRANSP, 0); // 透明背景
// 动画控件放在 anim_layer 里
lv_obj_t *btn = lv_btn_create(anim_layer);
// 移动按钮时,脏矩形只覆盖 anim_layer 区域
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, btn);
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_x);
lv_anim_set_values(&a, 0, 200);
lv_anim_set_time(&a, 1000);
lv_anim_start(&a);
这样做的好处是:背景层永远不会变脏,脏矩形只出现在动画层。我做过测试,分层后脏矩形面积平均减少 60%。
技巧二:使用局部缓冲区
LVGL 默认的缓冲区大小是屏幕的 1/10。如果你的屏幕是 240x240,16 位色深,缓冲区大小就是 240*240*2/10 = 11520 字节。这个值可以调。
// 增大缓冲区,减少刷新次数
static lv_disp_draw_buf_t draw_buf_dsc;
static lv_color_t buf_1[240 * 240]; // 全屏缓冲区
static lv_color_t buf_2[240 * 240]; // 双缓冲
lv_disp_draw_buf_init(&draw_buf_dsc, buf_1, buf_2, 240 * 240);
缓冲区越大,LVGL 能合并的脏矩形就越多,刷新效率越高。但要注意内存开销——STM32F103 这种芯片,RAM 只有 64KB,全屏缓冲区可能吃不消。
注意:缓冲区不是越大越好。我见过有人把缓冲区设成屏幕的 3 倍,结果内存溢出,系统直接死机。建议缓冲区大小控制在屏幕像素数的 1~2 倍之间。
技巧三:避免全屏透明控件
透明控件不会显示内容,但它会触发脏矩形。如果你在顶层放了一个全屏透明面板,每次动画都会导致整个屏幕重绘。
我曾经接手过一个项目,动画帧率只有 12fps。排查了半天,发现罪魁祸首就是一个全屏透明的 lv_obj,用来拦截触摸事件。解决方案很简单:
// 错误做法:全屏透明控件
lv_obj_t *cover = lv_obj_create(lv_scr_act());
lv_obj_set_size(cover, 240, 240);
lv_obj_set_style_bg_opa(cover, LV_OPA_TRANSP, 0);
// 正确做法:只拦截需要触摸的区域
lv_obj_t *cover = lv_obj_create(lv_scr_act());
lv_obj_set_size(cover, 100, 50); // 只覆盖按钮区域
lv_obj_set_style_bg_opa(cover, LV_OPA_TRANSP, 0);
改完之后,帧率直接跳到 30fps。你看,有时候性能问题就出在这些不起眼的地方。
技巧四:利用 lv_refr_now() 手动控制刷新时机
LVGL 默认在 lv_timer_handler() 里统一刷新脏矩形。但有些场景下,你可以手动触发刷新,把脏矩形的生命周期压缩到最短。
// 在关键动画帧结束后立即刷新
void my_anim_exec_cb(void *var, int32_t v) {
lv_obj_set_x((lv_obj_t *)var, v);
lv_refr_now(NULL); // 立即刷新脏矩形
}
这样做的好处是:脏矩形不会堆积,每次只刷新当前帧的变化区域。但要注意,频繁调用 lv_refr_now() 会增加 CPU 负载,适合在帧率要求高的场景下使用。
实战案例:一个滑动列表的优化
最后分享一个我实际做过的案例。一个智能家居面板,左侧是菜单列表,右侧是内容区域。滑动列表时,整个屏幕都在闪。
优化前:
- 脏矩形面积:全屏 240x240 = 57600 像素
- 帧率:18fps
- CPU 占用:65%
优化后(分层 + 局部缓冲区 + 避免透明控件):
- 脏矩形面积:仅列表区域 80x240 = 19200 像素(减少 67%)
- 帧率:42fps
- CPU 占用:28%
你看,同样的硬件,只是改了一下绘制策略,效果天差地别。
核心总结:脏矩形机制是 LVGL 性能优化的基石。理解它的生命周期,掌握合并策略,再配合分层绘制、局部缓冲区、避免透明控件这些实战技巧,你的动画想掉帧都难。