当前位置: 首页 > news >正文

LVGL源码(7):渲染

      在LVGL源码(4):LVGL关于EVENT事件的响应逻辑_lvgl实现显示打车-CSDN博客这篇文章中,我们提到了LVGL的三大步骤:检测用户输入操作、调用我们编写的逻辑、在屏幕上显示对应的画面;而在学习完“样式”之后,我们或许可以将上述步骤说明的更详细一些,即检测用户输入操作->调用控件的系统逻辑和我们编写的逻辑->如果控件样式改变则重新渲染->在屏幕上显示对应的画面;

      其实我们可以认为,UI的变化就是各个不同控件样式的变化,而样式变化就会涉及到如何渲染,在本篇文章中我们就来粗略的讲一下LVGL的渲染逻辑,如有错误,忘指正。

渲染图层:

      LVGL 使用分层(Layering)渲染,主要分为以下几种不同类型的图层(Layer Types)

1、普通渲染:

     大部分 静态 UI 组件(如 label, button, image)在主帧缓冲区中直接绘制,不需要额外的中间层。主帧缓冲区就是我们在lv_por_disp.c中定义的 static lv_disp_draw_buf_t draw_buf_dsc_x;

2、中间层(Intermediate Layers):

     当某些特效(opacity, blend_mode, transform_angle, transform_zoom) 启用时,LVGL 不会直接绘制对象,而是创建一个中间渲染层(Intermediate Layer),这通常是一个 离屏缓冲区(offscreen buffer),先在这里绘制,之后再合成到主屏幕,也被称为 "临时快照"(Snapshot) 或 "混合层"(Blending Layer),触发中间层的情况如下:

 (1)style_opa < 255(具有不透明度的控件)或者控件使用了blend_mode(使用混合模式):其中间层缓冲区分配方式在lv_conf.h中的

#define LV_LAYER_SIMPLE_BUF_SIZE          (24 * 1024)(小块缓冲区,减少内存占用)

#define LV_LAYER_SIMPLE_FALLBACK_BUF_SIZE (3 * 1024)(备用缓冲区,防止分配失败)

这种方式也称之为软件ARGB 渲染,A、R、G、B(透明+红绿蓝)通常 为32bit,支持透明度(alpha),R、G、B(红绿蓝)通常 24bit 或 16bit,不支持透明度,这是由显示驱动决定的,属于硬件层面。当使用RGB想要有透明度效果时就会使用上述中间渲染层。

(2)控件使用了transform_angle(旋转)、transform_zoom(缩放),由lv_mem_alloc 动态分配;

(3)当显示驱动启用了软件旋转功能(比如 90°/180°/270° 屏幕旋转),LVGL 会在绘图时,把待绘制内容旋转后再传给屏幕(注意这里是整个屏幕旋转而不是单个控件旋转),而这个旋转过程需要一个临时缓冲区 —— 由#define LV_DISP_ROT_MAX_BUF (10*1024)这个宏控制这个临时缓冲区的 最大大小(单位:字节);

上述所有的中间层缓冲区通过 lv_mem_alloc 动态申请,使用的内存来自于 lv_conf.h中定义的LV_MEM_SIZE

3、预合成层(Pre-Rendered Layers)

   如果一个组件 不会频繁变动,LVGL 可能会缓存它的渲染结果,然后直接拷贝到屏幕,而不是每次重绘,这种预合成层可以显著 提升性能,减少不必要的重绘,在lv_conf.h中可以定义关于预合成层的一些参数:

静态图像(image):#define LV_IMG_CACHE_DEF_SIZE 0

渐变缓存:#define LV_GRAD_CACHE_DEF_SIZE 0

复杂阴影(shadow):#define LV_SHADOW_CACHE_SIZE 0

圆形抗锯齿:#define LV_CIRCLE_CACHE_SIZE 4

渲染屏幕结构体介绍:

  lv_disp_t管理的是一个显示屏的“运行时上下文”,它是渲染逻辑、屏幕状态管理、无效区域控制、双缓冲处理等的核心。


/*** Display structure.* @note `lv_disp_drv_t` should be the first member of the structure.*/
typedef struct _lv_disp_t {/**显示器使用的驱动配置结构体(lv_disp_drv_t),包含绘图函数、刷新策略、分辨率等*/struct _lv_disp_drv_t * driver;/**刷新定时器,周期性检查无效区域invalidate(也叫dirty区域)并调用驱动进行渲染*/lv_timer_t * refr_timer;/**当前显示器使用的主题指针(控制颜色、字体、样式等)*/struct _lv_theme_t * theme;/**主题与屏幕管理*/struct _lv_obj_t ** screens;    /**显示器上所有屏幕对象的数组(包括历史、当前、待加载的屏幕)*/struct _lv_obj_t * act_scr;     /**当前显示器的活跃屏幕(active screen)*/struct _lv_obj_t * prev_scr;    /**上一个屏幕,在执行屏幕切换动画时使用*/struct _lv_obj_t * scr_to_load; /**准备加载的新屏幕(通过 lv_scr_load_anim 设定)*//**图层系统*/struct _lv_obj_t * top_layer;   /**顶层图层(如浮动窗口、对话框等)*/struct _lv_obj_t * sys_layer;   /**系统图层(如系统提示、输入法、光标等)*/uint32_t screen_cnt;            /**屏幕的数量(screens 数组的长度*/uint8_t draw_prev_over_act : 1; /**在渲染时是否把上一屏幕绘制在当前屏幕上(用于过渡动画),1为是*/uint8_t del_prev : 1;           /**在完成切换动画时是否自动删除上一屏幕,1为是*/uint8_t rendering_in_progress : 1; /**当前是否正在进行渲染(用于避免在渲染过程中修改无效区域等),1为是*//**背景设置*/lv_opa_t bg_opa;                /**背景透明度(当屏幕透明时,显示此背景色)*/lv_color_t bg_color;            /**显示器默认背景颜色*/const void * bg_img;            /**背景图像(可以设置为壁纸)*//** Dirty 区域管理(无效区域)*/lv_area_t inv_areas[LV_INV_BUF_SIZE];    /**当前显示器所有无效区域的数组*/uint8_t inv_area_joined[LV_INV_BUF_SIZE]; /**标记某个区域是否被合并(加快 dirty 区域更新效率),为1表示对应下标的inv_areas[i]已被合并*/    uint16_t inv_p;        /**当前有效的Dirty区域数量*/int32_t inv_en_cnt;    /**用于嵌套的“启用/禁用刷新”的计数器*//** 双缓冲同步区域 */lv_ll_t sync_areas;   /**双缓冲时用于帧同步的数据结构(链表形式,记录需要同步的区域)*//*活动时间记录*/uint32_t last_activity_time;        /**最近一次显示器被用户操作(如触摸)的时间戳,用于屏保或节能等功能*/
} lv_disp_t;

    lv_disp_drv_t 这个结构体是 LVGL 显示驱动的核心配置结构体,用于硬件抽象层(HAL)注册显示设备。你可以把它理解为连接 LVGL 和底层显示硬件之间的“桥梁”。LVGL 的渲染器会通过这个结构体完成绘图、刷新、缓存、色彩处理等任务。

/*** Display Driver structure to be registered by HAL.* Only its pointer will be saved in `lv_disp_t` so it should be declared as* `static lv_disp_drv_t my_drv` or allocated dynamically.*/
typedef struct _lv_disp_drv_t {
//分辨率与显示区域设置//当前LVGL显示区域分辨率(例如设置为 480x320)lv_coord_t hor_res;         /**水平方向*/lv_coord_t ver_res;         /**垂直方向*///实际物理显示屏的全分辨率(可用于全屏或窗口模式),-1为全屏大小lv_coord_t physical_hor_res;     /**水平方向*/lv_coord_t physical_ver_res;     /**垂直方向*///显示区域在物理屏上的偏移,用于设置子区域显示(如画中画),0为全屏模式lv_coord_t offset_x;             /**水平方向*/lv_coord_t offset_y;             /**垂直方向*///缓冲与绘图配置/** 显示缓冲区配置结构体(由 lv_disp_draw_buf_init() 初始化),LVGL的主帧缓冲区*/lv_disp_draw_buf_t * draw_buf;uint32_t direct_mode : 1;        /**1:直写模式(优点:不再需要整个帧缓冲,边画边显;条件:需要提供一块“映射到显存”的 draw buffer以便直接操作显存、不支持复杂叠加(比如半透明、遮罩)、要求显示驱动快速响应;场景:常用于显示屏有自己的硬件 framebuffer(比如 Linux framebuffer、GPU))0:普通模式(就是正常需要在lv_disp_port.c中定义主帧缓冲区大小的模式)普通模式画得漂亮但慢,要缓存;直写模式快但画不出复杂图像,没缓存。*/uint32_t full_refresh : 1;       /**< 1: 每一帧都重新渲染整个屏幕(调试、渐变动画时可用)*/uint32_t sw_rotate : 1;          /**< 1: 启用软件旋转(通常较慢)*/uint32_t antialiasing : 1;       /**< 1: 启用抗锯齿*/uint32_t rotated : 2;            /**< 1: 屏幕旋转90度.   屏幕旋转标志(0、90、180、270,LV_DISP_ROT_NONE 等),@warning不会随着旋转自动更新控件坐标*/uint32_t screen_transp : 1;      /** 1:处理透明屏幕背景(比如半透明层,开启会稍慢)*/uint32_t dpi : 10;              /** 屏幕的DPI(dot per inch)信息,决定了字体、布局缩放比例,默认值为`LV_DPI_DEF`.*///核心回调函数/** 必须实现:将 draw_buf 中的数据写入屏幕(区域刷新的核心)*/void (*flush_cb)(struct _lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);/** 可选: 扩展 dirty 区域时调用,用于对齐边界(如对 monochrome 显示做8, 16 ..像素对齐)
某些 显示硬件(尤其是低阶 monochrome、DMA、GPU 显示),对刷新的区域边界有要求,因此需要在渲染前把这些区域扩大或对齐成“合法”的区域,避免 DMA 错位、闪屏、绘图混乱*/void (*rounder_cb)(struct _lv_disp_drv_t * disp_drv, lv_area_t * area);/** 可选: 像素设置回调(用于不被 LVGL 支持的像素格式)如 2 bit -> 4 gray scales* @note 比LVGL支持的颜色格式更慢*/void (*set_px_cb)(struct _lv_disp_drv_t * disp_drv, uint8_t * buf, lv_coord_t buf_w, lv_coord_t x, lv_coord_t y,lv_color_t color, lv_opa_t opa);/** 可选: 自定义清除缓冲区的方法(可选)*/void (*clear_cb)(struct _lv_disp_drv_t * disp_drv, uint8_t * buf, uint32_t size);//调试、性能、同步相关回调函数/** 可选: 刷新结束后调用,提供渲染耗时和刷新像素数(用于性能分析)*/void (*monitor_cb)(struct _lv_disp_drv_t * disp_drv, uint32_t time, uint32_t px);/** 可选: 刷新等待中周期调用(可进行低功耗操作、yield 等)*/void (*wait_cb)(struct _lv_disp_drv_t * disp_drv);/** 可选: 清理 CPU cache(用于 DMA/GPU 与 CPU 协作时)*/void (*clean_dcache_cb)(struct _lv_disp_drv_t * disp_drv);/** 可选: 当驱动器参数被更改时调用(热插拔等场景)*/void (*drv_update_cb)(struct _lv_disp_drv_t * disp_drv);/** 可选: 每帧渲染开始时调用(可用于打 log、控制背光等) */void (*render_start_cb)(struct _lv_disp_drv_t * disp_drv);//色彩配置/** chroma-key 透明色(通常用于带透明背景的图像,默认LV_COLOR_CHROMA_KEY (lv_conf.h)*/lv_color_t color_chroma_key;//自定义绘图上下文(高级),一般用于定制 GPU 渲染器、不同色彩格式支持等lv_draw_ctx_t * draw_ctx;   /**绘图上下文指针,用于自定义绘图实现*//**初始化绘图上下文*/ void (*draw_ctx_init)(struct _lv_disp_drv_t * disp_drv, lv_draw_ctx_t * draw_ctx);/**释放绘图上下文资源*/ void (*draw_ctx_deinit)(struct _lv_disp_drv_t * disp_drv, lv_draw_ctx_t * draw_ctx);size_t draw_ctx_size;       /**绘图上下文结构体的大小*/ #if LV_USE_USER_DATAvoid * user_data; /**用户可自由使用的结构体指针*/
#endif} lv_disp_drv_t;
 

渲染步骤:

样式 -> 绘图 -> 渲染帧缓存

 lv_obj_refresh_style(obj)  │
│   └─ 标记样式刷新           │
│   └─ 判断是否需要布局重算   │
│   └─ 调用 lv_obj_invalidate,登记 dirty 区域 │
└────────────┬───────────────┘
             │
             ▼
┌────────────────────────────┐
│       LVGL 定时任务        │
│     lv_timer_handler()     │
│   → 显示屏disp定时器回调函数_lv_disp_refr_timer() 
│        检测到 dirty 区域触发刷新
└────────────┬───────────────┘
             │
             ▼
┌────────────────────────────┐
│ _lv_disp_refr_timer()             │
│  → lv_refr_obj_and_children│
│     → 检查 invalidated 对象,遍历 dirty 区域,开始重绘│
└────────────┬───────────────┘
             │
             ▼
┌────────────────────────────┐
refr_area_part()    │
│  └─ lv_obj_redraw()   │
│  └─ 调用控件绘制事件lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx); │
└────────────┬───────────────┘
             │
             ▼

