LVGL源码分析
文章基于LVGL V7.11.0版本https://github.com/lvgl/lvgl/tree/v7.11.0
UI框架核心流程:

对于一个UI系统来说最原始的三个需求:
- 控件树维护:在前端叫DOM树(创建控件时,都需要传一个父控件)
- UI显示、刷新:通过迭代控件树,调用每个控件自己的绘制函数
- 点击事件传递到控件:TP硬件获取到点击坐标,传给UI系统,在去控件树去找到对应位置的控件,然后调用该控件的点击回调函数
问题:
- LVGL启动流程
- Widgets的数据是如何显示到屏幕
- 屏幕点击事件,是如何传递到对应空间的事件回调中
- 不同控件之间有什么关系?为什么返回的都是lv_obj_t
- 图片解码、缓存流程,显示
提前须知:
LVGL中重要的数据结构:双向链表
// lv_ll.h
typedef struct {
uint32_t n_size;
lv_ll_node_t * head;
lv_ll_node_t * tail;
} lv_ll_t;
最重要的宏:LV_ROOTS
// lv_gc.c
#if(!defined(LV_ENABLE_GC)) || LV_ENABLE_GC == 0
LV_ROOTS
#endif /* LV_ENABLE_GC */
// lv_gc.h
#define LV_ITERATE_ROOTS(f) \
f(lv_ll_t, _lv_task_ll) /*Linked list to store the lv_tasks*/ \
f(lv_ll_t, _lv_disp_ll) /*Linked list of screens*/ \
f(lv_ll_t, _lv_indev_ll) /*Linked list of screens*/ \
f(lv_ll_t, _lv_drv_ll) \
f(lv_ll_t, _lv_file_ll) \
f(lv_ll_t, _lv_anim_ll) \
f(lv_ll_t, _lv_group_ll) \
f(lv_ll_t, _lv_img_defoder_ll) \
f(lv_ll_t, _lv_obj_style_trans_ll) \
f(lv_img_cache_entry_t*, _lv_img_cache_array) \
f(lv_task_t*, _lv_task_act) \
f(lv_mem_buf_arr_t , _lv_mem_buf) \
f(_lv_draw_mask_saved_arr_t , _lv_draw_mask_list) \
f(void * , _lv_theme_material_styles) \
f(void * , _lv_theme_template_styles) \
f(void * , _lv_theme_mono_styles) \
f(void * , _lv_theme_empty_styles) \
f(uint8_t *, _lv_font_decompr_buf) \
#define LV_DEFINE_ROOT(root_type, root_name) root_type root_name;
#define LV_ROOTS LV_ITERATE_ROOTS(LV_DEFINE_ROOT)
#define LV_GC_ROOT(x) x
很懵?那换一种方式:
lv_ll_t _lv_task_ll; /*Linked list to store the lv_tasks*/
lv_ll_t _lv_disp_ll; /*Linked list of display device*/
lv_ll_t _lv_indev_ll; /*Linked list of input device*/
lv_ll_t _lv_drv_ll;
lv_ll_t _lv_file_ll;
lv_ll_t _lv_anim_ll;
lv_ll_t _lv_group_ll;
lv_ll_t _lv_img_decoder_ll;
lv_ll_t _lv_obj_style_trans_ll;
lv_img_cache_entry_t* _lv_img_cache_array;
lv_task_t* _lv_task_act;
lv_mem_buf_arr_t _lv_mem_buf;
_lv_draw_mask_saved_arr_t _lv_draw_mask_list;
void* _lv_theme_material_styles;
void* _lv_theme_template_styles;
void* _lv_theme_mono_styles;
void* _lv_theme_empty_styles;
uint8_t* _lv_font_decompr_buf;
基本上是lvgl的context:task、disp、indev、drv、anim、img_decoder、style、theme、font全定义了; 使用时一般会用LV_GC_ROOT()
宏去获取使用,比如: LV_GC_ROOT(_lv_task_ll)
、LV_GC_ROOT(_lv_disp_ll)
、&LV_GC_ROOT(_lv_indev_ll)
;
显示时Widget之间的关系:父子关系
todo:
Task任务
todo:
LVGL启动流程
基本上程序都会分为两个阶段:一个是正常正常前的初始化阶段,把个各模块做相应的初始化;第二个是正常运行阶段,各个模块相互协作,完成任务,完成事件流阶段
- 初始化
lv_init()
核心初始化hal_init()
硬件初始化,及硬件和LVGL UI关联上lv_demo_widgets()
绘制自定义的UI
- 正常运行、周期性调度Task阶段(死循环)
- LVGL UI变化(更新)显示到屏幕(硬件)
- 触摸事件(TP硬件)响应到LVGL UI系统,找到对应的Widget,调用相应的事件回调
Widgets的数据是如何显示到屏幕
前提:
- 屏幕刷新有对应UI task,周期性的执行:
_lv_disp_refr_task
// UI 刷新task启动:
// main.c main() --> hal_init() --> lv_disp_drv_register(&disp_drv);
// lv_hal_disp.c --> lv_disp_drv_register函数中
disp->refr_task = lv_task_create(_lv_disp_refr_task, LV_DISP_DEF_REFR_PERIOD, LV_REFR_TASK_PRIO, disp);
- UI的刷新不是每次全部刷新,而是只刷新更新数据的区域
流程:
- Widget在设置数据的时候,会告诉
lv_disp_t
“显示屏”哪块区域需要重新绘制(标记) - 当UI task开始执行时,
lv_disp_t
“显示屏”会调用driver驱动,去刷新对应的区域
流程详解:
- 无论是lv_lable_t的
lv_label_set_text(label, text)
,还是lv_img_t的lv_img_set_src(img, src_img);
;都会去调用lv_obj_invalidate(obj);
,标记需要重新绘制 - “区域”会保存到
lv_disp_t
的inv_areas
数组字段中,inv_p
会记录个数,也就是数组长度; - 当UI的task开始执行里,
- 调用
_lv_disp_refr_task
UI刷新task- 调用
lv_refr_vdb_flush
把内容刷新到UI上- 调用driver去刷新屏幕:
disp->driver.flush_cb(&disp->driver, &vdb->area, vdb->buf_act);
- 调用driver去刷新屏幕:
- 调用
- 调用
下面是结构体lv_disp_t
,保留了关键字段:
typedef struct _disp_t {
lv_disp_drv_t driver; /**< Driver to the display*/
// ...
/** Invalidated (marked to redraw) areas*/
lv_area_t inv_areas[LV_INV_BUF_SIZE];
uint8_t inv_area_joined[LV_INV_BUF_SIZE];
uint32_t inv_p : 10;
} lv_disp_t;
这个
driver.flush_cb()
看着是不是有几分眼熟?对,就是main.c中初始化hal层hal_init()
,设置的disp_drv.flush_cb = xxx_flush;
。LVGL也不知道内容显示到哪里去,所以暴露了出来
这里只是简单的概括下流程,实际情况会复杂些,包括显示的数据内容,以及多个相交的区域处理。以及有的控件重绘,会导致其它的区域也需要重绘
屏幕点击事件,是如何传递到对应空间的事件回调中
我点击了屏幕TP,如何传递并调用我设置的回调函数lv_obj_set_event_cb(obj, event_cb);
前提:
- 对应有一个task任务在不停的获取点击屏幕的坐标
流程:
- 读取点击到的坐标x、y,以及状态state(press/release)
- 在press按压的时候搜索对应的Widget,递归查找
- 三层上搜索:sys layer > top layer > scr_act layer
- Widgets之间的关系:子父关系。root根是屏幕,一个屏幕可以对应一个或多个子widgets
- 按不同的情况触发不同的事件:
lv_event_send(indev_obj_act, LV_EVENT_XXX, NULL);
- 内部会调用到创建Widget时设置的回调函数
代码跟踪流程:
- _lv_indev_read_task
- _lv_indev_read
- indev->driver.read_cb(&indev->driver, data); // mouse_read 读取点击的x、y,以及是state(press/release)
- indev_button_proc
- indev_proc_press
- lv_indev_search_obj:lv_disp_get_layer_sys > lv_disp_get_layer_top > lv_disp_get_scr_act
- 递归遍历,找到对应的obj
- lv_event_send(indev_obj_act, LV_EVENT_XXX, NULL);
- lv_event_send_func(obj->event_cb, obj, event, data);
- if(event_xcb) event_xcb(obj, event); // 调用你最开始设置的回调
- lv_event_send_func(obj->event_cb, obj, event, data);
- lv_indev_search_obj:lv_disp_get_layer_sys > lv_disp_get_layer_top > lv_disp_get_scr_act
- indev_proc_press
- _lv_indev_read
不同控件之间有什么关系?为什么返回的都是lv_obj_t
lv_obj_t
通过一个void*
拓展成不同的Widget,lv_obj_t
定义共有/通用的部分,每个特别的Widget定义自己独特的部分,命名为lv_xxx_ext_t
,比如lv_label_ext_t
,lv_img_ext_t
...
lv_obj_t:
typedef struct _lv_obj_t {
// ...
void* ext_attr; // 指向
// ...
} lv_obj_t;
lv_label_ext_t:
typedef struct {
char * text;
union {
char * tmp_ptr;
char tmp[LV_LABEL_DOT_NUM + 1];
} dot;
uint32_t dot_end;
uint16_t anim_speed;
lv_point_t offset;
lv_draw_label_hint_t hint;
uint32_t sel_start;
uint32_t sel_end;
lv_label_long_mode_t long_mode : 3;
uint8_t static_txt : 1;
uint8_t align : 2;
uint8_t recolor : 1;
uint8_t expand : 1;
uint8_t dot_tmp_alloc : 1;
} lv_label_ext_t;
lv_img_ext_t:
typedef struct {
const void* src;
lv_point_t offset;
lv_coord_t w;
lv_coord_t h;
uint16_t angle;
lv_point_t pivot;
uint16_t zoom;
uint8_t src_type : 2;
uint8_t auto_size : 1;
uint8_t cf : 5;
uint8_t antialias : 1;
} lv_img_ext_t;
v8.0版本更改:
lv_label_t
嵌套lv_obj_t
,嵌套结构体更加符合面向对象语言中的继承关系,父类有的子类会继承过来;改成嵌套结构体,可能还有一个原因:早前版本的lv_xxx_create
至少会有两次malloc申请内存,一次malloc lv_obj_t
,一次malloc lv_xxx_ext_t
。
typedef struct {
lv_obj_t obj;
char* text;
union {
char* tmp_ptr;
char tmp[LV_LABEL_DOT_NUM + 1];
} dot;
uint32_t dot_end;
lv_draw_label_hint_t hint;
uint32_t sel_start;
uint32_t sel_end;
lv_point_t offset;
lv_label_long_mode_t long_mode : 3;
uint8_t static_txt : 1;
uint8_t recolor : 1;
uint8_t expand : 1;
uint8_t dot_tmp_alloc : 1;
} lv_label_t;
图片解码、缓存流程,显示
- 设置数据阶段:
- lv_img_set_src
- lv_img_decoder_get_info
- 标记这块区域要刷新
- 刷新绘制阶段:
- _lv_disp_refr_task
- lv_refr_areas
- lv_refr_area
- lv_refr_area_part
- lv_refr_obj
- obj->design_cb(obj, &obj_ext_mask, LV_DESIGN_DRAW_MAIN);// 调用widget自己的绘制函数
- lv_refr_obj_and_children
- lv_refr_obj
- lv_refr_area_part
- lv_refr_area
- lv_refr_areas
- 调用widget自己的design函数:lv_xxx_design
- lv_img_design
- lv_draw_img
- lv_img_draw_core
- _lv_img_cache_open 缓存 list(20)img
- lv_img_decoder_open
- _LV_LL_READ(LV_GC_ROOT(_lv_img_defoder_ll), d) // 尝试使用不同的解码器去解码:png/jpeg
- info / open
- _LV_LL_READ(LV_GC_ROOT(_lv_img_defoder_ll), d) // 尝试使用不同的解码器去解码:png/jpeg
- lv_img_decoder_open
- _lv_img_cache_open 缓存 list(20)img
- lv_img_draw_core
- lv_draw_img
坑
内存lv_mem.c
lvgl分配内存函数void * lv_mem_alloc(size_t size);
,如果你使用的内存超出了你分配的内存,不会有段错误,然后会导致整个内存分配系统不可用
char* p = lv_mem_alloc(8);
char* buffer = "hello lvgl !!!";
memset(p, buffer, strlen(buffer));