3、绘制优化(上):脏矩形机制详解、部分刷新与全刷新的取舍、缓冲区配置(单缓冲/双缓冲/三缓冲)、屏幕旋转与裁剪优化

各位好,欢迎来到第三章。这一章我们聊绘制优化,而且是上篇。

绘制,说白了就是 LVGL 把你要的界面画到屏幕上的过程。很多朋友调完 UI 发现帧率上不去,第一反应是「CPU 太弱了」。其实大部分时候,问题出在绘制策略上。你想想看,屏幕就那么点大,每次刷新都重画全屏,那得浪费多少带宽?

我个人习惯,拿到一个新平台,第一件事就是先把绘制相关的配置捋一遍。今天我们就从脏矩形机制开始,把缓冲区配置、刷新策略、旋转裁剪这些硬骨头啃下来。

绘制优化(上)知识体系 绘制优化核心 脏矩形机制 只重绘变化区域 部分刷新 vs 全刷新 场景决定取舍 缓冲区配置 单/双/三缓冲对比 旋转与裁剪优化 减少无效绘制区域 目标:最小化绘制面积

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% 左右的内存。