绘制优化:脏矩形机制、局部刷新策略、避免全屏重绘
说到 LVGL 的动画性能,很多人第一反应是「优化渲染器」或者「减少图层」。但说实话,我见过太多项目,一跑动画就卡,结果发现是每次刷新都在重绘整个屏幕。这就像你家里只有一面墙脏了,你却把整栋楼都刷了一遍漆——浪费,而且没必要。
这一章,我们就来聊聊怎么让 LVGL 只画「该画的部分」。核心就三个词:脏矩形、局部刷新、避免全屏重绘。
脏矩形机制:LVGL 的「聪明眼」
脏矩形(Dirty Rectangle)是 LVGL 绘制系统的基石。说白了,就是系统只记录那些「内容发生变化」的区域,然后只重绘这些区域。
LVGL 内部维护了一个或多个矩形区域,当某个对象的位置、大小、内容发生变化时,LVGL 会把这个对象所在的区域标记为「脏」。然后在下一帧刷新时,只处理这些脏区域。
核心原理:脏矩形合并与裁剪。LVGL 会把相邻或重叠的脏矩形合并成一个更大的矩形,减少绘制次数。同时,每个对象的绘制都会被裁剪到脏矩形范围内,避免画多余的内容。
我记得有一次调试一个仪表盘界面,指针旋转时整个屏幕都在闪。后来打开脏矩形调试模式一看,好家伙,每次指针动一下,整个屏幕都被标记成脏区域了。这就是典型的脏矩形扩散问题。
局部刷新策略:怎么让脏矩形「不扩散」
脏矩形扩散,是性能杀手。一个 10x10 像素的小按钮动画,如果扩散到整个 480x320 的屏幕,绘制量直接放大 1500 倍。你想想看,这能不卡吗?
要控制脏矩形,得从这几个方面入手:
- 对象层级管理:避免把动画对象放在父容器的边缘。父容器一旦被标记为脏,整个容器区域都会重绘。
- 使用独立图层:对于频繁更新的对象(比如指针、数字滚动),单独放到一个
lv_layer上。这样脏矩形只影响这个图层,不会污染背景。 - 禁用透明背景:如果对象背景不透明,LVGL 可以跳过背景重绘。透明背景会导致脏矩形向下穿透,引发连锁重绘。
我的习惯:在项目初期就打开 LV_USE_PERF_MONITOR 和 LV_USE_DRAW_MONITOR,实时观察脏矩形的数量和大小。如果脏矩形数量超过 5 个,或者单个脏矩形面积超过屏幕的 30%,就该警惕了。
避免全屏重绘:从根源上解决问题
全屏重绘是性能的「无底洞」。尤其是在低端 MCU 上,一次全屏重绘可能消耗 10ms 以上,而你的帧间隔可能只有 16ms(60fps)。
哪些情况容易触发全屏重绘?
| 触发场景 | 原因 | 解决方案 |
|---|---|---|
| 屏幕旋转/翻转 | LVGL 需要重新计算所有对象位置 | 避免运行时旋转,或使用硬件旋转 |
| 主题切换 | 所有对象样式重新应用 | 使用局部样式覆盖,而非全局主题 |
| 父容器尺寸变化 | 子对象位置重新计算,脏矩形扩散 | 固定父容器尺寸,或使用 lv_obj_set_pos 而非 lv_obj_set_size |
| 全屏遮罩/弹窗 | 遮罩层覆盖整个屏幕 | 使用半透明遮罩 + 局部刷新,或硬件合成 |
我曾经在一个智能家居面板项目上,遇到一个诡异的问题:每次滑动页面,都会触发一次全屏重绘。查了两天才发现,是某个子对象的 lv_obj_align 在滑动过程中不断被调用,导致父容器认为「所有子对象都需要重新布局」。后来改成固定坐标,问题立刻消失。
实战配置:让 LVGL 只画该画的
下面是一组我常用的配置,专门针对局部刷新优化:
// lv_conf.h 中的关键配置
// 1. 启用脏矩形调试(开发阶段)
#define LV_USE_PERF_MONITOR 1
#define LV_USE_DRAW_MONITOR 1
// 2. 设置脏矩形最大数量(默认 10,可适当增大)
#define LV_DISP_DEF_REFR_PERIOD 30 // 刷新周期,单位 ms
// 3. 启用局部刷新(必须)
#define LV_DISP_DEF_DRAW_BUF_SIZE (LV_HOR_RES_MAX * 50) // 绘制缓冲区大小
// 4. 禁用不必要的全局重绘
#define LV_USE_OBJ_REFRESH 1 // 对象级刷新
#define LV_USE_GROUP_REFRESH 0 // 禁用组级刷新(除非必要)
注意:LV_DISP_DEF_DRAW_BUF_SIZE 不是越大越好。过大的缓冲区会占用大量 RAM,反而导致内存碎片和性能下降。我一般设置为屏幕一行像素的 1.5~2 倍,比如 480x272 的屏幕,设置 480*2=960 字节就够用。
SVG 流程图:脏矩形处理流程
下面这张图展示了 LVGL 从检测脏矩形到最终绘制的完整流程。你可以看到,每一步都在「裁剪」和「合并」,目的就是减少实际绘制面积。
避坑指南:我踩过的那些坑
讲几个实战中容易忽略的点:
- 动画回调里不要改布局:我曾经在动画的
anim_ready_cb里调用了lv_obj_set_size,结果每次动画结束都触发一次全屏重绘。后来改成在动画开始前就固定好尺寸。 - 小心
lv_scr_load:这个函数会强制全屏重绘。如果你只是切换一个弹窗,用lv_obj_set_hidden或者lv_anim_start来做淡入淡出,脏矩形只影响弹窗区域。 - 双缓冲区的陷阱:如果开启了双缓冲区,脏矩形机制依然有效,但要注意缓冲区切换的时机。我习惯把
LV_DISP_DEF_REFR_PERIOD设置为 30ms,配合双缓冲区,既能保证流畅度,又不会过度刷新。