3、绘制优化(上):脏矩形机制详解、部分刷新与全刷新的取舍、缓冲区配置(单缓冲/双缓冲/三缓冲)、屏幕旋转与裁剪优化
各位好,欢迎来到第三章。这一章我们聊绘制优化,而且是上篇。
绘制,说白了就是 LVGL 把你要的界面画到屏幕上的过程。很多朋友调完 UI 发现帧率上不去,第一反应是「CPU 太弱了」。其实大部分时候,问题出在绘制策略上。你想想看,屏幕就那么点大,每次刷新都重画全屏,那得浪费多少带宽?
我个人习惯,拿到一个新平台,第一件事就是先把绘制相关的配置捋一遍。今天我们就从脏矩形机制开始,把缓冲区配置、刷新策略、旋转裁剪这些硬骨头啃下来。
3.1 脏矩形机制详解
先说说脏矩形。这名字听着挺糙,但道理很实在。
LVGL 内部维护了一个「脏矩形列表」。当某个控件的位置、大小、内容发生变化时,LVGL 不会立刻重绘整个屏幕,而是把这个控件所在的矩形区域标记为「脏」。等到下一次刷新周期,只重绘这些脏区域。
举个例子:你按下一个按钮,按钮颜色变了。如果没脏矩形机制,整个屏幕 480×320 都得重绘。有了脏矩形,可能只需要重绘按钮那 80×40 的区域。面积差了 48 倍。
核心原理:脏矩形合并算法。LVGL 会把相邻或重叠的脏矩形合并成一个更大的矩形,减少绘制次数。但合并太激进也不行——合并后的矩形太大,反而浪费性能。
我在项目中遇到过一个问题:一个动态图表每 100ms 更新一次数据点,每次只改几个像素。结果 LVGL 把整个图表区域都标记为脏了。后来发现是图表控件的 lv_obj_invalidate() 调用太粗暴,没有精确传递脏区域。改成手动调用 lv_obj_invalidate_area() 后,绘制量直接降了 70%。
小技巧:如果你自定义控件,尽量用 lv_inv_area() 而不是 lv_obj_invalidate()。前者可以指定精确的脏矩形范围,后者会把整个对象区域都标记为脏。
3.2 部分刷新与全刷新的取舍
LVGL 提供了两种刷新模式:LV_DISP_REFR_MODE_PARTIAL 和 LV_DISP_REFR_MODE_FULL。
| 刷新模式 | 原理 | 适用场景 | 性能特点 |
|---|---|---|---|
| 部分刷新 | 只刷新脏矩形区域 | 静态界面、少量控件变化 | 绘制面积小,但需要缓冲区支持 |
| 全刷新 | 每次刷新整个屏幕 | 动画密集、全屏切换 | 绘制面积大,但逻辑简单 |
你可能会问:全刷新这么浪费,为什么还要用?
嗯,这里要注意。部分刷新虽然省面积,但它依赖缓冲区。如果你的缓冲区配置不合理,部分刷新反而会因为频繁的缓冲区切换和脏矩形合并计算,导致 CPU 占用更高。我见过一个项目,在 STM32F429 上跑 LVGL,开了部分刷新后帧率反而从 30fps 掉到 18fps。后来换成全刷新,帧率稳定在 28fps。
避坑指南:我曾经在一个低端 MCU 上调试 UI,发现部分刷新模式下屏幕闪烁严重。查了半天,原来是显示驱动 IC 不支持局部更新。部分刷新需要硬件配合,不是所有屏幕都支持。遇到闪烁,先查硬件规格。
我的建议是:
- 静态界面为主(仪表盘、设置页)→ 用部分刷新
- 全屏动画、视频播放 → 用全刷新
- 不确定时 → 两个模式都跑一遍 benchmark,看实际帧率
3.3 缓冲区配置:单缓冲 / 双缓冲 / 三缓冲
缓冲区配置是绘制优化的基石。说白了,缓冲区就是一块内存,用来暂存要显示的画面。LVGL 支持三种模式。
3.3.1 单缓冲
最简单,也最省内存。LVGL 直接往屏幕的显存里画。画完就显示。
- 优点:内存占用最小,只需要一个缓冲区
- 缺点:绘制过程中屏幕会看到「画一半」的画面,出现撕裂
- 适用:内存极度受限、对显示质量要求不高的场景
3.3.2 双缓冲
两个缓冲区:一个用于显示(front buffer),一个用于绘制(back buffer)。绘制完成后,交换指针。
- 优点:消除撕裂,画面流畅
- 缺点:内存翻倍
- 适用:大多数嵌入式 UI 场景,我推荐作为默认配置
3.3.3 三缓冲
三个缓冲区:一个显示,一个绘制,一个等待。主要用于 GPU 加速场景。
- 优点:绘制和显示完全解耦,帧率更稳定
- 缺点:内存占用大,延迟增加一帧
- 适用:带 GPU 的高端 MCU 或 MPU
| 模式 | 内存占用 | 撕裂风险 | 帧率稳定性 | 推荐场景 |
|---|---|---|---|---|
| 单缓冲 | 1× | 高 | 低 | 极低内存设备 |
| 双缓冲 | 2× | 无 | 中 | 大多数 MCU 项目 |
| 三缓冲 | 3× | 无 | 高 | GPU 加速平台 |
个人经验:在 STM32H743 上做项目时,我一开始用了双缓冲,帧率 45fps。后来发现显示驱动支持 DMA2D,就改成了三缓冲 + GPU 渲染,帧率飙到 60fps。但代价是内存从 200KB 涨到 600KB。内存够用的话,三缓冲确实香。
3.4 屏幕旋转与裁剪优化
屏幕旋转,说白了就是把横屏 UI 搬到竖屏上,或者反过来。LVGL 支持通过 lv_disp_set_rotation() 实现 0°、90°、180°、270° 旋转。
但旋转是有代价的。每次旋转,LVGL 内部需要重新计算所有控件的坐标。如果你在运行时频繁切换旋转方向,性能会明显下降。
裁剪优化呢?LVGL 的裁剪机制会判断哪些区域在屏幕可见范围内,只绘制可见部分。旋转之后,裁剪区域的计算会更复杂。
优化策略:
- 尽量在初始化时确定旋转方向,不要在运行时切换
- 如果必须支持多方向,考虑用两套 UI 布局,而不是依赖旋转
- 裁剪区域的计算可以手动优化:用
lv_disp_drv_t的rounder_cb回调对齐脏矩形边界,减少裁剪计算量
我记得有一次做手持设备,用户要求横竖屏自适应。我一开始用 LVGL 的旋转功能,结果每次旋转都要卡顿 200ms。后来改成两套 UI 布局,横屏一套、竖屏一套,切换时直接加载对应布局,卡顿降到 20ms 以内。
注意:旋转 90° 或 270° 时,LVGL 的缓冲区大小不会自动调整。如果你的缓冲区是按横屏分配的(比如 480×320),旋转到竖屏后,缓冲区还是 480×320,但实际显示区域只有 320×480。多出来的部分浪费了。手动调整缓冲区大小可以省下 30% 左右的内存。