LVGL源码学习之渲染、更新过程(1)---标记和激活
LVGL版本:8.1
在正式探究源码前,先对几个和更新相关的名词进行说明,有些名词后面可能会混用:
- 渲染(render):指的是将发生修改的对象,根据修改的内容,重新写入显示缓存;
- 绘制(draw):在LVGL中可以等同于渲染概念;
- 融合(blend):在LVGL中特指写入缓存的过程;
- 刷写(flush):将显示缓存写入显示器,这是写入硬件的过程;
- 更新/刷新(refresh):指的是渲染/绘制和更新两个过程的结合,即:将修改的对象显示到显示器屏幕上。
LVGL的层级概念:
显示器(Display)是绘制和显示像素(flush)的硬件对象;
屏幕(screen)是显示器下的高级根对象,一个显示器可以存在多个屏幕(同一时刻仅显示一个屏幕),但一个屏幕只对应一个显示器;
控件(Widgets),是LVGL图形界面的核心元素,是显示和交互的基石,上面所说的屏幕本质上也是一种控件。
缓冲模式(buffer):
这里说的缓冲指的就是显示缓存,通常将单个缓冲大小设置为显示器的像素总数,如果内存紧缺,也可以设置一个较小的缓冲,同样可以实现正常的显示效果,只是效率会相对低一些。在LVGL中,可以设置单缓冲和双缓冲两种缓冲模式。
- 单缓冲:缓冲同时用于刷写和更新,若当前正在刷写,则所有的渲染都需要等待,直至刷写完成;
- 双缓冲:一个用于缓存刷写,另一个用于更新渲染,渲染完成后复制到另一个缓冲进行刷写(或交换地址进行刷写),两个缓冲并行工作;
更新模式:
- 直写模式(direct):根据绝对位置坐标对显示缓存进行绘制,该模式下需要显示缓存大小等于显示器的像素总数,因此每片区域更新完毕后,可以直接将缓冲刷写到显示器上。如果设置了双缓冲,则每次渲染完毕将更新的区域记录下来,留给另一块缓冲渲染使用;
- 全刷新(full_refresh):每次发生更新,总是重新绘制整个屏幕(而非区域重绘),该模式和缓冲数量无关:在单缓冲下刷写和渲染不能同时进行;如果使用双缓冲,可以在刷写时仅交换缓冲地址;
- 普通模式(normal):又称分块模式(partial),上述两种模式标志位都为0时触发,分块刷新区域。和直接模式不同的是,普通模式可以设置较小的缓冲,分块刷写到显示器硬件。
以上相关概念的介绍,将会在接下来的源码分析经常使用到。
更新过程
更新源自于对象的修改,从显示器、屏幕到组件层对象的修改都可以触发更新,因此从标记这些修改开始,开启LVGL渲染更新的过程。
标记和激活
每次修改样式后(包括手动修改、动画修改等),都会触发更新,告诉系统要对被修改的对象节点进行标记,具体包括标记无效区域(invalidate area)和标记脏对象(dirty)两种。
//lv_obj_style.c
void lv_obj_refresh_style(lv_obj_t * obj, lv_style_selector_t selector, lv_style_prop_t prop)
{LV_ASSERT_OBJ(obj, MY_CLASS);if(!style_refr) return;lv_obj_invalidate(obj); //标记无效区域lv_part_t part = lv_obj_style_get_selector_part(selector);if(prop & LV_STYLE_PROP_LAYOUT_REFR) { //需要更新布局if(part == LV_PART_ANY ||part == LV_PART_MAIN ||lv_obj_get_style_height(obj, 0) == LV_SIZE_CONTENT ||lv_obj_get_style_width(obj, 0) == LV_SIZE_CONTENT) {lv_event_send(obj, LV_EVENT_STYLE_CHANGED, NULL);lv_obj_mark_layout_as_dirty(obj); //标记脏数据}}if((part == LV_PART_ANY || part == LV_PART_MAIN) && (prop == LV_STYLE_PROP_ANY ||(prop & LV_STYLE_PROP_PARENT_LAYOUT_REFR))) { //需要更新父节点的布局lv_obj_t * parent = lv_obj_get_parent(obj);if(parent) lv_obj_mark_layout_as_dirty(parent); //标记脏数据}if(prop == LV_STYLE_PROP_ANY || (prop & LV_STYLE_PROP_EXT_DRAW)) {lv_obj_refresh_ext_draw_size(obj); //更新额外绘制区域}lv_obj_invalidate(obj); //标记无效区域if(prop == LV_STYLE_PROP_ANY ||((prop & LV_STYLE_PROP_INHERIT) && ((prop & LV_STYLE_PROP_EXT_DRAW) || (prop & LV_STYLE_PROP_LAYOUT_REFR)))) {if(part != LV_PART_SCROLLBAR) {refresh_children_style(obj);}}
}
标记无效区域(invalidate area):
所有无效区域都被存储到inv_areas[]中,且用inv_p标识当前的无效区域数量。
//lv_hal_disp.h
typedef struct _lv_disp_t {/*......*//** Invalidated (marked to redraw) areas*/lv_area_t inv_areas[LV_INV_BUF_SIZE]; //数组,存储前面标记的所有无效区域坐标uint16_t inv_p; //无效区域数量/*......*/
} lv_disp_t;
具体标记函数为lv_obj_invalidate():
//lv_obj_pos.c
void lv_obj_invalidate(const lv_obj_t * obj)
{LV_ASSERT_OBJ(obj, MY_CLASS);/* 获取对象所在的区域坐标 */lv_area_t obj_coords;lv_coord_t ext_size = _lv_obj_get_ext_draw_size(obj); //获取额外绘制大小lv_area_copy(&obj_coords, &obj->coords);obj_coords.x1 -= ext_size;obj_coords.y1 -= ext_size;obj_coords.x2 += ext_size;obj_coords.y2 += ext_size;lv_obj_invalidate_area(obj, &obj_coords); //标记对象所在的无效区域
}
底层调用_lv_inv_area(),对于全刷新模式(full_refresh),一旦对象发生样式修改,就会将全屏区域纳入无效区域,且仅更新一次。而其它模式下,将在更新任务到来之前,收集所有在此期间发生修改的无效区域,并记录数量。
//lv_refr.c
/*** 函数功能:将显示器某区域标记为无效区域* @param disp 指针,被标记区域属于哪个“显示器(displayer)”,就是“屏幕(screen)”的上层* @param area_p 指针,被标记的区域坐标范围(x1,y1,x2,y2)*/
void _lv_inv_area(lv_disp_t * disp, const lv_area_t * area_p)
{if(!disp) disp = lv_disp_get_default();if(!disp) return;/* 传入区域为NULL时,将无效区域数组长度设为0,表示清除无效区域(invalidate area),这是个用于清除的特殊操作 */if(area_p == NULL) {disp->inv_p = 0;return;}lv_area_t scr_area; //获取显示器的显示范围scr_area.x1 = 0;scr_area.y1 = 0;scr_area.x2 = lv_disp_get_hor_res(disp) - 1;scr_area.y2 = lv_disp_get_ver_res(disp) - 1;lv_area_t com_area;bool suc;suc = _lv_area_intersect(&com_area, area_p, &scr_area); //取显示范围和无效区域的并集if(suc == false) return; /* 超出显示范围,直接返回 *//* 在全刷新(full_refresh)模式下,只要发生样式更新,就设置无效区域为全屏,且仅更新一次 */if(disp->driver->full_refresh) {disp->inv_areas[0] = scr_area; //刷新全屏,只需要设置第一个索引区域为全屏disp->inv_p = 1; //只更新一次(全屏)lv_timer_resume(disp->refr_timer); //恢复更新任务定时器,等待更新执行return;}/* 其它模式下,需要一个个将无效区域加入到数组中,并更新索引 *///有些设备只能匹配特定宽高的像素,需要对区域做舍入处理if(disp->driver->rounder_cb) disp->driver->rounder_cb(disp->driver, &com_area);/* 若当前无效区域是数组中已存在的区域的子集,则作废当前区域并返回 */uint16_t i;for(i = 0; i < disp->inv_p; i++) {if(_lv_area_is_in(&com_area, &disp->inv_areas[i], 0) != false) return;}/* 保存当前无效区域 */if(disp->inv_p < LV_INV_BUF_SIZE) {lv_area_copy(&disp->inv_areas[disp->inv_p], &com_area);}else { /* 如果无效区域数量超出设定,则清空并从0保存 */disp->inv_p = 0;lv_area_copy(&disp->inv_areas[disp->inv_p], &scr_area);}disp->inv_p++; //无效区域数量加1lv_timer_resume(disp->refr_timer); //恢复更新任务定时器,告诉系统可以继续定时更新了
}
标记脏数据(dirty):
当对象被修改时,有两种情况下可能会被标记为脏数据:
- 如果带有更新布局的标志位(通常对象大小、位置、变换等样式会附带),就会被标记为脏数据以便后期更新布局;
- 的
后面在布局更新时会再次提及,其最终目的还是为了标记无效区域。
//lv_obj_pos.c
void lv_obj_mark_layout_as_dirty(lv_obj_t * obj)
{obj->layout_inv = 1; //将对象标记为脏(无效invalidate)lv_obj_t * scr = lv_obj_get_screen(obj);scr->scr_layout_inv = 1; //将对象所处的屏幕标记为脏(无效invalidate)lv_disp_t * disp = lv_obj_get_disp(scr); //获取该对象所在的屏幕lv_timer_resume(disp->refr_timer); //恢复显示器的刷新定时器,告诉系统该显示器可以继续定时更新了
}
无论是无效区域还是脏数据的标记,在每次标记完后,都会启动更新任务的定时器(重复的启动仅以第一次有效),可以称这个过程为“激活”,在定时器到期后,系统会调用更新任务进行屏幕内容的刷新。
定时更新
在注册显示器时,LVGL就为其创建了更新定时器,默认周期为30ms。上一节说到,在标记修改后会激活更新任务的定时器,就是启动该定时器,即:
//lv_hal_disp.c
lv_disp_t * lv_disp_drv_register(lv_disp_drv_t * driver)
{/*......*/disp->refr_timer = lv_timer_create(_lv_disp_refr_timer, LV_DISP_DEF_REFR_PERIOD, disp);/*......*/
}
其中定时更新的函数_lv_disp_refr_timer()是本文的核心,内容有很多,主要包括布局更新、区域合并、区域分块刷新、硬件刷写以及监视器更新(需要在配置文件激活),其中监视器内容本质上是重复上面的更新内容。下面将着重探究前面的几个重点部分。
//lv_refr.c
void _lv_disp_refr_timer(lv_timer_t * tmr)
{REFR_TRACE("begin");uint32_t start = lv_tick_get();volatile uint32_t elaps = 0;disp_refr = tmr->user_data; //这里的user_data就是注册显示器时传入的display#if LV_USE_PERF_MONITOR == 0 && LV_USE_MEM_MONITOR == 0/*** 如果使用了任意监视器,会在屏幕左下角(或右下角)实时显示CPU和内存使用情况,此时就不可以暂停更新定时器。* 否则必须暂停定时器,等待有脏数据产生再启动*/lv_timer_pause(tmr);
#endif/* 更新各个屏幕内的布局(如有需要) */lv_obj_update_layout(disp_refr->act_scr);if(disp_refr->prev_scr) lv_obj_update_layout(disp_refr->prev_scr);lv_obj_update_layout(disp_refr->top_layer);lv_obj_update_layout(disp_refr->sys_layer); //这个顺序更新,表明sys_layer > top_layer > act_scr/* 没有活跃的屏幕,则直接返回 */if(disp_refr->act_scr == NULL) {disp_refr->inv_p = 0;LV_LOG_WARN("there is no active screen");REFR_TRACE("finished");return;}lv_refr_join_area(); //合并无效区域,减少重叠区域带来的计算开销lv_refr_areas(); //刷新无效区域的内容到显示缓存,直写模式将直接刷写到显示器/* 如果存在无效区域更新 */if(disp_refr->inv_p != 0) {if(disp_refr->driver->full_refresh) { //全刷新模式下,将在此刷写到显示器上draw_buf_flush();}/* 更新完毕后,清除所有无效区域 */lv_memset_00(disp_refr->inv_areas, sizeof(disp_refr->inv_areas));lv_memset_00(disp_refr->inv_area_joined, sizeof(disp_refr->inv_area_joined));disp_refr->inv_p = 0;elaps = lv_tick_elaps(start);/* 如果定义了监视器,则执行监视器回调函数 */if(disp_refr->driver->monitor_cb) {disp_refr->driver->monitor_cb(disp_refr->driver, elaps, px_num);}}lv_mem_buf_free_all();_lv_font_clean_up_fmt_txt();/*...省略监视器内容...*/REFR_TRACE("finished");
}
布局更新
对显示器下的几个屏幕都进行一次布局上的更新,更新与否需要依据前面推送的脏数据,一旦某个屏幕上存在脏数据,就会对整个屏幕的内容进行布局更新。
//lv_obj_pos.c
void lv_obj_update_layout(const lv_obj_t * obj)
{static bool mutex = false; //互斥信号量,最原始的实现方式if(mutex) {LV_LOG_TRACE("Already running, returning");return;}mutex = true;lv_obj_t * scr = lv_obj_get_screen(obj); //获取当前对象所处的屏幕/* 更新布局,直到没有脏数据了,注意在更新过程中有可能产生新的脏数据 */while(scr->scr_layout_inv) {LV_LOG_INFO("Layout update begin");scr->scr_layout_inv = 0; //将屏幕更新状态重置layout_update_core(scr);LV_LOG_TRACE("Layout update end");}mutex = false;
}
底层是一个递归调用更新:
做了两件事:
①常规布局更新。重新计算对象的大小和位置坐标(检查样式的变化),计算完毕后将对象坐标区域纳入无效区域;
②特殊布局更新。主要有两种:flex和grid布局,运用了这两种布局的对象会在这里进行额外更新。(如果用户有自己注册的布局,也会在这里更新)。
//lv_obj_pos.c
static void layout_update_core(lv_obj_t * obj)
{uint32_t i;uint32_t child_cnt = lv_obj_get_child_cnt(obj); //获取孩子节点数量for(i = 0; i < child_cnt; i++) { //遍历孩子节点,对这些节点做递归嵌套调用lv_obj_t * child = obj->spec_attr->children[i];layout_update_core(child); //深度优先遍历}if(obj->layout_inv == 0) return; //如果不是脏数据,则直接返回obj->layout_inv = 0; //重置脏数据状态lv_obj_refr_size(obj); //重新计算对象大小lv_obj_refr_pos(obj); //重新计算目标位置坐标if(child_cnt > 0) { //如果该对象有孩子节点,且使用了特殊布局,则运用该布局专属的回调函数进行额外更新uint32_t layout_id = lv_obj_get_style_layout(obj, LV_PART_MAIN);if(layout_id > 0 && layout_id <= layout_cnt) {void * user_data = LV_GC_ROOT(_lv_layout_list)[layout_id - 1].user_data;LV_GC_ROOT(_lv_layout_list)[layout_id - 1].cb(obj, user_data);}}
}
经过布局更新后,所有的脏数据都转变为了无效区域,接下来就是对所有的无效区域进行处理了。这部分内容留到下一篇继续分析。