┌────────────────────────────┐
│lv_obj_event_base(NULL, e);基础事件处理函数    │
│  └─ 调用当前所绘制控件的draw_xxxx()绘制函数   │
│  └─ 调用lv_draw_...() 绘图函数族  rect / label / image 等│
└────────────┬───────────────┘
             │
             ▼
┌────────────────────────────┐
│ 将像素写入 draw_buf主屏幕缓冲区       │
│  →通过自己在lv_disp_port.c中写的 flush_cb刷新画面

└────────────┬───────────────┘          

void lv_style_set_transition(lv_style_t * style, const lv_style_transition_dsc_t * value)
{lv_style_value_t v = {.ptr = value};lv_style_set_prop(style, LV_STYLE_TRANSITION, v);
}void lv_style_set_prop(lv_style_t * style, lv_style_prop_t prop, lv_style_value_t value)
{lv_style_set_prop_internal(style, prop, value, lv_style_set_prop_helper);
}void _lv_obj_style_init(void){_lv_ll_init(&LV_GC_ROOT(_lv_obj_style_trans_ll), sizeof(trans_t));}

渲染通知:

    如果已应用到对象的样式发生变化(如添加或修改了属性,或对象状态发生改变),我们就需要通过void lv_obj_refresh_style(lv_obj_t * obj, lv_style_selector_t selector, lv_style_prop_t prop)函数通知 LVGL 重新渲染对象,该函数就像一个风向标,本身并不负责渲染,只负责告知哪些地方需要渲染。

    同时由于在LVGL中,渲染的目的是因为有控件的样式发生了改变(界面的背景(act_scr)实际上也是一个控件),而控件的样式刷新都需要通过 lv_obj_refresh_style()来告知渲染区域,因此我们可以认为使用lv_obj_refresh_style()是渲染的前置条件,这里称之为渲染通知,一般在 lv_obj_add_style()lv_obj_remove_style()lv_obj_report_style_change() 时调用,以更新对象的样式。

1、复杂渲染

渲染通知函数的大概工作逻辑如下:

1、使对象无效(invalidate),触发重绘。

2、根据查表法查找当前样式属性ID是否属于LV_STYLE_PROP_LAYOUT_REFR(刷新布局)、LV_STYLE_PROP_EXT_DRAW(扩展绘制区域)、LV_STYLE_PROP_INHERIT(继承传播)、LV_STYLE_PROP_LAYER_REFR(层类型)这四种渲染类型中的某一个或多个,并用对应的四个标志位来分别保存查表结果;

3、当前属性ID属于LV_STYLE_PROP_LAYOUT_REFR(刷新布局)渲染样式时,如果样式影响主部分(LV_PART_MAIN)或者 width/height 设为 LV_SIZE_CONTENT,则触发 LV_EVENT_STYLE_CHANGED 事件,通知对象样式发生变化,并标记对象布局为脏,意味着对象的大小、位置可能需要重新计算。

4、如果影响主部分(LV_PART_MAIN) 且样式属性ID属于LV_STYLE_PROP_LAYOUT_REFR(刷新布局)渲染样式或样式属性ID为LV_STYLE_PROP_ANY也需要标记父对象布局为脏以刷新父对象的布局,因为对象尺寸变了,可能影响父对象的排列。

5、如果样式属性ID属于LV_STYLE_PROP_LAYER_REFR(层类型)渲染样式则说明可能需要使用中间层进行渲染,这里将中间层分为两种图层类型,即LV_LAYER_TYPE_SIMPLE(控件使用OPA或者混合模式)、LV_LAYER_TYPE_TRANSFORM(控件旋转或者缩放),LV_LAYER_TYPE_NONE就是不用中间层进行渲染。我们首先使用calculate_layer_type(obj)计算图层类型,如果obj->spec_attr 不为空则存储当前图层类型,如果obj->spec_attr 为空且当前图层类型不为LV_LAYER_TYPE_NONE则分配 spec_attr 结构体再存储当前图层类型。

6、如果 样式属性ID属于LV_STYLE_PROP_EXT_DRAW(扩展绘制区域) 或 样式属性ID为LV_STYLE_PROP_ANY则需要重新计算对象的 扩展绘制区域(例如shadow, outline 可能需要更大绘制区域)。

7、如果:prop == LV_STYLE_PROP_ANY(所有样式属性变化)或is_inheritable == true 且 (影响扩展绘制或布局)则 递归刷新子对象的样式。特殊情况:如果是 LV_PART_SCROLLBAR,则 不需要 递归刷新子对象。

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);bool is_layout_refr = lv_style_prop_has_flag(prop, LV_STYLE_PROP_LAYOUT_REFR);bool is_ext_draw = lv_style_prop_has_flag(prop, LV_STYLE_PROP_EXT_DRAW);bool is_inheritable = lv_style_prop_has_flag(prop, LV_STYLE_PROP_INHERIT);bool is_layer_refr = lv_style_prop_has_flag(prop, LV_STYLE_PROP_LAYER_REFR);if(is_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 || is_layout_refr)) {lv_obj_t * parent = lv_obj_get_parent(obj);if(parent) lv_obj_mark_layout_as_dirty(parent);}/*Cache the layer type*/if((part == LV_PART_ANY || part == LV_PART_MAIN) && is_layer_refr) {lv_layer_type_t layer_type = calculate_layer_type(obj);if(obj->spec_attr) obj->spec_attr->layer_type = layer_type;else if(layer_type != LV_LAYER_TYPE_NONE) {lv_obj_allocate_spec_attr(obj);obj->spec_attr->layer_type = layer_type;}}if(prop == LV_STYLE_PROP_ANY || is_ext_draw) {lv_obj_refresh_ext_draw_size(obj);}lv_obj_invalidate(obj);if(prop == LV_STYLE_PROP_ANY || (is_inheritable && (is_ext_draw || is_layout_refr))) {if(part != LV_PART_SCROLLBAR) {refresh_children_style(obj);}}
}

2、简单渲染

    从lv_obj_refresh_style()函数介绍中我们可以发现,如果只是修改了颜色、透明度等简单属性而不是一些复杂的属性如字体、边框,我们其实就只使用了lv_obj_invalidate()函数对界面进行重绘,如下:

lv_obj_invalidate(obj);

lv_obj_invalidate(lv_scr_act()); /* 重新绘制整个屏幕 */

      该函数功能为标记一个对象的可视区域为“无效”区域(dirty area),即告诉 LVGL:“这个区域需要重绘”,LVGL 是按区域(Area)进行局部重绘的,因此在样式变化、内容变化、动画等情况下,必须先标记区域为脏区(dirty),然后 LVGL 才会在下一帧中只重绘这个区域,大概工作逻辑如下:

1、初始化一个区域结构体 obj_coords,用于存储这个对象需要重绘的屏幕区域;

2、计算“扩展绘制区域大小”,比如一个按钮设置了 shadowoutline,这些效果是“超出对象边框”的,所以需要扩大绘制区域;

3、lv_obj_invalidate_area(obj, &obj_coords):把这块区域注册为“需要刷新的区域”,LVGL 会将其加入 dirty 区域列表,在下一轮刷新时进行重新绘制;


