绘制优化技巧:减少不必要的绘制调用
绘制优化,说白了就是让屏幕少干活。你想想看,每次刷新屏幕,MCU都要吭哧吭哧地算一遍像素点。如果算出来的东西根本没变化,那不就是白费力气吗?
我在做智能手表项目时遇到过这种情况:一个简单的数字时钟界面,每秒刷新一次,结果CPU占用率飙到80%。后来一查,发现整个屏幕都在重绘。嗯,这就是典型的「无效绘制」问题。
今天我们就来聊聊LVGL里三个最实用的绘制优化手段:绘制缓存、裁剪区域和图层合并。
绘制缓存:让不变的部分「躺平」
绘制缓存的核心思想很简单——只重绘变化的部分。LVGL默认会为每个对象分配一个缓存区,但这个缓存怎么用,其实很有讲究。
核心原则:静态内容(背景、边框、静态文字)只绘制一次,动态内容(滑块、进度条、滚动列表)才实时刷新。
我个人习惯在初始化时做两件事:
- 开启对象的缓存标志:
lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0)告诉LVGL这个对象背景不透明,可以放心缓存。 - 使用
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);
经验之谈:图层合并最适合「静态背景+少量动态元素」的场景。比如仪表盘界面,背景是固定的刻度盘,只有指针在动。把刻度盘合并到一个图层,指针单独一个图层,刷新时只更新指针图层,性能提升非常明显。
知识体系总览
下面这张图总结了三种优化手段的关系和适用场景:
实战避坑指南
这三种优化手段用好了,界面流畅度能提升一个档次。但我也踩过不少坑,分享几个给你:
- 缓存别开太大:绘制缓存会占用RAM。我见过有人给每个对象都开缓存,结果内存爆了。建议只给静态内容开缓存,动态内容别开。
- 裁剪别乱用:裁剪区域计算本身也有开销。如果界面只有两三个对象,开裁剪反而更慢。我一般只在列表、滚动视图这种场景用裁剪。
- 图层合并别贪多:合并太多绘制调用,会导致单次绘制时间过长,反而卡顿。我建议一次合并不超过10个绘制命令。
我的调试习惯:先用LVGL自带的 lv_disp_get_inactive_time() 看刷新间隔,再用 lv_disp_get_draw_buf_usage() 看缓存使用率。这两个指标能帮你快速定位绘制瓶颈。