绘制优化:脏矩形机制、局部刷新策略、避免全屏重绘

说到 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 从检测脏矩形到最终绘制的完整流程。你可以看到,每一步都在「裁剪」和「合并」,目的就是减少实际绘制面积。

LVGL 脏矩形处理流程 脏矩形处理流程 对象状态变化 标记脏矩形 脏矩形合并 裁剪与绘制 刷新显示 关键点:脏矩形合并减少绘制次数,裁剪避免绘制无关区域 每次刷新只处理脏矩形区域,非脏区域直接复用上一帧的缓冲区内容 脏矩形扩散原因 • 父容器边缘动画 • 透明背景穿透 • 对象层级嵌套过深

避坑指南:我踩过的那些坑

讲几个实战中容易忽略的点:

  • 动画回调里不要改布局:我曾经在动画的 anim_ready_cb 里调用了 lv_obj_set_size,结果每次动画结束都触发一次全屏重绘。后来改成在动画开始前就固定好尺寸。
  • 小心 lv_scr_load:这个函数会强制全屏重绘。如果你只是切换一个弹窗,用 lv_obj_set_hidden 或者 lv_anim_start 来做淡入淡出,脏矩形只影响弹窗区域。
  • 双缓冲区的陷阱:如果开启了双缓冲区,脏矩形机制依然有效,但要注意缓冲区切换的时机。我习惯把 LV_DISP_DEF_REFR_PERIOD 设置为 30ms,配合双缓冲区,既能保证流畅度,又不会过度刷新。