void lv_obj_invalidate(const lv_obj_t * obj)
{LV_ASSERT_OBJ(obj, MY_CLASS);/*Truncate the area to the object*/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_coord_t _lv_obj_get_ext_draw_size(const lv_obj_t * obj)
{if(obj->spec_attr) return obj->spec_attr->ext_draw_size;else return 0;
}/*** Copy an area* @param dest pointer to the destination area* @param src pointer to the source area*/
inline static void lv_area_copy(lv_area_t * dest, const lv_area_t * src)
{dest->x1 = src->x1;dest->y1 = src->y1;dest->x2 = src->x2;dest->y2 = src->y2;
}

   lv_obj_invalidate_area(obj, &obj_coords)的大概工作逻辑如下:

1、获取该对象所属的显示器(display) ,LVGL 支持多个 display,每个 display 都有自己的“无效区域列表”和刷新逻辑;

2、如果此时不允许无效区域登记(比如正在刷新),则直接返回,避免冲突;

3、复制“需要刷新的区域area” 到本地变量area_tmp,避免原始数据被改动,判断区域是否在屏幕内(是否可见),不可见就直接返回不用刷新了,节省性能。

4、调用 _lv_inv_area()area_tmp 注册为 dirty 区域。LVGL 在下一帧会处理所有 dirty 区域并刷新重绘;


void lv_obj_invalidate_area(const lv_obj_t * obj, const lv_area_t * area)
{LV_ASSERT_OBJ(obj, MY_CLASS);lv_disp_t * disp   = lv_obj_get_disp(obj);if(!lv_disp_is_invalidation_enabled(disp)) return;lv_area_t area_tmp;lv_area_copy(&area_tmp, area);if(!lv_obj_area_is_visible(obj, &area_tmp)) return;_lv_inv_area(lv_obj_get_disp(obj),  &area_tmp);
}

    _lv_inv_area(lv_disp_t * disp, const lv_area_t * area_p)的大概工作逻辑如下:

1、如果输入的显示器disp句柄为NULL则将disp设置为默认显示器句柄,如果默认显示器句柄也为为NULL就直接返回,判断当前显示器 disp 是否允许进行 invalidate 操作(即脏区域标记),不允许也直接返回;

2、如果当前显示器处于刷新状态就直接返回,防止在绘制过程中修改 dirty 区域,这会导致数据错乱;  是否会导致快速输入时状态和显示不一致,丢状态的情况?看渲染触发条件

3、如果刷新区域area_p为NULL表示清空所有 dirty 区域,同时将当前有效的Dirty区域数量disp->inv_p清零;

4、计算出当前屏幕的完整区域(也就是整个屏幕的坐标范围),将 area_p 和屏幕区域取交集(com_area),裁剪出「可见部分」。如果完全在屏幕外,则直接跳过;

5、如果显卡/驱动要求“每次都全屏刷新”(full_refresh=1),那就忽略局部无效区域,直接刷全屏,将当前有效的Dirty区域数量disp->inv_p置1;

6、执行显示驱动的 rounder 回调函数,用来对 com_area 进行对齐(比如按 8px 对齐,方便 DMA)

7、如果该区域已经包含在之前的 dirty 区域中,跳过,避免重复加入。

8、保存该区域到 dirty 区域数组中(最多 LV_INV_BUF_SIZE 个)。
如果超过最大值,就清空重来,直接标记整屏刷。

9、更新 dirty 区域数量disp->inv_p,确保下一轮 lv_refr_timer() 会触发屏幕刷新。


/*** Invalidate an area on display to redraw it* @param area_p pointer to area which should be invalidated (NULL: delete the invalidated areas)* @param disp pointer to display where the area should be invalidated (NULL can be used if there is* only one display)*/
void _lv_inv_area(lv_disp_t * disp, const lv_area_t * area_p)
{if(!disp) disp = lv_disp_get_default();if(!disp) return;if(!lv_disp_is_invalidation_enabled(disp)) return;if(disp->rendering_in_progress) {LV_LOG_ERROR("detected modifying dirty areas in render");return;}/*Clear the invalidate buffer if the parameter is NULL*/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; /*Out of the screen*//*If there were at least 1 invalid area in full refresh mode, redraw the whole screen*/if(disp->driver->full_refresh) {disp->inv_areas[0] = scr_area;disp->inv_p = 1;if(disp->refr_timer) lv_timer_resume(disp->refr_timer);return;}if(disp->driver->rounder_cb) disp->driver->rounder_cb(disp->driver, &com_area);/*Save only if this area is not in one of the saved areas*/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;}/*Save the area*/if(disp->inv_p < LV_INV_BUF_SIZE) {lv_area_copy(&disp->inv_areas[disp->inv_p], &com_area);}else {   /*If no place for the area add the screen*/disp->inv_p = 0;lv_area_copy(&disp->inv_areas[disp->inv_p], &scr_area);}disp->inv_p++;if(disp->refr_timer) lv_timer_resume(disp->refr_timer);
}

开始渲染:

     void _lv_disp_refr_timer(lv_timer_t * tmr)屏幕刷新定时器回调函数,通过lv_timer_handler()被周期性调用,调用逻辑可参考

LVGL源码(3):通过LVGL显示屏刷新进一步理解lv_task_handler()函数_lvgl 源码-CSDN博客

    用于执行 屏幕刷新 + 样式过渡 + 动画执行 + 性能统计 + 缓存清理等 全流程逻辑;

大致工作逻辑如下:

1、获取当前显示器对象,如果定时器指针tmr存在就将显示屏句柄disp_refr设置为tmr的user_data参数,这里定时器tmr创建时将user_data设置为其绑定的显示屏句柄disp;

2、刷新布局lv_obj_update_layout(...):如果你有使用 lv_obj_set_flex_flow()grid 布局等,它们的计算都会在这里被更新;

3、如果当前显示屏disp中没有活动屏幕act_scr,直接退出

4、合并无效区域 & 准备绘制,这里是刷新的关键:只有无效区域会被重绘。你在前面通过lv_obj_invalidate() 标记无效区域之后,先通过lv_refr_join_area();合并重叠的无效区域,减少刷新次数,提高性能,inv_areas[i]和inv_area_joined[i]一一对应,当inv_area_joined[i]为1时表明对应的inv_areas[i]已经被合并到另一个inv_areas[x]中,绘制的时候就会忽略inv_areas[i],只有当inv_areas[i]的inv_area_joined[i]为0时才会对inv_areas[i]进行绘制;

    refr_sync_areas();  在直写模式(direct_mode = true) 且启用双缓冲时记录上帧未被重绘的无效区域,并在切换缓冲时拷贝到新缓冲区。

    最终在refr_invalid_areas();  这里被绘制出来。

5、若刷新了内容,执行清理 & 性能监控,将本轮记录的无效区域信息清空,为下一帧做准备;统计刷新耗时,如果用户注册了 monitor_cb(性能监控回调函数),就会被调用,用于记录刷新时间和像素数,便于调试或 UI 性能统计。需要注意的是如果刷新方式为双缓冲且direct_mode 为真时,此模式下显示驱动不会等待刷新完成再换缓冲区,需要手动同步哪些区域是“脏的”,以便下一次刷新前正确处理。

6、缓存清理 & 特效释放:

    lv_mem_buf_free_all();    // 临时内存释放_lv_font_clean_up_fmt_txt();   // 文字格式缓存清理#if LV_DRAW_COMPLEX_lv_draw_mask_cleanup();   // 蒙版栈清理
#endif

7、#if LV_USE_PERF_MONITOR && LV_USE_LABEL:如果你启用了性能监控标签绘制,这里将会为开发者在屏幕上输出实时 FPS / CPU 占用;

#if LV_USE_MEM_MONITOR && LV_MEM_CUSTOM == 0 && LV_USE_LABEL:如果你启用了内存监控标签绘制,这里将会为开发者在屏幕上输出实时内存占用;

