【奶茶Beta专项】【LVGL9.4源码分析】03-显示框架-局部刷新和脏区计算规则
【奶茶Beta专项】【LVGL9.4源码分析】03-显示框架-局部刷新和脏区计算规则
文档版本: 1.0
更新日期: 2025年11月
适用对象: LVGL9.4 显示驱动/移植工程师
1. 概述
1.1 文档目的
本篇旨在帮助读者理解 LVGL9.4 显示框架中的“局部刷新”和“脏区(dirty area)计算”机制:弄清 _lv_display_t 中与刷新状态、渲染模式、无效区域缓存相关的字段含义,理解从对象无效化到最终调用 flush_cb 之间的关键步骤,以及在双缓冲/直接渲染等场景下,如何通过合理配置减少无效刷新的开销,为后续的性能调优与问题排查提供参考。
1.2 代码版本与范围
- 仓库路径:
https://github.com/lvgl/lvgl.git - 版本:v9.4.0
- commit:
c016f72d4c125098287be5e83c0f1abed4706ee5 - 重点文件:
src/display/lv_display_private.h(struct _lv_display_t中的刷新与脏区相关字段)src/core/lv_refr.c(局部刷新主流程与脏区处理逻辑)
2. 字段含义与版本差异
2.1 _lv_display_t 中的关键字段含义
围绕“局部刷新和脏区计算”的字段,主要集中在 _lv_display_t 的 Buffering 小节之后:
-
刷新状态相关
volatile int flushing- 含义:当前是否有一次正在进行的显示刷新(flush);
- 典型用途:配合
lv_display_flush_ready()控制刷新完成时机,避免在刷新未结束时再次提交 buffer。
volatile int flushing_last- 含义:当前这次 flush 是否是本轮刷新中的“最后一块区域/最后一帧”;
- 在双缓冲/三缓冲中经常用于判断是否需要切换
buf_act或进行额外同步。
volatile uint32_t last_area : 1、last_part : 1- 含义:标记“当前处理的是最后一个 area / 当前 area 的最后一个 part”;
- 作用:配合
flushing_last一起决定在何时进行资源释放、缓冲切换或同步。
-
渲染模式与抗锯齿
lv_display_render_mode_t render_mode- 取值:
LV_DISPLAY_RENDER_MODE_PARTIAL/FULL/DIRECT等; - 影响:
- 是否以“分块/脏区”为单位进行局部刷新;
- 是否要求 draw buffer 至少覆盖整屏;
- 在
DIRECT模式下,还会影响双缓冲同步逻辑(sync_areas)。
- 取值:
uint32_t antialiasing : 1- 含义:该 display 是否开启抗锯齿;
- 会影响绘制管线的复杂度与性能,但不直接改变脏区算法本身。
-
Tile 与 stride 设置
uint32_t tile_cnt : 8- 含义:将显示缓冲划分为多少个 tile(小块);
- 作用:在局部刷新和脏区处理时,可以按 tile 粒度分割刷新区域,有助于减少每次刷新处理的区域大小。
uint32_t stride_is_auto : 1- 含义:当前缓冲区的 stride 是否由 LVGL 自动计算;
- 该标志主要与缓冲创建 API(如
lv_display_set_buffers*)相关,用于后续 reshape 或重建时决定是否重新计算 stride。
-
脏区缓存与无效计数
lv_area_t inv_areas[LV_INV_BUF_SIZE]- 含义:本轮刷新中累计的“无效区域”列表;
- 每个元素是一块需要重绘的矩形区域。
uint8_t inv_area_joined[LV_INV_BUF_SIZE]- 含义:与
inv_areas数组一一对应的标记位,指示某个区域是否已经被合并到其它区域; - 作用:在进行“脏区合并”时,避免重复刷新被包含/合并的子区域。
- 含义:与
uint32_t inv_p- 含义:当前已经记录了多少个有效的无效区域(即
inv_areas中的有效条目数); - 在刷新循环中常用来遍历所有待处理的脏区。
- 含义:当前已经记录了多少个有效的无效区域(即
int32_t inv_en_cnt- 含义:无效化(invalidate)是否被临时禁用的计数器,常见用法是通过成对的“禁用/启用”调用避免在某些批量操作中产生过多、过碎的脏区。
-
双缓冲同步区域
lv_ll_t sync_areas- 含义:在“双缓冲 + DIRECT 渲染模式”下,记录上一次刷新中更新的区域链表;
- 用途:下一帧刷新时,可利用这些区域在两个缓冲之间做同步,避免显示残影或信息不一致。
小结:
flushing*与last_*决定了一次刷新生命周期的“起止与收尾时机”,render_mode/tile_cnt决定“以什么粒度刷新”,而inv_*与sync_areas则负责“记账和复用”这些局部刷新的信息。
2.2 与 LVGL 8.4.0 的差异简述
- 结构组织层面:
- 8.4.0 中,部分刷新相关状态分散在
lv_disp_t、lv_refr.c的静态变量及驱动配置中; - 9.4.0 将更多“刷新状态 + 脏区缓存”字段集中到了
_lv_display_t内部,更清晰地表达出 display 作为刷新单元的角色。
- 8.4.0 中,部分刷新相关状态分散在
- 脏区管理粒度:
- 8.4.0 主要依赖
lv_inv_area()等接口,在内部做简单的脏区合并与裁剪; - 9.4.0 在保留这些接口的同时,引入了
tile_cnt、更丰富的render_mode,为“按 tile 分块、按不同模式优化刷新路径”提供了基础。
- 8.4.0 主要依赖
- 双缓冲同步机制:
- 8.4.0 的双缓冲更多是“简单的 buf1/buf2 轮换”,同步区域通常需要驱动端自行处理;
- 9.4.0 在
DIRECT + double buffer场景下,增加了sync_areas列表,用于记录和复用“需要在两块缓冲间同步的区域”。
从设计意图看,9.4 在保持向下兼容的基础上,强化了“display 是一个完整刷新单元”的概念,为多后端、多缓冲策略的统一实现打下了基础。
3. 局部刷新整体流程
3.1 从对象无效化到刷新触发
在应用层调用 lv_obj_invalidate(obj) 或修改对象属性时,LVGL 会将对应区域标记为“无效”,大致流程可以概括为:
UI 改动(属性/布局/动画等)
│
▼
lv_obj_invalidate(...)
│
▼
将对象区域写入 display 的 inv_areas[]
│
▼
刷新定时器触发(refr_timer)
│
▼
lv_refr_timer() → lv_refr_invalid_areas()
│
▼
扫描 inv_areas[],合并/裁剪脏区,并按 render_mode/tile_cnt 调度绘制与 flush_cb
关键点:
- 所有无效区域最终都会进入某个 display 实例的
inv_areas[]; - 刷新并不是每次
invalidate立刻执行,而是通过刷新定时器节流(避免频繁小更新导致过多刷新); - 脏区的合并、裁剪与遍历,主要发生在
lv_refr.c的一系列refr_*函数中。
3.2 lv_refr_timer() 主流程概览
在 9.4 中,lv_refr_timer() 是局部刷新的主入口之一(省略非关键细节后):
- 1)获取当前 display 与 active draw buffer(
disp_refr->buf_act),若无则直接返回; - 2)发送
LV_EVENT_REFR_START事件,通知上层即将刷新; - 3)根据需要对 active screen、prev screen 以及 top/bottom/sys layer 做一次布局更新;
- 4)若没有 active screen,清空
inv_areas[]与inv_p并结束; - 5)调用:
lv_refr_join_area():尝试合并重叠或相互包含的脏区;refr_sync_areas():在 DIRECT 双缓冲模式下,将需要同步的区域记录到sync_areas;refr_invalid_areas():对每个有效的inv_areas[i]进行实际绘制与 flush;
- 6)刷新完成后,清空
inv_areas[]/inv_area_joined[],重置inv_p,并发送LV_EVENT_REFR_READY。
用一个简化的 ASCII 流程图表示:
lv_refr_timer()
├─ 检查 disp_refr / buf_act 是否可用
├─ 发送 LV_EVENT_REFR_START
├─ 更新各层布局
├─ 无 active screen? → 清空 inv_* 并结束
├─ lv_refr_join_area()
├─ refr_sync_areas()
├─ refr_invalid_areas()
├─ 清空 inv_areas[] / inv_area_joined[] / inv_p
└─ 发送 LV_EVENT_REFR_READY
4. 脏区计算与 tile 分块规则
4.1 脏区合并与裁剪(inv_areas / inv_area_joined / inv_p)
在一帧内,应用层可能多次调用 lv_obj_invalidate(),如果所有小区域都原样刷新,会造成大量重复绘制。为此,LVGL 对脏区进行合并与裁剪:
- 每次无效化:
- 新区域尝试与
inv_areas[]中已有区域进行合并(如 A 完全覆盖 B,或两者相交后合并为更大矩形); - 若某个已有区域被合并到新区域中,则在
inv_area_joined[]中标记该区域已被“覆盖”,避免重复刷新; - 若不能合并,且
inv_p < LV_INV_BUF_SIZE,则将新区域追加到inv_areas[inv_p],并递增inv_p。
- 新区域尝试与
简化理解可以用下面的示意图:
inv_areas[]:
[0] ── area A (有效)
[1] ── area B (已被 A 合并,inv_area_joined[1] = 1)
[2] ── area C (有效)
...
遍历时只处理 inv_area_joined[i] == 0 的条目,
这样可以避免对被完全包含/合并的旧区域再次刷新。
最终,refr_invalid_areas() 只会对那些“未被标记为 joined”的脏区执行绘制与 flush。
4.2 tile_cnt 对刷新粒度的影响与调优思路
tile_cnt 用于将显示缓冲划分为若干个 tile(例如上下拆成若干水平条),配合 partial 刷新模式,可以进一步减少单次刷新的区域大小。
4.2.1 基本直觉
- 当
tile_cnt > 1时,可以理解为:
整屏区域:
+------------------------+
| tile 0 |
+------------------------+
| tile 1 |
+------------------------+
| tile 2 |
+------------------------+
| ... |
+------------------------+
- 刷新时,LVGL 会尝试按 tile 维度拆分脏区,使每次绘制/刷新的区域更小;
- 好处:在带宽或渲染能力有限的场景中,可以降低单次操作的峰值压力;
- 代价:管理逻辑更复杂,且如果
tile_cnt过大,调度/遍历的开销也会增加,需要根据实际屏幕分辨率和缓冲大小酌情设置。
4.2.2 推荐的调参步骤
下面给出一套在实际项目中常用的调参思路,供参考:
-
从 1 开始,确认基线性能
- 将
tile_cnt设为 1,保证逻辑最简单; - 在典型 UI 场景(列表滚动、页面切换、动画等)下观察:
- FPS / 帧时间;
- 刷新过程中 CPU 占用和总线带宽(如能监控的话)。
- 将
-
按“屏高的 1/2、1/3、1/4”尝试
- 以垂直分辨率为例(如 480 行或 800 行),尝试:
tile_cnt = 2(每 tile 约半屏高);tile_cnt = 3 ~ 4(每 tile 约 1/3 ~ 1/4 屏高);
- 每次只改一个参数,再重复第 1 步的测试,记录帧时间和主观流畅度。
- 以垂直分辨率为例(如 480 行或 800 行),尝试:
-
观察“典型脏区”的大小
- 对滚动列表/局部更新场景,统计一帧内典型
inv_areas[]的高度范围; - 经验上:
- 若典型脏区高度远小于单个 tile 高度,
tile_cnt提升的收益有限; - 若典型脏区高度接近或略大于单 tile 高度,适当增加
tile_cnt通常能降低单次刷新的面积。
- 若典型脏区高度远小于单个 tile 高度,
- 对滚动列表/局部更新场景,统计一帧内典型
-
平衡“刷新次数 vs 每次面积”
tile_cnt增大:- 每次刷新的区域变小 → 每次带宽占用下降;
- 但 tile 数增加 → 每帧可能需要多次绘制/flush;
- 实践中建议通过简单表格记录:
- 每帧平均刷新调用次数;
- 单次刷新平均/最大区域高度;
- 对应的 FPS / CPU 占用;
- 选取在三者间平衡较好的组合,而不是一味追求“越多 tile 越好”。
-
结合硬件特性做微调
- 若底层 LCD 控制器/总线在一条较宽的连续区域刷新的效率明显更高,可以适当降低
tile_cnt,让每次刷新区域更“长条”; - 若 DMA / GPU 启动一次开销较大,则不宜把
tile_cnt调得太高,以免频繁启动绘制引擎。
- 若底层 LCD 控制器/总线在一条较宽的连续区域刷新的效率明显更高,可以适当降低
总体来说:tile_cnt 是一个与分辨率、缓冲大小、硬件特性强相关的参数,需要在“测一测、记录数据”的基础上,选择一个对项目最合适的折中值。
4.3 DIRECT 双缓冲下的 sync_areas
在 render_mode == LV_DISPLAY_RENDER_MODE_DIRECT 且双缓冲启用的情况下,sync_areas 用于记录上一次刷新中更新的区域:
- 在
lv_refr_timer()中,当满足条件时:- 对每个未被
inv_area_joined标记的inv_areas[i],插入到sync_areas链表中;
- 对每个未被
- 下一帧开始前,可以利用这些区域在两块 buffer 之间进行同步,只更新真正发生变化的部分;
- 这样可以避免“整屏拷贝”带来的巨大带宽开销,尤其是在高分辨率或显存总线较窄的场景下。
5. 关键代码片段简析
下面节选 lv_refr.c 中的一段核心流程,帮助理解 inv_areas / inv_area_joined / inv_p 与 sync_areas 的协同关系,以及它们对整个绘制/刷新流程的影响(略去错误处理与非核心分支):
lv_refr_timer(lv_timer_t * tmr)
{
...
lv_draw_buf_t * buf_act = disp_refr->buf_act;
if(!(buf_act && buf_act->data && buf_act->data_size)) { ... }
...
if(disp_refr->act_scr == NULL) {
disp_refr->inv_p = 0;
goto refr_finish;
}
lv_refr_join_area(); /* 合并/裁剪 inv_areas[] 中的脏区 */
refr_sync_areas(); /* 在 DIRECT 双缓冲模式下记录 sync_areas */
refr_invalid_areas(); /* 对每个有效脏区执行绘制和 flush */
if(disp_refr->inv_p == 0) goto refr_finish;
if(lv_display_is_double_buffered(disp_refr) &&
disp_refr->render_mode == LV_DISPLAY_RENDER_MODE_DIRECT) {
uint32_t i;
for(i = 0; i < disp_refr->inv_p; i++) {
if(disp_refr->inv_area_joined[i]) continue;
lv_area_t * sync_area = lv_ll_ins_tail(&disp_refr->sync_areas);
*sync_area = disp_refr->inv_areas[i];
}
}
lv_memzero(disp_refr->inv_areas, sizeof(disp_refr->inv_areas));
lv_memzero(disp_refr->inv_area_joined, sizeof(disp_refr->inv_area_joined));
disp_refr->inv_p = 0;
refr_finish:
...
}
这段代码串联起了几个关键点:
- 先合并/裁剪脏区,再进行实际刷新;
- 在 DIRECT 双缓冲下,将未合并的脏区记录到
sync_areas,用于后续同步;- 刷新完成后清理
inv_areas/inv_area_joined,为下一帧做好准备。
5.1 实战中需要特别关注的几点
-
刷新生命周期与
flushing/flushing_last:flushing未及时通过lv_display_flush_ready()置回 0,会导致后续帧无法顺利提交,表现为“画面卡在某一帧”;flushing_last/last_area/last_part决定了何时做缓冲切换或同步,驱动在判断“最后一块区域”时要与这些状态保持一致。
-
脏区数量与合并策略:
- 如果
inv_areas[]中条目很多、且inv_area_joined[]合并不充分,即使每块区域不大,也会放大 CPU 绘制与 flush 次数; - 在 UI 设计和控件实现上,尽量避免频繁、大面积的无效化(例如每帧重置整个容器),让脏区尽可能集中。
- 如果
-
tile_cnt的取值与刷新粒度:tile_cnt过小,局部刷新的粒度太粗,会导致“微小改动也刷新很大一块”;tile_cnt过大,则每帧需要遍历和调度的 tile 数量爆炸,可能得不偿失;- 建议结合分辨率与缓冲大小,选取一个既能减小单次刷新的物理面积、又不过度增加调度开销的折中值。
-
DIRECT 双缓冲下
sync_areas的正确性:- 若驱动在 DIRECT 模式下自行操作 buffer,而忽略
sync_areas的语义,可能导致前一帧残留内容被误复制到下一帧,出现“局部花屏”或“影子”; - 推荐在调试阶段打印/可视化
sync_areas,确认在双缓冲同步时只更新真正变动的区域。
- 若驱动在 DIRECT 模式下自行操作 buffer,而忽略
-
inv_en_cnt对批量操作的影响:- 在做批量 UI 更新(如大规模创建/销毁对象)时,适当使用“禁止/启用 invalidate”的成对封装,可以避免产生大量零散脏区;
- 但如果忘记恢复(计数不归零),会让后续正常的
invalidate失效,表现为“UI 改了但不刷新”。
-
调试建议:
- 可以在调试构建中加上对
inv_areas[]/inv_p/sync_areas的日志打印或可视化 overlay,观察每一帧真正刷新的区域; - 结合 FPS/CPU 占用等指标,评估当前脏区策略是否合适,必要时调整控件实现或
render_mode/tile_cnt配置。
- 可以在调试构建中加上对
6. 附录
A. 参考文档(外部)
B. 相关资源(外部)
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)