绘制优化技巧:减少不必要的绘制调用

绘制优化,说白了就是让屏幕少干活。你想想看,每次刷新屏幕,MCU都要吭哧吭哧地算一遍像素点。如果算出来的东西根本没变化,那不就是白费力气吗?

我在做智能手表项目时遇到过这种情况:一个简单的数字时钟界面,每秒刷新一次,结果CPU占用率飙到80%。后来一查,发现整个屏幕都在重绘。嗯,这就是典型的「无效绘制」问题。

今天我们就来聊聊LVGL里三个最实用的绘制优化手段:绘制缓存、裁剪区域和图层合并。

绘制缓存:让不变的部分「躺平」

绘制缓存的核心思想很简单——只重绘变化的部分。LVGL默认会为每个对象分配一个缓存区,但这个缓存怎么用,其实很有讲究。

核心原则:静态内容(背景、边框、静态文字)只绘制一次,动态内容(滑块、进度条、滚动列表)才实时刷新。

我个人习惯在初始化时做两件事:

  1. 开启对象的缓存标志:lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0) 告诉LVGL这个对象背景不透明,可以放心缓存。
  2. 使用 lv_obj_invalidate 代替 lv_obj_refresh:前者只标记需要重绘的区域,后者会强制整个对象重绘。
// 示例:为静态背景开启缓存优化
lv_obj_t * bg = lv_obj_create(lv_scr_act());
lv_obj_set_style_bg_opa(bg, LV_OPA_COVER, 0);  // 背景不透明,可缓存
lv_obj_set_style_radius(bg, 0, 0);              // 无圆角,减少裁剪计算

// 动态内容单独处理
lv_obj_t * slider = lv_slider_create(bg);
lv_obj_set_pos(slider, 10, 10);
// 滑块变化时只重绘滑块区域,背景不动

小技巧:如果你的界面有大量静态文字,可以考虑用 lv_label_set_text_static() 代替 lv_label_set_text()。前者直接引用字符串常量,不复制数据,省内存又省绘制。

裁剪区域:只画「看得见」的部分

裁剪区域是LVGL里最容易被忽视的优化点。它的原理很简单:只绘制屏幕上可见的像素。如果一个对象被遮挡了80%,那被挡住的部分就不需要绘制。

我曾经调试过一个列表界面,滚动时卡得不行。后来发现,列表项有100个,但屏幕只能显示5个。LVGL默认会尝试绘制所有列表项,哪怕它们根本不在屏幕上。

解决办法就是设置裁剪区域:

// 为滚动列表设置裁剪区域
lv_obj_t * list = lv_list_create(lv_scr_act());
lv_obj_set_size(list, 200, 300);
lv_obj_set_scrollbar_mode(list, LV_SCROLLBAR_MODE_AUTO);

// 关键:设置裁剪回调,只绘制可见项
lv_obj_set_style_clip_corner(list, true, 0);  // 开启裁剪

开启裁剪后,LVGL会自动计算哪些列表项在可视区域内,只绘制那5个。剩下的95个?直接跳过,不画。

场景 未裁剪 裁剪后
100项列表 绘制100次 绘制5次
嵌套容器 绘制所有子对象 只绘制可见子对象
弹出窗口 重绘整个屏幕 只重绘窗口区域

注意:裁剪区域不是万能的。如果对象有阴影、模糊等特效,裁剪可能会破坏视觉效果。这种情况下,我建议用图层合并代替裁剪。

图层合并:把多个绘制调用「打包」

图层合并,说白了就是把多个小绘制任务合并成一个大任务。为什么这样能优化?因为每次绘制调用都有固定开销(函数调用、上下文切换、缓存刷新)。合并后,这些开销只产生一次。

LVGL里实现图层合并有两种方式:

  • 硬件图层:利用MCU的硬件图层功能,把不同内容放在不同图层上,硬件自动合成。
  • 软件图层:使用 lv_draw_ctx 的缓存机制,手动合并绘制命令。

我建议优先用硬件图层,性能提升最明显。但硬件图层需要MCU支持,不是所有芯片都有。如果硬件不支持,那就用软件图层:

// 软件图层合并示例
lv_draw_ctx_t * draw_ctx = lv_disp_get_draw_ctx(disp);

// 开启缓存合并
lv_draw_ctx_set_buf(draw_ctx, buf, buf_size);

// 连续绘制多个对象,它们会被合并到同一个绘制调用中
lv_draw_rect(draw_ctx, &rect_dsc, &area1);
lv_draw_label(draw_ctx, &label_dsc, &area2, "Hello");
lv_draw_img(draw_ctx, &img_dsc, &area3, img_src);

// 最后统一提交
lv_draw_ctx_flush(draw_ctx);

经验之谈:图层合并最适合「静态背景+少量动态元素」的场景。比如仪表盘界面,背景是固定的刻度盘,只有指针在动。把刻度盘合并到一个图层,指针单独一个图层,刷新时只更新指针图层,性能提升非常明显。

知识体系总览

下面这张图总结了三种优化手段的关系和适用场景:

绘制优化三大手段 绘制缓存 裁剪区域 图层合并 适用场景 静态背景、固定文字 不透明对象 适用场景 长列表、滚动视图 嵌套容器、弹出窗口 适用场景 仪表盘、游戏界面 硬件图层支持 优化效果对比 绘制缓存:减少绘制次数 30%-50% 裁剪区域:减少绘制面积 40%-70% 图层合并:减少绘制调用 50%-80%

实战避坑指南

这三种优化手段用好了,界面流畅度能提升一个档次。但我也踩过不少坑,分享几个给你:

  • 缓存别开太大:绘制缓存会占用RAM。我见过有人给每个对象都开缓存,结果内存爆了。建议只给静态内容开缓存,动态内容别开。
  • 裁剪别乱用:裁剪区域计算本身也有开销。如果界面只有两三个对象,开裁剪反而更慢。我一般只在列表、滚动视图这种场景用裁剪。
  • 图层合并别贪多:合并太多绘制调用,会导致单次绘制时间过长,反而卡顿。我建议一次合并不超过10个绘制命令。

我的调试习惯:先用LVGL自带的 lv_disp_get_inactive_time() 看刷新间隔,再用 lv_disp_get_draw_buf_usage() 看缓存使用率。这两个指标能帮你快速定位绘制瓶颈。