lv_refer.c:
/*** Called periodically to handle the refreshing* @param tmr pointer to the timer itself*/
void _lv_disp_refr_timer(lv_timer_t * tmr)
{REFR_TRACE("begin");uint32_t start = lv_tick_get();volatile uint32_t elaps = 0;if(tmr) {disp_refr = tmr->user_data;
#if LV_USE_PERF_MONITOR == 0 && LV_USE_MEM_MONITOR == 0/*** Ensure the timer does not run again automatically.* This is done before refreshing in case refreshing invalidates something else.*/lv_timer_pause(tmr);
#endif}else {disp_refr = lv_disp_get_default();}/*Refresh the screen's layout if required*/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);/*Do nothing if there is no active screen*/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();   // 合并重叠的 invalid 区域refr_sync_areas();     // 在直写模式(direct_mode = true) 且启用双缓冲时记录上帧未更新的区域,并在切换缓冲时拷贝回来。refr_invalid_areas();  // 调用 draw_ctx 执行重绘/*If refresh happened ...*/if(disp_refr->inv_p != 0) {/*Copy invalid areas for sync next refresh in double buffered direct mode*/if(disp_refr->driver->direct_mode && disp_refr->driver->draw_buf->buf2) {uint16_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];}}/*Clean up*/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);/*Call monitor cb if present*/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();   // 文字格式缓存清理#if LV_DRAW_COMPLEX_lv_draw_mask_cleanup();   // 蒙版栈清理
#endif#if LV_USE_PERF_MONITOR && LV_USE_LABELlv_obj_t * perf_label = perf_monitor.perf_label;if(perf_label == NULL) {perf_label = lv_label_create(lv_layer_sys());lv_obj_set_style_bg_opa(perf_label, LV_OPA_50, 0);lv_obj_set_style_bg_color(perf_label, lv_color_black(), 0);lv_obj_set_style_text_color(perf_label, lv_color_white(), 0);lv_obj_set_style_pad_top(perf_label, 3, 0);lv_obj_set_style_pad_bottom(perf_label, 3, 0);lv_obj_set_style_pad_left(perf_label, 3, 0);lv_obj_set_style_pad_right(perf_label, 3, 0);lv_obj_set_style_text_align(perf_label, LV_TEXT_ALIGN_RIGHT, 0);lv_label_set_text(perf_label, "?");lv_obj_align(perf_label, LV_USE_PERF_MONITOR_POS, 0, 0);perf_monitor.perf_label = perf_label;}if(lv_tick_elaps(perf_monitor.perf_last_time) < 300) {if(px_num > 5000) {perf_monitor.elaps_sum += elaps;perf_monitor.frame_cnt ++;}}else {perf_monitor.perf_last_time = lv_tick_get();uint32_t fps_limit;uint32_t fps;if(disp_refr->refr_timer) {fps_limit = 1000 / disp_refr->refr_timer->period;}else {fps_limit = 1000 / LV_DISP_DEF_REFR_PERIOD;}if(perf_monitor.elaps_sum == 0) {perf_monitor.elaps_sum = 1;}if(perf_monitor.frame_cnt == 0) {fps = fps_limit;}else {fps = (1000 * perf_monitor.frame_cnt) / perf_monitor.elaps_sum;}perf_monitor.elaps_sum = 0;perf_monitor.frame_cnt = 0;if(fps > fps_limit) {fps = fps_limit;}perf_monitor.fps_sum_all += fps;perf_monitor.fps_sum_cnt ++;uint32_t cpu = 100 - lv_timer_get_idle();lv_label_set_text_fmt(perf_label, "%"LV_PRIu32" FPS\n%"LV_PRIu32"%% CPU", fps, cpu);}
#endif#if LV_USE_MEM_MONITOR && LV_MEM_CUSTOM == 0 && LV_USE_LABELlv_obj_t * mem_label = mem_monitor.mem_label;if(mem_label == NULL) {mem_label = lv_label_create(lv_layer_sys());lv_obj_set_style_bg_opa(mem_label, LV_OPA_50, 0);lv_obj_set_style_bg_color(mem_label, lv_color_black(), 0);lv_obj_set_style_text_color(mem_label, lv_color_white(), 0);lv_obj_set_style_pad_top(mem_label, 3, 0);lv_obj_set_style_pad_bottom(mem_label, 3, 0);lv_obj_set_style_pad_left(mem_label, 3, 0);lv_obj_set_style_pad_right(mem_label, 3, 0);lv_label_set_text(mem_label, "?");lv_obj_align(mem_label, LV_USE_MEM_MONITOR_POS, 0, 0);mem_monitor.mem_label = mem_label;}if(lv_tick_elaps(mem_monitor.mem_last_time) > 300) {mem_monitor.mem_last_time = lv_tick_get();lv_mem_monitor_t mon;lv_mem_monitor(&mon);uint32_t used_size = mon.total_size - mon.free_size;;uint32_t used_kb = used_size / 1024;uint32_t used_kb_tenth = (used_size - (used_kb * 1024)) / 102;lv_label_set_text_fmt(mem_label,"%"LV_PRIu32 ".%"LV_PRIu32 " kB used (%d %%)\n""%d%% frag.",used_kb, used_kb_tenth, mon.used_pct,mon.frag_pct);}
#endifREFR_TRACE("finished");
}

渲染逻辑:

refr_invalid_areas()

 1、没有需要刷新的无效区域,直接返回

2、找到所有需要刷新的“独立无效区域”中最后一个的位置last_i,inv_area_joined[i] == 0 代表该无效区域是“独立”的(没有被合并到其他无效区域中)

3、执行可选回调函数:disp_refr->driver->render_start_cb(disp_refr->driver)如果你自定义驱动时实现了它,这里可以通知驱动做一些准备工作(如打开 DMA 通道、锁定帧缓冲等)

4、设置状态标志:准备渲染,用于告诉底层 draw buffer

disp_refr->driver->draw_buf->last_area = 0;       //我们刚开始刷;
disp_refr->driver->draw_buf->last_part = 0;   //还没有结束;
disp_refr->rendering_in_progress = true;      //当前帧正在绘制中

5、遍历所有未被合并的无效区域,调用 refr_area() 实际绘制,如果为last_i则修改标志位last_area = 1表示当前处理的是最后一个区域,供底层绘制做结束处理。px_num:统计本次刷新绘制的像素总数,后续可用于性能监测回调(monitor_cb),绘制结束更改标志位rendering_in_progress为false;

/*** Refresh the joined areas*/
static void refr_invalid_areas(void)
{px_num = 0;if(disp_refr->inv_p == 0) return;/*Find the last area which will be drawn*/int32_t i;int32_t last_i = 0;for(i = disp_refr->inv_p - 1; i >= 0; i--) {if(disp_refr->inv_area_joined[i] == 0) {last_i = i;break;}}/*Notify the display driven rendering has started*/if(disp_refr->driver->render_start_cb) {disp_refr->driver->render_start_cb(disp_refr->driver);}disp_refr->driver->draw_buf->last_area = 0;disp_refr->driver->draw_buf->last_part = 0;disp_refr->rendering_in_progress = true;for(i = 0; i < disp_refr->inv_p; i++) {/*Refresh the unjoined areas*/if(disp_refr->inv_area_joined[i] == 0) {if(i == last_i) disp_refr->driver->draw_buf->last_area = 1;disp_refr->driver->draw_buf->last_part = 0;refr_area(&disp_refr->inv_areas[i]);px_num += lv_area_get_size(&disp_refr->inv_areas[i]);}}disp_refr->rendering_in_progress = false;
}

 refr_area():把给定的区域 area_p 按照是否是 full-refresh / direct-mode / normal mode 等方式,分块绘制(或一次性绘制),调用底层 refr_area_part() 完成实际的图像渲染。

      full_refresh 模式和普通模式(full-refresh和direct-mode都为0就是normal mode)的区别就是使用前者时需要主帧缓冲区的大小和屏幕屏幕分辨率一致,因为它要整屏一次性重绘,不能分块绘制,这样做需要很大的RAM,而且由于每次都刷新一整块屏幕,因此还对屏幕刷新接口通讯速率有要求,优点就是逻辑简单。一般我们都默认使用普通模式;

    直写模式(direct-mode)(优点:不再需要整个帧缓冲,边画边显;条件:需要提供一块“映射到显存”的 draw buffer以便直接操作显存、不支持复杂叠加(比如半透明、遮罩)、要求显示驱动快速响应;场景:常用于显示屏有自己的硬件 framebuffer(比如 Linux framebuffer、GPU))

    主要逻辑如下:

1、将当前绘图上下文 draw_ctx 的目标缓冲区设置为当前使用的帧缓冲(buf_act

2、判断是否为 full_refresh 或direct-mode模式,前者就构建整屏区域 disp_area,作为绘制目标区域,调用 refr_area_part() 绘制整个屏幕(整帧),并设置 last_part 标志标记为最后一部分;在直写模式下,仅绘制当前 area_p 区域,不做切块。并设置 last_part 标志是否为最后一块;

3、普通模式下就分块刷新,首先计算 area_p 的宽高,防止越界。然后计算当前区域最多可绘制多少行,主要依据 draw_buf 的大小来决定(比如只开了 1/10 的屏幕大小缓冲,就只能一次画最多 1/10 的区域),然后将其作为一个分块。

4、for(row = area_p->y1; row + max_row - 1 <= y2; row += max_row)分块遍历绘制每一个子区域,将大区域切成若干行块 sub_area,依次绘制,最后一块时,设置 last_part 标志。

5、收尾: if(y2 != row_last)若还有未处理的尾部区域,再绘制一次,这个是为了确保最后那一小段也能被绘制(比如分块无法整除总高时的余数);


/*** Refresh an area if there is Virtual Display Buffer* @param area_p  pointer to an area to refresh*/
static void refr_area(const lv_area_t * area_p)
{lv_draw_ctx_t * draw_ctx = disp_refr->driver->draw_ctx;draw_ctx->buf = disp_refr->driver->draw_buf->buf_act;/*With full refresh just redraw directly into the buffer*//*In direct mode draw directly on the absolute coordinates of the buffer*/if(disp_refr->driver->full_refresh || disp_refr->driver->direct_mode) {lv_area_t disp_area;lv_area_set(&disp_area, 0, 0, lv_disp_get_hor_res(disp_refr) - 1, lv_disp_get_ver_res(disp_refr) - 1);draw_ctx->buf_area = &disp_area;if(disp_refr->driver->full_refresh) {disp_refr->driver->draw_buf->last_part = 1;draw_ctx->clip_area = &disp_area;refr_area_part(draw_ctx);}else {disp_refr->driver->draw_buf->last_part = disp_refr->driver->draw_buf->last_area;draw_ctx->clip_area = area_p;refr_area_part(draw_ctx);}return;}/*Normal refresh: draw the area in parts*//*Calculate the max row num*/lv_coord_t w = lv_area_get_width(area_p);lv_coord_t h = lv_area_get_height(area_p);lv_coord_t y2 = area_p->y2 >= lv_disp_get_ver_res(disp_refr) ?lv_disp_get_ver_res(disp_refr) - 1 : area_p->y2;int32_t max_row = get_max_row(disp_refr, w, h);lv_coord_t row;lv_coord_t row_last = 0;lv_area_t sub_area;for(row = area_p->y1; row + max_row - 1 <= y2; row += max_row) {/*Calc. the next y coordinates of draw_buf*/sub_area.x1 = area_p->x1;sub_area.x2 = area_p->x2;sub_area.y1 = row;sub_area.y2 = row + max_row - 1;draw_ctx->buf_area = &sub_area;draw_ctx->clip_area = &sub_area;draw_ctx->buf = disp_refr->driver->draw_buf->buf_act;if(sub_area.y2 > y2) sub_area.y2 = y2;row_last = sub_area.y2;if(y2 == row_last) disp_refr->driver->draw_buf->last_part = 1;refr_area_part(draw_ctx);}/*If the last y coordinates are not handled yet ...*/if(y2 != row_last) {/*Calc. the next y coordinates of draw_buf*/sub_area.x1 = area_p->x1;sub_area.x2 = area_p->x2;sub_area.y1 = row;sub_area.y2 = y2;draw_ctx->buf_area = &sub_area;draw_ctx->clip_area = &sub_area;draw_ctx->buf = disp_refr->driver->draw_buf->buf_act;disp_refr->driver->draw_buf->last_part = 1;refr_area_part(draw_ctx);}
}

渲染核心逻辑:

refr_area_part():负责“在当前缓冲区上把指定区域完整绘制出来”的具体函数,内部执行了对象树遍历、背景绘制、双缓冲同步、系统层叠加、flush 输出等完整的刷新流程。

大概工作逻辑如下:

1、初始化缓冲区(可选):如果 draw_ctx 里提供了初始化函数,则先初始化缓冲区,比如清除背景色。

2、如果是 单缓冲 或 全屏双缓冲(full_refresh 模式),必须等待上一次绘制完成再画。如果定义了wait_cb()就在等待绘制完成时调用该函数;关于缓冲区绘制完成的通知函数使用,下面给一个例子:

void LV_ATTRIBUTE_FLUSH_READY lv_disp_flush_ready(lv_disp_drv_t * disp_drv)
{disp_drv->draw_buf->flushing = 0;disp_drv->draw_buf->flushing_last = 0;
}// SPI + DMA 传输完成中断回调函数
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{if (hspi->Instance == SPI1){  // 根据实际 SPI 实例lv_disp_flush_ready(disp_drv_temp);       // 通知LVGL刷新完成transfer_complete = 1;     // 设置传输完成标志LCD_CS1(1); //传输完成拉高片选}
}

3、#if LV_COLOR_SCREEN_TRANSP if(disp_refr->driver->screen_transp) 如果设置了屏幕透明(alpha 渲染),在刷新前需要清理掉之前的缓冲内容。 

4、使用lv_refr_get_top_obj()获取当前激活屏幕/前一个屏幕中,从子对象列表中按“从上到下”(即后添加的在前,因为先添加的在下层,后添加的在上层)顺序进行遍历,查找 在指定区域 buf_area 内可见、未被其他对象完全遮挡的最上层对象;

5、无对象时,绘制背景(填充背景色、背景图)

if(top_act_scr == NULL && top_prev_scr == NULL) {// draw_ctx->draw_bg 或 bg_img / bg_color 绘制背景
}

6、使用refr_obj_and_children()函数刷新对象树(当前屏幕 + 前一帧) 、刷新系统图层(Top Layer + Sys Layer),绘制控件的具体函数就是在refr_obj_and_children()函数中被调用;

7、draw_buf_flush(disp_refr);如果是普通模式(分块双缓冲):等待另一个缓冲区刷写完,标记是否是最后一个区域(只在分块渲染中使用),通知 LVGL 调用 flush_cb() 进行缓冲区输出(在 direct_mode 下不会执行 flush_cb,而是系统负责交换帧缓冲),最后进行进行缓冲区切换(双缓冲专用),将前面刷写完的缓冲区作为后面的渲染缓冲区;

    直写模式不用等待是因为其直接对显存进行操作,这种方式一般都是靠显示屏驱动的硬件同步;

 疑问点:为什么全屏大小的双缓冲需要先等待另一个缓冲区刷写完毕再对当前缓冲区进行渲染,而普通分块双缓冲则是先对当前缓冲区进行渲染,再等另一个缓冲区刷写完毕,明明后者的逻辑更合理一些?


static void refr_area_part(lv_draw_ctx_t * draw_ctx)
{lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);if(draw_ctx->init_buf)draw_ctx->init_buf(draw_ctx);/* Below the `area_p` area will be redrawn into the draw buffer.* In single buffered mode wait here until the buffer is freed.* In full double buffered mode wait here while the buffers are swapped and a buffer becomes available*/bool full_sized = draw_buf->size == (uint32_t)disp_refr->driver->hor_res * disp_refr->driver->ver_res;if((draw_buf->buf1 && !draw_buf->buf2) ||(draw_buf->buf1 && draw_buf->buf2 && full_sized)) {while(draw_buf->flushing) {if(disp_refr->driver->wait_cb) disp_refr->driver->wait_cb(disp_refr->driver);}/*If the screen is transparent initialize it when the flushing is ready*/
#if LV_COLOR_SCREEN_TRANSPif(disp_refr->driver->screen_transp) {if(disp_refr->driver->clear_cb) {disp_refr->driver->clear_cb(disp_refr->driver, disp_refr->driver->draw_buf->buf_act, disp_refr->driver->draw_buf->size);}else {lv_memset_00(disp_refr->driver->draw_buf->buf_act, disp_refr->driver->draw_buf->size * LV_IMG_PX_SIZE_ALPHA_BYTE);}}
#endif}lv_obj_t * top_act_scr = NULL;lv_obj_t * top_prev_scr = NULL;/*Get the most top object which is not covered by others*/top_act_scr = lv_refr_get_top_obj(draw_ctx->buf_area, lv_disp_get_scr_act(disp_refr));if(disp_refr->prev_scr) {top_prev_scr = lv_refr_get_top_obj(draw_ctx->buf_area, disp_refr->prev_scr);}/*Draw a display background if there is no top object*/if(top_act_scr == NULL && top_prev_scr == NULL) {lv_area_t a;lv_area_set(&a, 0, 0,lv_disp_get_hor_res(disp_refr) - 1, lv_disp_get_ver_res(disp_refr) - 1);if(draw_ctx->draw_bg) {lv_draw_rect_dsc_t dsc;lv_draw_rect_dsc_init(&dsc);dsc.bg_img_src = disp_refr->bg_img;dsc.bg_img_opa = disp_refr->bg_opa;dsc.bg_color = disp_refr->bg_color;dsc.bg_opa = disp_refr->bg_opa;draw_ctx->draw_bg(draw_ctx, &dsc, &a);}else if(disp_refr->bg_img) {lv_img_header_t header;lv_res_t res = lv_img_decoder_get_info(disp_refr->bg_img, &header);if(res == LV_RES_OK) {lv_draw_img_dsc_t dsc;lv_draw_img_dsc_init(&dsc);dsc.opa = disp_refr->bg_opa;lv_draw_img(draw_ctx, &dsc, &a, disp_refr->bg_img);}else {LV_LOG_WARN("Can't draw the background image");}}else {lv_draw_rect_dsc_t dsc;lv_draw_rect_dsc_init(&dsc);dsc.bg_color = disp_refr->bg_color;dsc.bg_opa = disp_refr->bg_opa;lv_draw_rect(draw_ctx, &dsc, draw_ctx->buf_area);}}if(disp_refr->draw_prev_over_act) {if(top_act_scr == NULL) top_act_scr = disp_refr->act_scr;refr_obj_and_children(draw_ctx, top_act_scr);/*Refresh the previous screen if any*/if(disp_refr->prev_scr) {if(top_prev_scr == NULL) top_prev_scr = disp_refr->prev_scr;refr_obj_and_children(draw_ctx, top_prev_scr);}}else {/*Refresh the previous screen if any*/if(disp_refr->prev_scr) {if(top_prev_scr == NULL) top_prev_scr = disp_refr->prev_scr;refr_obj_and_children(draw_ctx, top_prev_scr);}if(top_act_scr == NULL) top_act_scr = disp_refr->act_scr;refr_obj_and_children(draw_ctx, top_act_scr);}/*Also refresh top and sys layer unconditionally*/refr_obj_and_children(draw_ctx, lv_disp_get_layer_top(disp_refr));refr_obj_and_children(draw_ctx, lv_disp_get_layer_sys(disp_refr));draw_buf_flush(disp_refr);
}/*** Flush the content of the draw buffer*/
static void draw_buf_flush(lv_disp_t * disp)
{lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);/*Flush the rendered content to the display*/lv_draw_ctx_t * draw_ctx = disp->driver->draw_ctx;if(draw_ctx->wait_for_finish) draw_ctx->wait_for_finish(draw_ctx);/* In partial double buffered mode wait until the other buffer is freed* and driver is ready to receive the new buffer */bool full_sized = draw_buf->size == (uint32_t)disp_refr->driver->hor_res * disp_refr->driver->ver_res;if(draw_buf->buf1 && draw_buf->buf2 && !full_sized) {while(draw_buf->flushing) {if(disp_refr->driver->wait_cb) disp_refr->driver->wait_cb(disp_refr->driver);}}draw_buf->flushing = 1;if(disp_refr->driver->draw_buf->last_area && disp_refr->driver->draw_buf->last_part) draw_buf->flushing_last = 1;else draw_buf->flushing_last = 0;bool flushing_last = draw_buf->flushing_last;if(disp->driver->flush_cb) {/*Rotate the buffer to the display's native orientation if necessary*/if(disp->driver->rotated != LV_DISP_ROT_NONE && disp->driver->sw_rotate) {draw_buf_rotate(draw_ctx->buf_area, draw_ctx->buf);}else {call_flush_cb(disp->driver, draw_ctx->buf_area, draw_ctx->buf);}}/*If there are 2 buffers swap them. With direct mode swap only on the last area*/if(draw_buf->buf1 && draw_buf->buf2 && (!disp->driver->direct_mode || flushing_last)) {if(draw_buf->buf_act == draw_buf->buf1)draw_buf->buf_act = draw_buf->buf2;elsedraw_buf->buf_act = draw_buf->buf1;}
}static void call_flush_cb(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{REFR_TRACE("Calling flush_cb on (%d;%d)(%d;%d) area with %p image pointer", area->x1, area->y1, area->x2, area->y2,(void *)color_p);lv_area_t offset_area = {.x1 = area->x1 + drv->offset_x,.y1 = area->y1 + drv->offset_y,.x2 = area->x2 + drv->offset_x,.y2 = area->y2 + drv->offset_y};drv->flush_cb(drv, &offset_area, color_p);
}

refr_obj_and_children():从某个对象 top_obj 开始,递归绘制它及其子对象,同时向上追溯并绘制其兄弟对象(以及父对象的事件回调)。

工作逻辑如下:

1、确定起始对象,如果传入的 top_obj == NULL,默认取当前激活屏幕。

2、使用refr_obj(draw_ctx, top_obj);刷新对象及其所有子对象,这是对象递归绘制的核心函数:refr_obj() 会处理 top_objdraw() 回调,并递归其子对象。

3、从 top_obj 向父级逐层上溯,border_p 标记“当前处理对象”的位置,用来识别“其之后的兄弟节点”。

4、绘制当前父级对象中排在 top_obj 后的兄弟对象

5、触发父对象的 绘制后事件(LV_EVENT_DRAW_POST),用于通知父对象:其子项已绘制完,可以做额外的绘制(如遮罩、特效、边框等)。

6、把当前 parent 设为下一个 border 起点,再往上走一级,重复步骤。

总结:

  • 向下递归:绘制 top_obj 及其所有子孙。

  • 向上回溯:对其父对象中比它靠后的兄弟节点也做刷新(因为可能遮挡你)。

  • 加事件:每一级父对象调用 Post Draw 事件,允许执行额外绘制。


/*** Make the refreshing from an object. Draw all its children and the youngers too.* @param top_p pointer to an objects. Start the drawing from it.* @param mask_p pointer to an area, the objects will be drawn only here*/
static void refr_obj_and_children(lv_draw_ctx_t * draw_ctx, lv_obj_t * top_obj)
{/*Normally always will be a top_obj (at least the screen)*but in special cases (e.g. if the screen has alpha) it won't.*In this case use the screen directly*/if(top_obj == NULL) top_obj = lv_disp_get_scr_act(disp_refr);if(top_obj == NULL) return;  /*Shouldn't happen*//*Refresh the top object and its children*/refr_obj(draw_ctx, top_obj);/*Draw the 'younger' sibling objects because they can be on top_obj*/lv_obj_t * parent;lv_obj_t * border_p = top_obj;parent = lv_obj_get_parent(top_obj);/*Do until not reach the screen*/while(parent != NULL) {bool go = false;uint32_t i;uint32_t child_cnt = lv_obj_get_child_cnt(parent);for(i = 0; i < child_cnt; i++) {lv_obj_t * child = parent->spec_attr->children[i];if(!go) {if(child == border_p) go = true;}else {/*Refresh the objects*/refr_obj(draw_ctx, child);}}/*Call the post draw draw function of the parents of the to object*/lv_event_send(parent, LV_EVENT_DRAW_POST_BEGIN, (void *)draw_ctx);lv_event_send(parent, LV_EVENT_DRAW_POST, (void *)draw_ctx);lv_event_send(parent, LV_EVENT_DRAW_POST_END, (void *)draw_ctx);/*The new border will be the last parents,*so the 'younger' brothers of parent will be refreshed*/border_p = parent;/*Go a level deeper*/parent = lv_obj_get_parent(parent);}
}

refr_obj():LVGL 中绘制每个对象(lv_obj_t)的核心函数之一。它处理了对象的基本绘制逻辑,同时考虑了图层(layer)、不透明度(opa)、裁剪、旋转、缩放等高级特性。

工作逻辑如下:

1、对于隐藏对象(带有 LV_OBJ_FLAG_HIDDEN),直接跳过不渲染。

2、判断该对象的图层类型(NONE, SIMPLE, OPA, TRANSP, 等),决定渲染方式,非图层对象就使用lv_obj_redraw(draw_ctx, obj);直接绘制;如果需要使用到中间层渲染,就再做进一步处理;

3、如果使用中间层渲染,先处理旋转、缩放、混合等样式特性,然后将中间层存入临时缓冲区,再通过lv_obj_redraw(draw_ctx, obj); // 在 layer_ctx 上绘制该对象,lv_draw_layer_blend(draw_ctx, layer_ctx, &draw_dsc); // 将 layer_ctx 合成到原 draw_ctx 上

完成整个渲染流程;


void refr_obj(lv_draw_ctx_t * draw_ctx, lv_obj_t * obj)
{/*Do not refresh hidden objects*/if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return;lv_layer_type_t layer_type = _lv_obj_get_layer_type(obj);if(layer_type == LV_LAYER_TYPE_NONE) {lv_obj_redraw(draw_ctx, obj);}else {lv_opa_t opa = lv_obj_get_style_opa_layered(obj, 0);if(opa < LV_OPA_MIN) return;lv_area_t layer_area_full;lv_res_t res = layer_get_area(draw_ctx, obj, layer_type, &layer_area_full);if(res != LV_RES_OK) return;lv_draw_layer_flags_t flags = LV_DRAW_LAYER_FLAG_HAS_ALPHA;if(_lv_area_is_in(&layer_area_full, &obj->coords, 0)) {lv_cover_check_info_t info;info.res = LV_COVER_RES_COVER;info.area = &layer_area_full;lv_event_send(obj, LV_EVENT_COVER_CHECK, &info);if(info.res == LV_COVER_RES_COVER) flags &= ~LV_DRAW_LAYER_FLAG_HAS_ALPHA;}if(layer_type == LV_LAYER_TYPE_SIMPLE) flags |= LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE;lv_draw_layer_ctx_t * layer_ctx = lv_draw_layer_create(draw_ctx, &layer_area_full, flags);if(layer_ctx == NULL) {LV_LOG_WARN("Couldn't create a new layer context");return;}lv_point_t pivot = {.x = lv_obj_get_style_transform_pivot_x(obj, 0),.y = lv_obj_get_style_transform_pivot_y(obj, 0)};if(LV_COORD_IS_PCT(pivot.x)) {pivot.x = (LV_COORD_GET_PCT(pivot.x) * lv_area_get_width(&obj->coords)) / 100;}if(LV_COORD_IS_PCT(pivot.y)) {pivot.y = (LV_COORD_GET_PCT(pivot.y) * lv_area_get_height(&obj->coords)) / 100;}lv_draw_img_dsc_t draw_dsc;lv_draw_img_dsc_init(&draw_dsc);draw_dsc.opa = opa;draw_dsc.angle = lv_obj_get_style_transform_angle(obj, 0);if(draw_dsc.angle > 3600) draw_dsc.angle -= 3600;else if(draw_dsc.angle < 0) draw_dsc.angle += 3600;draw_dsc.zoom = lv_obj_get_style_transform_zoom(obj, 0);draw_dsc.blend_mode = lv_obj_get_style_blend_mode(obj, 0);draw_dsc.antialias = disp_refr->driver->antialiasing;if(flags & LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE) {layer_ctx->area_act = layer_ctx->area_full;layer_ctx->area_act.y2 = layer_ctx->area_act.y1 + layer_ctx->max_row_with_no_alpha - 1;if(layer_ctx->area_act.y2 > layer_ctx->area_full.y2) layer_ctx->area_act.y2 = layer_ctx->area_full.y2;}while(layer_ctx->area_act.y1 <= layer_area_full.y2) {if(flags & LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE) {layer_alpha_test(obj, draw_ctx, layer_ctx, flags);}lv_obj_redraw(draw_ctx, obj);draw_dsc.pivot.x = obj->coords.x1 + pivot.x - draw_ctx->buf_area->x1;draw_dsc.pivot.y = obj->coords.y1 + pivot.y - draw_ctx->buf_area->y1;/*With LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE it should also go the next chunk*/lv_draw_layer_blend(draw_ctx, layer_ctx, &draw_dsc);if((flags & LV_DRAW_LAYER_FLAG_CAN_SUBDIVIDE) == 0) break;layer_ctx->area_act.y1 = layer_ctx->area_act.y2 + 1;layer_ctx->area_act.y2 = layer_ctx->area_act.y1 + layer_ctx->max_row_with_no_alpha - 1;}lv_draw_layer_destroy(draw_ctx, layer_ctx);}
}

lv_obj_redraw():主要是通过调用lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx);事件组绘制控件主图形,调用lv_event_send(obj, LV_EVENT_DRAW_POST, draw_ctx);事件组绘制后处理内容;


void lv_obj_redraw(lv_draw_ctx_t * draw_ctx, lv_obj_t * obj)
{const lv_area_t * clip_area_ori = draw_ctx->clip_area;lv_area_t clip_coords_for_obj;/*Truncate the clip area to `obj size + ext size` area*/lv_area_t obj_coords_ext;lv_obj_get_coords(obj, &obj_coords_ext);lv_coord_t ext_draw_size = _lv_obj_get_ext_draw_size(obj);lv_area_increase(&obj_coords_ext, ext_draw_size, ext_draw_size);bool com_clip_res = _lv_area_intersect(&clip_coords_for_obj, clip_area_ori, &obj_coords_ext);/*If the object is visible on the current clip area OR has overflow visible draw it.*With overflow visible drawing should happen to apply the masks which might affect children */bool should_draw = com_clip_res || lv_obj_has_flag(obj, LV_OBJ_FLAG_OVERFLOW_VISIBLE);if(should_draw) {draw_ctx->clip_area = &clip_coords_for_obj;lv_event_send(obj, LV_EVENT_DRAW_MAIN_BEGIN, draw_ctx);lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx);lv_event_send(obj, LV_EVENT_DRAW_MAIN_END, draw_ctx);
#if LV_USE_REFR_DEBUGlv_color_t debug_color = lv_color_make(lv_rand(0, 0xFF), lv_rand(0, 0xFF), lv_rand(0, 0xFF));lv_draw_rect_dsc_t draw_dsc;lv_draw_rect_dsc_init(&draw_dsc);draw_dsc.bg_color.full = debug_color.full;draw_dsc.bg_opa = LV_OPA_20;draw_dsc.border_width = 1;draw_dsc.border_opa = LV_OPA_30;draw_dsc.border_color = debug_color;lv_draw_rect(draw_ctx, &draw_dsc, &obj_coords_ext);
#endif}/*With overflow visible keep the previous clip area to let the children visible out of this object too*With not overflow visible limit the clip are to the object's coordinates to clip the children*/lv_area_t clip_coords_for_children;bool refr_children = true;if(lv_obj_has_flag(obj, LV_OBJ_FLAG_OVERFLOW_VISIBLE)) {clip_coords_for_children  = *clip_area_ori;}else {if(!_lv_area_intersect(&clip_coords_for_children, clip_area_ori, &obj->coords)) {refr_children = false;}}if(refr_children) {draw_ctx->clip_area = &clip_coords_for_children;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];refr_obj(draw_ctx, child);}}/*If the object was visible on the clip area call the post draw events too*/if(should_draw) {draw_ctx->clip_area = &clip_coords_for_obj;/*If all the children are redrawn make 'post draw' draw*/lv_event_send(obj, LV_EVENT_DRAW_POST_BEGIN, draw_ctx);lv_event_send(obj, LV_EVENT_DRAW_POST, draw_ctx);lv_event_send(obj, LV_EVENT_DRAW_POST_END, draw_ctx);}draw_ctx->clip_area = clip_area_ori;
}

 调用控件绘制事件

     我们知道每一个控件都有其绘制函数draw_main(),有的复杂控件还有draw_xx()函数,这里拿dropdowm控件举例:下面代码中的draw_main(e);就是绘制下拉列表控件主体的函数,而draw_list(e);就是绘制下拉列表控件下拉列表的函数;


static void lv_dropdown_event(const lv_obj_class_t * class_p, lv_event_t * e)
{...else if(code == LV_EVENT_DRAW_MAIN) {draw_main(e);}
}static void lv_dropdown_list_event(const lv_obj_class_t * class_p, lv_event_t * e)
{...else if(code == LV_EVENT_DRAW_POST) {draw_list(e);res = lv_obj_event_base(MY_CLASS_LIST, e);if(res != LV_RES_OK) return;}
}

    我们从LVGL源码(4):LVGL关于EVENT事件的响应逻辑_lvgl event-CSDN博客中可以知道,当我们调用lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx);时,最终会执行一个lv_obj_event_base(NULL, e);基础事件处理函数(对象的基类的事件处理,控件面向LVGL系统的事件),该函数就会调用当前控件的事件回调函数,如果当前控件为lv_dropdown的话那么毫无疑问其事件回调函数lv_dropdown_event中的draw_main(e)主体绘制函数将会得以执行;lv_event_send(obj, LV_EVENT_DRAW_POST, draw_ctx);同理;

    因此我们可以知道,在上面渲染过程中一层又一层的函数嵌套,最终执行控件绘制的行为还是通过调用lv_event_send(obj, LV_EVENT_DRAW_MAIN, draw_ctx);和lv_event_send(obj, LV_EVENT_DRAW_POST, draw_ctx);事件处理函数,然后调用最终的draw_main(e)、draw_xxxx(e)等来进行实现;

样式过渡渲染实现:

/***************************未完成*************************/

void _lv_obj_style_init(void){ 

_lv_ll_init(&LV_GC_ROOT(_lv_obj_style_trans_ll), sizeof(trans_t));

}

void _lv_obj_style_create_transition(lv_obj_t * obj, lv_part_t part, lv_state_t prev_state, lv_state_t new_state,const _lv_obj_style_transition_dsc_t * tr_dsc)

   判断样式是否变化,如果开启了 LV_STYLE_TRANSITION,就会向 _lv_obj_style_trans_ll 添加 trans_t 节点,启动动画。通过调用lv_timer_handler() -> lv_anim_timer():在全局 timer tick 中推进每一帧。


void _lv_obj_style_create_transition(lv_obj_t * obj, lv_part_t part, lv_state_t prev_state, lv_state_t new_state,const _lv_obj_style_transition_dsc_t * tr_dsc)
{trans_t * tr;/*Get the previous and current values*/obj->skip_trans = 1;obj->state = prev_state;lv_style_value_t v1 = lv_obj_get_style_prop(obj, part, tr_dsc->prop);obj->state = new_state;lv_style_value_t v2 = lv_obj_get_style_prop(obj, part, tr_dsc->prop);obj->skip_trans = 0;if(v1.ptr == v2.ptr && v1.num == v2.num && v1.color.full == v2.color.full)  return;obj->state = prev_state;v1 = lv_obj_get_style_prop(obj, part, tr_dsc->prop);obj->state = new_state;_lv_obj_style_t * style_trans = get_trans_style(obj, part);lv_style_set_prop(style_trans->style, tr_dsc->prop, v1);   /*Be sure `trans_style` has a valid value*/if(tr_dsc->prop == LV_STYLE_RADIUS) {if(v1.num == LV_RADIUS_CIRCLE || v2.num == LV_RADIUS_CIRCLE) {lv_coord_t whalf = lv_obj_get_width(obj) / 2;lv_coord_t hhalf = lv_obj_get_height(obj) / 2;if(v1.num == LV_RADIUS_CIRCLE) v1.num = LV_MIN(whalf + 1, hhalf + 1);if(v2.num == LV_RADIUS_CIRCLE) v2.num = LV_MIN(whalf + 1, hhalf + 1);}}tr = _lv_ll_ins_head(&LV_GC_ROOT(_lv_obj_style_trans_ll));LV_ASSERT_MALLOC(tr);if(tr == NULL) return;tr->start_value = v1;tr->end_value = v2;tr->obj = obj;tr->prop = tr_dsc->prop;tr->selector = part;lv_anim_t a;lv_anim_init(&a);lv_anim_set_var(&a, tr);lv_anim_set_exec_cb(&a, trans_anim_cb);lv_anim_set_start_cb(&a, trans_anim_start_cb);lv_anim_set_ready_cb(&a, trans_anim_ready_cb);lv_anim_set_values(&a, 0x00, 0xFF);lv_anim_set_time(&a, tr_dsc->time);lv_anim_set_delay(&a, tr_dsc->delay);lv_anim_set_path_cb(&a, tr_dsc->path_cb);lv_anim_set_early_apply(&a, false);
#if LV_USE_USER_DATAa.user_data = tr_dsc->user_data;
#endiflv_anim_start(&a);
}

相关文章:

LVGL源码(7):渲染

在LVGL源码(4):LVGL关于EVENT事件的响应逻辑_lvgl实现显示打车-CSDN博客这篇文章中&#xff0c;我们提到了LVGL的三大步骤&#xff1a;检测用户输入操作、调用我们编写的逻辑、在屏幕上显示对应的画面&#xff1b;而在学习完“样式”之后&#xff0c;我们或许可以将上述步骤说明…...

02_通过调用硅基流动平台deepseekapi按输入的标题生成文章

from openai import OpenAIclient OpenAI(base_urlhttps://api.siliconflow.cn/v1,api_keyyou api-key )# 定义关键词变量 keyword "人性的弱点都有哪些&#xff1f;"# 发送带有流式输出的请求 response client.chat.completions.create(model"deepseek-ai/D…...

三、Virtual Device Manager

一、创建AVD AVD是Android Virtual Device&#xff08;安卓虚拟设备&#xff09;,我们可以启动Android Studio 选择 Virtual Device Manager 创建并启动一个模拟器。 二、设置屏幕大小 上面直接创建的镜像是不能设置屏幕大小的&#xff0c;启动后笔记本屏幕都放不下&#xff…...

MATLAB2022b安装

1 从百度网盘下载MATLAB2022b&#xff0c;下载完成后解压到某个文件夹&#xff1b; 链接: MATLAB2022b 提取码: 6666 2 打开解压后的文件夹&#xff0c;进入setup文件夹&#xff0c;双击打开“setup.exe”文件&#xff1b; 3 在弹出窗口中选择“高级选项”-->“我有文件安…...

计算机编码

计算机&#xff0c;不能直接存储文字&#xff0c;存储的是编码。 计算机只能处理二进制的数据&#xff0c;其它数据&#xff0c;比如&#xff1a;0-9、a-z、A-Z&#xff0c;这些字符&#xff0c;我们可以定义一套规则来表示。假如&#xff1a;A用110表示&#xff0c;B用111表示…...

Dell EMC Unity NAS 认证方式介绍

近日有个客户要配置EMC Unity的NAS访问&#xff0c;我们知道NAS有Linux环境下的NFS和Windows环境下的SMB&#xff08;也叫做CIFS&#xff09;。单独配置其中的一种访问协议相对简单&#xff0c;但是客户提出的要求是要对文件系统同时NFS和SMB访问&#xff0c;这就有些复杂&…...

SpringAi 会话记忆功能

在使用chatGPT&#xff0c;豆包等产品后&#xff0c;就会发现他们的会话有“记忆”功能。 那么我们用API接口的话&#xff0c;这个是怎么实现的呢&#xff1f; 属于比较粗暴的方式&#xff0c;把之前的内容与新的提示词一起再次发给大模型。让我们看到他们有记忆功能。 下面介绍…...

BUUCTF-web刷题篇(25)

34.the mystery of ip 给出链接&#xff0c;输入得到首页&#xff1a; 有三个按钮&#xff0c;flag点击后发现页面窃取客户端的IP地址&#xff0c;通过给出的github代码中的php文件发现可以通过XFF或Client-IP传入值。使用hackbar或BP 使用XSS&#xff0c;通过github给出的目录…...

Elasticsearch 性能优化:从原理到实践的全面指南

Elasticsearch&#xff08;ES&#xff09;作为一款基于 Lucene 的分布式搜索和分析引擎&#xff0c;广泛应用于日志分析、搜索引擎和实时数据处理等场景。然而&#xff0c;在高并发、大数据量环境下&#xff0c;Elasticsearch 的性能可能面临瓶颈&#xff0c;如查询延迟高、索引…...

UITableVIew性能优化概述

UITableVIew性能优化概述 文章目录 UITableVIew性能优化概述前言如何优化优化的本质卡顿的原因 CPU层级cell复用UITableVIew尽量采用复用 定义cell的种类尽量少&#xff0c;可以多用hidden缓存cell高度基础设置预先设置高度设置一个预先缓存 异步绘制滑动按照需加载尽量显示大小…...

【Linux网络与网络编程】09.传输层协议TCP

前言 TCP 即 传输控制协议 (Transmission Control Protocol)&#xff0c;该协议要对数据的传输进行一个详细的控制&#xff08;数据传输时什么时候传输&#xff0c;一次发多少&#xff0c;怎么发&#xff0c;出错了怎么办……&#xff09; 本篇博客将从下面这张TCP协议格式图…...

08.unity 游戏开发-unity编辑器资源的导入导出分享

08.unity 游戏开发-unity编辑器资源的导入导出分享 提示&#xff1a;帮帮志会陆续更新非常多的IT技术知识&#xff0c;希望分享的内容对您有用。本章分享的是Python基础语法。前后每一小节的内容是存在的有&#xff1a;学习and理解的关联性&#xff0c;希望对您有用~ unity简介…...

Docker Swarm 集群

Docker Swarm 集群 本文档介绍了 Docker Swarm 集群的基本概念、工作原理以及相关命令使用示例&#xff0c;包括如何在服务调度中使用自定义标签。本文档适用于需要管理和扩展 Docker 容器化应用程序的生产环境场景。 1. 什么是 Docker Swarm Docker Swarm 是用于管理 Docker…...

数据中台、数据湖和数据仓库 区别

1. 核心定义与定位 数据仓库&#xff08;Data Warehouse&#xff09; 定义&#xff1a;面向主题的、集成的、历史性且稳定的结构化数据集合&#xff0c;主要用于支持管理决策和深度分析。定位&#xff1a;服务于管理层和数据分析师&#xff0c;通过历史数据生成报表和商业智能…...

【CodeMirror】系列(二)官网示例(五)可撤销操作、拆分视图、斑马条纹

一、可撤销操作 默认情况下&#xff0c;history 历史记录扩展仅跟踪文档和选择的更改&#xff0c;撤销操作只会回滚这些更改&#xff0c;而不会影响编辑器状态的其他部分。 不过你也可以将其他的操作定义成可撤销的。如果把这些操作看作状态效果&#xff0c;就可以把相关功能整…...

SpringBoot 动态路由菜单 权限系统开发 菜单权限 数据库设计 不同角色对应不同权限

介绍 系统中的路由配置可以根据用户的身份、角色或其他权限信息动态生成&#xff0c;而不是固定在系统中。不同的用户根据其权限会看到不同的路由&#xff0c;访问不同的页面。对应各部门不同的权限。 效果 [{"id": 1,"menuName": "用户管理"…...

scikit-learn 开源框架在机器学习中的应用

文章目录 scikit-learn 开源框架介绍1. 框架概述1.1 基本介绍1.2 版本信息 2. 核心功能模块2.1 监督学习2.2 无监督学习2.3 数据处理 3. 关键设计理念3.1 统一API设计3.2 流水线(Pipeline) 4. 重要辅助功能4.1 模型选择4.2 评估指标 5. 性能优化技巧5.1 并行计算5.2 内存优化 6…...

GPT-4、Grok 3与Gemini 2.0 Pro:三大AI模型的语气、风格与能力深度对比

更新后的完整CSDN博客文章 以下是基于您的要求&#xff0c;包含修正后的幻觉率部分并保留原始信息的完整CSDN博客风格文章。幻觉率已调整为更符合逻辑的描述&#xff0c;其他部分保持不变。 GPT-4、Grok 3与Gemini 2.0 Pro&#xff1a;三大AI模型的语气、风格与能力深度对比 …...

Cyber Weekly #51

赛博新闻 1、英伟达开源新模型&#xff0c;性能直逼DeepSeek-R1 本周&#xff0c;英伟达开源了基于Meta早期Llama-3.1-405B-Instruct模型开发的Llama-3.1-Nemotron-Ultra-253B-v1大语言模型&#xff0c;该模型拥有2530亿参数&#xff0c;在多项基准测试中展现出与6710亿参数的…...

QT聊天项目开发DAY02

1.添加输入密码的保密性 LoginWidget::LoginWidget(QDialog*parent): QDialog(parent) {ui.setupUi(this);ui.PassWord_Edit->setEchoMode(QLineEdit::Password);BindSlots(); }2.添加密码的验证提示 3.修复内存泄漏&#xff0c;并嵌套UI子窗口到主窗口里面 之前并没有设置…...

Spring AI高级RAG功能查询重写和查询翻译

1、创建查询重写转换器 // 创建查询重写转换器queryTransformer RewriteQueryTransformer.builder().chatClientBuilder(openAiChatClient.mutate()).build(); 查询重写是RAG系统中的一个重要优化技术&#xff0c;它能够将用户的原始查询转换成更加结构化和明确的形式。这种转…...

速盾:高防CDN的原理和高防IP一样吗?

随着互联网的发展&#xff0c;网络安全威胁日益严重&#xff0c;尤其是DDoS攻击、CC攻击等恶意行为&#xff0c;给企业带来了巨大的风险。为了应对这些挑战&#xff0c;许多企业开始采用高防CDN&#xff08;内容分发网络&#xff09;和高防IP作为防御措施。尽管两者都能提供一定…...

SQLite-Web:一个轻量级的SQLite数据库管理工具

SQLite-Web 是一个基于 Web 浏览器的轻量级 SQLite 数据库管理工具。它基于 Python 开发&#xff0c;免费开源&#xff0c;无需复杂的安装或配置&#xff0c;适合快速搭建本地或内网的 SQLite 管理和开发环境。 SQLite-Web 支持常见的 SQLite 数据库管理和开发任务&#xff0c;…...

数智读书笔记系列028 《奇点更近》

一、引言 在科技飞速发展的今天&#xff0c;我们对未来的好奇与日俱增。科技将如何改变我们的生活、社会乃至人类本身&#xff1f;雷・库兹韦尔的《奇点更近》为我们提供了深刻的见解和大胆的预测&#xff0c;让我们得以一窥未来几十年的科技蓝图。这本书不仅是对未来科技趋势…...

深入理解linux操作系统---第4讲 用户、组和密码管理

4.1 UNIX系统的用户和组 4.1.1 用户与UID UID定义&#xff1a;用户身份唯一标识符&#xff0c;16位或32位整数&#xff0c;范围0-65535。系统用户UID为0&#xff08;root&#xff09;、1-999&#xff08;系统服务&#xff09;&#xff0c;普通用户从1000开始分配特殊UID&…...

系统设计模块之安全架构设计(常见攻击防御(SQL注入、XSS、CSRF、DDoS))

一、SQL注入攻击防御 SQL注入是通过恶意输入篡改数据库查询逻辑的攻击方式&#xff0c;可能导致数据泄露或数据库破坏。防御核心在于隔离用户输入与SQL代码&#xff0c;具体措施包括&#xff1a; 参数化查询&#xff08;预编译语句&#xff09; 原理&#xff1a;将SQL语句与用…...

redission锁释放失败处理

redission锁释放失败处理 https://www.jianshu.com/p/055ae798547a 就是可以删除 锁的key 这样锁就释放了&#xff0c;但是 还是要结合业务&#xff0c;这种是 非正规的处理方式&#xff0c;还是要在代码层面进行处理。...

Visual Studio Code 在.S汇编文件中添加调试断点及功能简介

目录 一、VS Code汇编文件添加断点二、VS Code断点调试功能简介1. 设置断点(1) 单行断点(2) 条件断点(3) 日志断点 2. 查看断点列表3. 调试时的断点控制4. 禁用/启用断点5. 删除断点6. 条件断点的使用7. 多线程调试8. 远程调试9. 调试配置文件 一、VS Code汇编文件添加断点 最…...

计算视觉与数学结构及AI拓展

在快速发展的计算视觉领域&#xff0c;算法、图像处理、神经网络和数学结构的交叉融合&#xff0c;在提升我们对视觉感知和分析的理解与能力方面发挥着关键作用。本文探讨了支撑计算视觉的基本概念和框架&#xff0c;强调了数学结构在开发鲁棒的算法和模型中的重要性。 AI拓展…...

Vue2 老项目升级 Vue3 深度解析教程

Vue2 老项目升级 Vue3 深度解析教程 摘要 Vue3 带来了诸多改进和新特性&#xff0c;如性能提升、组合式 API、更好的 TypeScript 支持等&#xff0c;将 Vue2 老项目升级到 Vue3 可以让项目获得这些优势。本文将深入解析升级过程&#xff0c;涵盖升级前的准备工作、具体升级步骤…...

器件封装-2025.4.13

1.器件网格设置要与原理图一致&#xff0c;同时器件符号要与数据手册一致 2.或者通过向导进行编辑&#xff0c;同时电机高级符号向导进行修改符号名称 2.封装一般尺寸大小要比数据手册大2倍到1.5倍 焊盘是在顶层绘制&#xff0c;每个焊盘距离要用智能尺子测量是否跟数据手册一…...

Python 基础语法汇总

Python 语法 │ ├── 基本结构 │ ├── 语句&#xff08;Statements&#xff09; │ │ ├── 表达式语句&#xff08;如赋值、算术运算&#xff09; │ │ ├── 控制流语句&#xff08;if, for, while&#xff09; │ │ ├── 定义语句&#xff08;def…...

Java函数式编程魔法:Stream API的10种妙用

在Java 8中引入的Stream API为函数式编程提供了一种全新的方式。它允许我们以声明式的方式处理数据集合&#xff0c;使代码更加简洁、易读且易于维护。本文将介绍Stream API的10种妙用&#xff0c;帮助你更好地理解和应用这一强大的工具。 1. 过滤操作&#xff1a;筛选符合条件…...

【力扣hot100题】(094)编辑距离

记得最初做这题完全没思路&#xff0c;这次凭印象随便写了一版居然对了。 感觉这题真的有点为出题而出题的意思&#xff0c;谁拿到这题会往动态规划方向想啊jpg 也算是总结出规律了&#xff0c;凡是遇到这种比较俩字符串的十有八九是动态规划&#xff0c;而且是二维动态规划&…...

穿透三层内网VPC2

网络拓扑 目标出网web地址&#xff1a;192.168.139.4 信息收集端口扫描&#xff1a; 打开8080端口是一个tomcat的服务 版本是Apache Tomcat/7.0.92 很熟悉了&#xff0c;可能存在弱口令 tomcat/tomcat 成功登录 用哥斯拉生成马子&#xff0c;上传war包&#xff0c;进入后台 C…...

AI数字消费第一股,重构商业版图的新物种

伍易德带领团队发布“天天送AI数字商业引擎”&#xff0c;重新定义流量与消费的关系 【2025年4月&#xff0c;深圳】在人工智能浪潮席卷全球之际&#xff0c;深圳天天送网络科技有限公司于深圳大中华喜来登酒店重磅召开“AI数字消费第一股”发布盛典。公司创始人伍易德首次系统…...

Unity 基于navMesh的怪物追踪惯性系统

今天做项目适合 策划想要实现一个在现有的怪物追踪系统上实现怪物拥有惯性功能 以下是解决方案分享&#xff1a; 怪物基类代码&#xff1a; ​ using UnityEngine; using UnityEngine.AI;[RequireComponent(typeof(NavMeshAgent))] [RequireComponent(typeof(AudioSource))] …...

【OpenCV】【XTerminal】talk程序运用和linux进程之间通信程序编写,opencv图像库编程联系

目录 一、talk程序的运用&Linux进程间通信程序的编写 1.1使用talk程序和其他用户交流 1.2用c语言写一个linux进程之间通信&#xff08;聊天&#xff09;的简单程序 1.服务器端程序socket_server.c编写 2.客户端程序socket_client.c编写 3.程序编译与使用 二、编写一个…...

中断的硬件框架

今天呢&#xff0c;我们来讲讲中断的硬件框架&#xff0c;这里会去举3个开发板&#xff0c;去了解中断的硬件框架&#xff1a; 中断路径上的3个部件&#xff1a; 中断源 中断源多种多样&#xff0c;比如GPIO、定时器、UART、DMA等等。 它们都有自己的寄存器&#xff0c;可以…...

大数据面试问答-Hadoop/Hive/HDFS/Yarn

1. Hadoop 1.1 MapReduce 1.1.1 Hive语句转MapReduce过程 可分为 SQL解析阶段、语义分析阶段、逻辑计划生成阶段、逻辑优化阶段、物理计划生成阶段。 SQL解析阶段 词法分析(Lexical Analysis)&#xff1a;使用Antlr3将SQL字符串拆分为有意义的token序列 语法分析(Syntax An…...

【小沐学GIS】基于C++绘制三维数字地球Earth(QT5、OpenGL、GIS、卫星)第五期

&#x1f37a;三维数字地球系列相关文章如下&#x1f37a;&#xff1a;1【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;OpenGL、glfw、glut&#xff09;第一期2【小沐学GIS】基于C绘制三维数字地球Earth&#xff08;OpenGL、glfw、glut&#xff09;第二期3【小沐学GIS】…...

初始图形学(3)

昨天休息了一天&#xff0c;今天继续图形学的学习 向场景发射光线 现在我们我们准备做一个光线追踪器。其核心在于&#xff0c;光线追踪程序通过每个像素发送光线。这意味着对于图像中的每个像素点&#xff0c;程序都会计算一天从观察者出发&#xff0c;穿过该像素的光线。并…...

如果想在 bean 创建出来之前和销毁之前做一些自定义操作可以怎么来实现呢?

使用生命周期扩展接口&#xff08;最灵活&#xff09;​ 创建前拦截可以通过实现 InstantiationAwareBeanPostProcessor 接口的 postProcessBeforeInstantiation 方法&#xff0c;在Bean实例化前执行逻辑 在销毁前拦截可以通过实现 DestructionAwareBean 接口的 postProcessBe…...

【甲子光年】DeepSeek开启AI算法变革元年

目录 引言人工智能的发展拐点算力拐点&#xff1a;DeepSeek的突破数据拐点&#xff1a;低参数量模型的兴起算法创新循环算法变革推动AI普惠应用全球AI科技竞争进入G2时代结论 引言 2025年&#xff0c;人工智能的发展已经走到了一个战略拐点。随着技术能力的不断提升&#xff0…...

Go语言--语法基础4--基本数据类型--整数类型

整型是所有编程语言里最基础的数据类型。 Go 语言支持如下所示的这些整型类型。 需要注意的是&#xff0c; int 和 int32 在 Go 语言里被认为是两种不同的类型&#xff0c;编译器也不会帮你自动做类型转换&#xff0c; 比如以下的例子会有编译错误&#xff1a; var value2 in…...

MCP基础学习计划详细总结

MCP基础学习计划详细总结 1.MCP概述与基础 • MCP&#xff08;Model Context Protocol&#xff09;&#xff1a;由Anthropic公司于2024年11月推出&#xff0c;旨在实现大型语言模型&#xff08;LLM&#xff09;与外部数据源和工具的无缝集成。 • 核心功能&#xff1a; • 资…...

大模型到底是怎么产生的?一文揭秘大模型诞生全过程

前言 大模型到底是怎么产生的呢? 本文将从最基础的概念开始,逐步深入,用通俗易懂的语言为大家揭开大模型的神秘面纱。 大家好,我是大 F,深耕AI算法十余年,互联网大厂核心技术岗。 知行合一,不写水文,喜欢可关注,分享AI算法干货、技术心得。 【专栏介绍】: 欢迎关注《…...

Node.js介绍

一、Node.js 核心定义 本质&#xff1a;基于 Chrome V8 引擎构建的 JavaScript 运行时环境&#xff0c;用于在服务器端执行 JavaScript 代码。 定位&#xff1a;非阻塞、事件驱动的 I/O 模型&#xff0c;专为高并发、实时应用设计。 诞生&#xff1a;2009 年由 Ryan Dahl 发布…...

DRABP_NSGA2最新算法神圣宗教算法优化BP做代理模型,NSGA2反求最优因变量和对应的最佳自变量组合,Matlab代码

一、神圣宗教算法&#xff08;DRA&#xff09;优化BP代理模型 1. DRA的核心原理 DRA是一种模拟宗教社会层级互动的元启发式算法&#xff0c;通过“追随者学习”、“传教士传播”和“领导者引导”三种行为模式优化搜索过程。在BP神经网络优化中&#xff0c;DRA通过以下步骤调整…...

Android Studio 在 Windows 上的完整安装与使用指南

Android Studio 在 Windows 上的完整安装与使用指南—目录 一、Android Studio 简介二、下载与安装1. 下载 Android Studio2. 安装前的依赖准备3. 安装步骤 三、基础使用指南1. 首次启动配置2. 创建第一个项目3. 运行应用4. 核心功能 四、进阶功能配置1. 配置 SDK 和工具2. 自定…...