LVGL源码学习之渲染、更新过程(2)---无效区域的处理

LVGL版本:8.1

往期回顾:

LVGL源码学习之渲染、更新过程(1)---标记和激活

区域合并

       在前面的代码分析中,发现标记无效区域的工作其实很繁琐,虽然大部分区域因为包含关系被剔除,但仍可能存在相互交叉的区域,导致重复计算减少效率。因此在正式处理前,还需要根据情况选择性地合并这些交叉的区域。

       前面在标记无效区域时,将区域坐标及其数量都存储在inv_areas[]数组和inv_p这两个属性里,而还有一个属性inv_area_joined[]用于标明被合并掉的区域(数组元素值为1表示该索引对应的无效区域已被合并,后续刷新将略过它):

//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;  //无效区域数量uint8_t inv_area_joined[LV_INV_BUF_SIZE];  //数组,用于标明被合并的区域/*......*/
} lv_disp_t;

       具体合并算法过程如下面函数,其中用join_in下标表示合并主体区域,用join_from下标表示被合并区域:

//lv_refr.c
static void lv_refr_join_area(void)
{uint32_t join_from;uint32_t join_in;lv_area_t joined_area;/* 外循环,每次取一个合并主体区域 */for(join_in = 0; join_in < disp_refr->inv_p; join_in++) {if(disp_refr->inv_area_joined[join_in] != 0) continue;  //如果该区域已经被合并,则跳过/* 内循环,将其它被合并区域和外循环的合并主体区域进行比对,查看重叠情况 */for(join_from = 0; join_from < disp_refr->inv_p; join_from++) {if(disp_refr->inv_area_joined[join_from] != 0 || join_in == join_from) {continue;     //如果该区域是合并主体自身,或者该区域已经被合并,则跳过}/* 检查两块区域是否存在重叠 */if(_lv_area_is_on(&disp_refr->inv_areas[join_in], &disp_refr->inv_areas[join_from]) == false) {continue;}/* 生成一块更大的区域,使它刚好同时容纳两块重叠的区域 */_lv_area_join(&joined_area, &disp_refr->inv_areas[join_in], &disp_refr->inv_areas[join_from]);/* 只有当合成区域面积小于两块重叠区域面积之和,才将生成区域保存 */if(lv_area_get_size(&joined_area) < (lv_area_get_size(&disp_refr->inv_areas[join_in]) +lv_area_get_size(&disp_refr->inv_areas[join_from]))) {lv_area_copy(&disp_refr->inv_areas[join_in], &joined_area);/* 标记被合并的区域,下次遍历将跳过这块区域 */disp_refr->inv_area_joined[join_from] = 1;}}}
}

       其中的_lv_area_is_on()函数就是检查两块区域是否存在重叠:

       如果有重叠,_lv_area_join()函数会将两块重叠区域合成一个更大的区域。(或者这样描述:生成一个新的区域,使它刚好同时容纳两块重叠区域)。

       合成新区域后,还需要对比和之前两块重叠区域的面积进行比较,若小于两块面积之和,则将新区域覆盖到inv_area[join_in],并将inv_area_joined[join_from]置1,表示被合并(后续会被忽略),否则废弃。

       以上过程可以用下图来形象解释:

区域分块更新

       在注册显示器时,用于显示的缓存大小一般会设置为显示器像素总数,但有时候内存不足,可能仅设置了一个小缓存,同时无效区域的大小是有可能超出这个缓存大小的,因此在合并好区域后,又调用lv_refr_areas()对每个区域按行再进行一次分块刷新,具体分块原理后面会进一步说明。

//lv_refr.c
static void lv_refr_areas(void)
{px_num = 0;if(disp_refr->inv_p == 0) return;  //当前不存在无效区域,直接返回/* 找到最后一块要绘制的区域 */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;}}disp_refr->driver->draw_buf->last_area = 0;disp_refr->driver->draw_buf->last_part = 0;for(i = 0; i < disp_refr->inv_p; i++) {/* 更新合并后的区域 */if(disp_refr->inv_area_joined[i] == 0) {/* 最后一块合并区域,置位last_area标志 */if(i == last_i) disp_refr->driver->draw_buf->last_area = 1;disp_refr->driver->draw_buf->last_part = 0;  //在开始处理前清空last_part标志,该标志会在下面的函数完成后置位lv_refr_area(&disp_refr->inv_areas[i]);  //具体进行分块刷新的函数px_num += lv_area_get_size(&disp_refr->inv_areas[i]);  //累计区域像素数}}
}

        这里涉及到两个标志位,后续的解读会用“区域”来指代area,用“”来指代part

//lv_hal_disp.h
typedef struct _lv_disp_draw_buf_t {/*......*/volatile uint32_t last_area : 1;  /* 1表示最后一块区域正在被渲染*/volatile uint32_t last_part : 1;  /* 1表示该区域的最后一块正在被渲染*//*......*/
} lv_disp_draw_buf_t;

       上面函数针对每个合并后的区域,都使用lv_refr_area()进行分块刷新,具体分块的方法可以通俗解释为“在区域内等间距地画几条贯穿的横线分隔出几块”,即对区域的行数进行拆分,注意每次分块大小不能大于计算缓存(注册时传入,通常等于显示器总像素数),下面用图形解析不同的情况(绿色框代表显示器像素范围,红色框代表无效区域):

①渲染区域宽度超出显示器宽度(这通常不会发生,但为了减少可能存在的错误,依然考虑在内),此时单次刷新的最大高度被限制为max_row=buffer_size/area_width,根据区域高度大小有三种情况:

  • 区域高度很小

  • 区域高度很大但未超出显示器像素高度

  • 区域高度完全超出显示器像素高度

       注意上面第二、三点,最后一次刷新的分块高度,通常会小于理论的最大高度(max_row

②渲染区域宽高在显示器内,但显示缓存不足(小于显示器像素总数),也会发生和①类似的情况:

③渲染区域的宽度在显示器宽度内,但高度超出显示器高度(这通常也不会发生),此时截取掉超出的部分,仅渲染处于显示器内的区域,并一次性刷新完毕:

④渲染区域的宽高都在显示器范围内,也是一次性刷新:

        代码如下:

//lv_refr.c
static void lv_refr_area(const lv_area_t * area_p)
{/* 如果设置了全刷新,直接刷新全屏范围,并设置last_part为1表示最后一块 */if(disp_refr->driver->full_refresh) {lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);draw_buf->area.x1        = 0;draw_buf->area.x2        = lv_disp_get_hor_res(disp_refr) - 1;draw_buf->area.y1        = 0;draw_buf->area.y2        = lv_disp_get_ver_res(disp_refr) - 1;disp_refr->driver->draw_buf->last_part = 1;lv_refr_area_part(area_p);return;}/* 常规刷新方式: 分块刷新区域(重点) */lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);  //获取显示缓存buffer/* 计算单次刷新的最大行数 */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 = (uint32_t)draw_buf->size / w;  //size就是显示器的总像素数if(max_row > h) max_row = h;  //max_row就是分块后,单次刷新的最大行数/* 舍入操作(如有定义) */if(disp_refr->driver->rounder_cb) {lv_area_t tmp;tmp.x1 = 0;tmp.x2 = 0;tmp.y1 = 0;lv_coord_t h_tmp = max_row;do {tmp.y2 = h_tmp - 1;disp_refr->driver->rounder_cb(disp_refr->driver, &tmp);/* 如果舍入结果小于max_row,则符合条件,跳出 */if(lv_area_get_height(&tmp) <= max_row) break;/* 减少分块高度,以匹配显示器的舍入规则 */h_tmp--;} while(h_tmp > 0);if(h_tmp <= 0) {LV_LOG_WARN("Can't set draw_buf height using the round function. (Wrong round_cb or to ""small draw_buf)");return;}else {max_row = tmp.y2 + 1;}}/* 在direct模式下,所有缓存将直接绘制到绝对坐标位置上 */if(disp_refr->driver->direct_mode) {draw_buf->area.x1 = 0;draw_buf->area.x2 = lv_disp_get_hor_res(disp_refr) - 1;draw_buf->area.y1 = 0;draw_buf->area.y2 = lv_disp_get_ver_res(disp_refr) - 1;disp_refr->driver->draw_buf->last_part = disp_refr->driver->draw_buf->last_area;lv_refr_area_part(area_p);}else {/* 常规刷新模式下,从给定区域的起始位置开始分块刷新 */lv_coord_t row;lv_coord_t row_last = 0;for(row = area_p->y1; row + max_row - 1 <= y2; row += max_row) {/* 计算下一块的起始和结束行数 */draw_buf->area.x1 = area_p->x1;draw_buf->area.x2 = area_p->x2;draw_buf->area.y1 = row;draw_buf->area.y2 = row + max_row - 1;if(draw_buf->area.y2 > y2) draw_buf->area.y2 = y2;row_last = draw_buf->area.y2;if(y2 == row_last) disp_refr->driver->draw_buf->last_part = 1;  //y2正好时最后一行lv_refr_area_part(area_p);}/* y2是行下限,上面的均匀分块可能会导致最后一次遍历错过一些行,在这里要补上 */if(y2 != row_last) {draw_buf->area.x1 = area_p->x1;draw_buf->area.x2 = area_p->x2;draw_buf->area.y1 = row;draw_buf->area.y2 = y2;  //将y2作为底部disp_refr->driver->draw_buf->last_part = 1;lv_refr_area_part(area_p);}}
}

       在每次分块完成后,都是使用lv_refr_area_part()函数进一步处理,该函数里需要先找到能完全覆盖该无效区域的最上层对象(子对象总是默认放置在父对象的上层),

//lv_refr.c
static void lv_refr_area_part(const lv_area_t * area_p)
{lv_disp_draw_buf_t * draw_buf = lv_disp_get_draw_buf(disp_refr);/* 单一缓存下,当缓存正在刷写到屏幕,需要等待直至缓存释放 */if(draw_buf->buf1 && !draw_buf->buf2) {while(draw_buf->flushing) {  //flushing标志置位表示正在刷写,需要调用lv_disp_flush_ready()进行清除if(disp_refr->driver->wait_cb) disp_refr->driver->wait_cb(disp_refr->driver);}}lv_obj_t * top_act_scr = NULL;lv_obj_t * top_prev_scr = NULL;/* 取无效区域和活动屏幕的交集 */lv_area_t start_mask;_lv_area_intersect(&start_mask, area_p, &draw_buf->area);/* 获取活跃屏幕内,能完全覆盖无效区域的最上层对象(后创建的在上层) */top_act_scr = lv_refr_get_top_obj(&start_mask, lv_disp_get_scr_act(disp_refr));if(disp_refr->prev_scr) {  //寻找上一帧是否存在上述类型的对象top_prev_scr = lv_refr_get_top_obj(&start_mask, disp_refr->prev_scr);}/* 如果不存在能完全覆盖该区域的对象,则先绘制背景 */if(top_act_scr == NULL && top_prev_scr == NULL) {if(disp_refr->bg_fn) {disp_refr->bg_fn(&start_mask);} else if(disp_refr->bg_img) {lv_draw_img_dsc_t dsc;lv_draw_img_dsc_init(&dsc);dsc.opa = disp_refr->bg_opa;lv_img_header_t header;lv_res_t res;res = lv_img_decoder_get_info(disp_refr->bg_img, &header);if(res == LV_RES_OK) {lv_area_t a;lv_area_set(&a, 0, 0, header.w - 1, header.h - 1);lv_draw_img(&a, &start_mask, disp_refr->bg_img, &dsc);}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(&start_mask, &start_mask, &dsc);}}/* 刷新上一帧屏幕,仅动画使用 */if(disp_refr->prev_scr) {/* 获取上一帧该区域未被遮挡的上层对象 */if(top_prev_scr == NULL) {top_prev_scr = disp_refr->prev_scr;}/* 对该对象进行刷新 */lv_refr_obj_and_children(top_prev_scr, &start_mask);}if(top_act_scr == NULL) {top_act_scr = disp_refr->act_scr;}/* 刷新活跃屏幕(act_scr)上对应的对象 */lv_refr_obj_and_children(top_act_scr, &start_mask);/* 无条件地刷新顶层和系统层屏幕 */lv_refr_obj_and_children(lv_disp_get_layer_top(disp_refr), &start_mask);lv_refr_obj_and_children(lv_disp_get_layer_sys(disp_refr), &start_mask);/* 在双缓冲模式下,仅在所有区域重新渲染完毕后,进行一次绘制(flush)* 普通模式下,每个区域的更新都会进行一次绘制 */if(disp_refr->driver->full_refresh == false) {draw_buf_flush();}
}

       层层解剖,最后使用lv_refr_obj_and_children()函数进行更新。该函数除了要刷新上面说的完全覆盖区域的上层对象,还要更新其“弟弟”节点,因为这些“弟弟”们也可以覆盖在该对象上,进一步的,还要更新该对象的父对象的“弟弟”节点,并一直向上寻找直至顶层屏幕结束。如下图,粉色节点为上层对象,橙色节点为同样需要更新的“弟弟”或“叔叔”节点,绿色节点为这些待更新节点的子节点(会被一同刷新)。

//lv_refr.c
static void lv_refr_obj_and_children(lv_obj_t * top_p, const lv_area_t * mask_p)
{/*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_p == NULL) top_p = lv_disp_get_scr_act(disp_refr);if(top_p == NULL) return;  /*Shouldn't happen*//* 刷新该对象及其子对象 */lv_refr_obj(top_p, mask_p);/* 接下来的操作都是刷新该节点的“弟弟”节点(后创建但同属一个父节点) */lv_obj_t * par;lv_obj_t * border_p = top_p;par = lv_obj_get_parent(top_p);/* 寻找“弟弟”节点 */while(par != NULL) {bool go = false;uint32_t i;uint32_t child_cnt = lv_obj_get_child_cnt(par);for(i = 0; i < child_cnt; i++) {lv_obj_t * child = par->spec_attr->children[i];if(!go) {if(child == border_p) go = true;  //标记自身,之后遍历的都是“弟弟”了}else {lv_refr_obj(child, mask_p);}}/* 调用父节点的“后期绘制事件”(DRAW_POST)相关回调函数 */lv_event_send(par, LV_EVENT_DRAW_POST_BEGIN, (void *)mask_p);lv_event_send(par, LV_EVENT_DRAW_POST, (void *)mask_p);lv_event_send(par, LV_EVENT_DRAW_POST_END, (void *)mask_p);/* 对父节点同样执行一遍上述操作,直至没有下一个父节点(顶层屏幕) */border_p = par;par = lv_obj_get_parent(par);}
}

       上面的迭代,每次遍历一个对象,都使用lv_refr_obj()函数来更新其自身及其所有子孙节点。该函数就是更新区域的最后一步了,用于更新传入的节点,并对所有子孙节点做同样的递归更新操作。

//lv_refr.c
static void lv_refr_obj(lv_obj_t * obj, const lv_area_t * mask_ori_p)
{/* 不刷新隐藏的节点对象 */if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return;bool union_ok; /* 传入对象的坐标区域和无效区域的子集 */lv_area_t obj_mask;lv_area_t obj_ext_mask;lv_area_t obj_area;lv_coord_t ext_size = _lv_obj_get_ext_draw_size(obj);  //获取绘制阶段的额外范围lv_obj_get_coords(obj, &obj_area);  //获取对象坐标范围obj_area.x1 -= ext_size;obj_area.y1 -= ext_size;obj_area.x2 += ext_size;obj_area.y2 += ext_size;union_ok = _lv_area_intersect(&obj_ext_mask, mask_ori_p, &obj_area);/* 仅当对象主绘制区域和传入的无效区域存在交集时才进行绘制 */if(union_ok != false) {/* 主绘制阶段 */lv_event_send(obj, LV_EVENT_DRAW_MAIN_BEGIN, &obj_ext_mask);lv_event_send(obj, LV_EVENT_DRAW_MAIN, &obj_ext_mask);lv_event_send(obj, LV_EVENT_DRAW_MAIN_END, &obj_ext_mask);/* 去掉主绘制阶段的额外范围,进行子节点绘制(子节点无法看到这些范围) */lv_obj_get_coords(obj, &obj_area);union_ok = _lv_area_intersect(&obj_mask, mask_ori_p, &obj_area);if(union_ok != false) {lv_area_t mask_child; /*Mask from obj and its child*/lv_area_t child_area;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];lv_obj_get_coords(child, &child_area);ext_size = _lv_obj_get_ext_draw_size(child);child_area.x1 -= ext_size;child_area.y1 -= ext_size;child_area.x2 += ext_size;child_area.y2 += ext_size;/* 获取子节点和无效区域的交集 */union_ok = _lv_area_intersect(&mask_child, &obj_mask, &child_area);/* 如果有交集则递归调用该函数刷新子节点 */if(union_ok) {/* 刷新下一个子节点 */lv_refr_obj(child, &mask_child);}}}/* 当所有子节点都绘制完毕,进入“后期绘制”阶段 */lv_event_send(obj, LV_EVENT_DRAW_POST_BEGIN, &obj_ext_mask);lv_event_send(obj, LV_EVENT_DRAW_POST, &obj_ext_mask);lv_event_send(obj, LV_EVENT_DRAW_POST_END, &obj_ext_mask);}
}

       到这里才发现,最终遍历到具体的对象和区域后,会发送DRAW相关事件给该对象,而更新的关键就在于DRAW事件的相关回调函数。

       到这里回过头捋一下更新任务内部实现的调用路径:

--> _lv_disp_refr_timer(tmr),更新任务主体

     --> lv_obj_update_layout(screen),布局更新,将脏数据进一步转化成无效区域

     --> lv_refr_join_area(),合并重叠的无效区域

      --> lv_refr_areas(),遍历无效区域进行更新

            --> lv_refr_area(area),将每一片无效区域进一步分块

                  --> lv_refr_part(area),找到分块后,完全覆盖该块的最顶层元素进行遍历

                       --> lv_refr_obj_and_children(top_obj, area),遍历最顶层元素及其“弟弟”节点们

                            --> lv_refr_obj(top_obj, area),向需要更新的对象发送draw相关事件

     --> draw_buf_flush(),将缓存刷写到显示器

       下一片文章,将分析draw事件回调函数如何重绘对象并最终刷写缓冲。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/80102.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

01 dnsmasq 中 dns服务

前言 这里我们主要是 来看一下 dns 服务器这边的相关业务处理 通常来说 在我们日常生活中 还是经常会需要使用 dns 的情况, 主要是更加友好的去给一个主机命名一个别名 比如 现在我的应用在服务器 192.168.220.133 但是我不想记这个生硬的 ip, 我可能更期望记录一个域名, …

最优化方法Python计算:有约束优化应用——线性Lasso回归分类器

利用线性Lasso模型类LineLassoModel类&#xff08;见博文《最优化方法Python计算&#xff1a;有约束优化应用——线性Lasso回归预测器》&#xff09;及分类器类Classification&#xff08;见博文《最优化方法Python计算&#xff1a;无约束优化应用——线性回归分类器》&#xf…

Python基础学习-Day20

目录 奇异值分解&#xff08;SVD&#xff09;的输入和输出奇异值的应用实际案例1. 问题分析2. 解决方案&#xff1a;对测试集应用相同的变换3. 为什么不能对测试集单独做 SVD&#xff1f;4. 代码示例&#xff1a;训练集和测试集的 SVD 降维6. 实际操作中的注意事项 奇异值分解&…

2025年 全新 AI 编程工具 Cursor 安装使用教程

一、Cursor 软件下载 首选&#xff0c;登录Cursor官网&#xff0c;进行软件下载&#xff0c;官网下载地址如下&#xff1a; Cursor AI IDE 下载 二、Cursor软件安装配置 此处以Windows10系统安装为例&#xff0c;下载完成之后&#xff0c;右键安装包&#xff0c;以管理员身份…

[vue]error:0308010C:digital envelope routines::unsupported

npm run dev 报错&#xff1a; \node_modules\webpack\hot\dev-server.jsnode:internal/crypto/hash:71 this[kHandle] new _Hash(algorithm, xofLen); Error: error:0308010C:digital envelope routines::unsupported opensslErrorStack: [ error:03000086:digital env…

开放的力量:新零售生态的共赢密码

当某头部生鲜平台向供应商开放销售预测系统后&#xff0c;合作伙伴的库存周转率竟提升12%——这个反常识的案例&#xff0c;正在重塑商业竞争的底层逻辑。 生态共建三板斧 ▌模块化设计&#xff1a;像搭积木一样开放 • 乐高式API架构&#xff1a;30%接口支持自由组合&#xff…

深入理解Spring缓存注解:@Cacheable与@CacheEvict

在现代应用程序开发中&#xff0c;缓存是提升系统性能的重要手段。Spring框架提供了一套简洁而强大的缓存抽象&#xff0c;其中Cacheable和CacheEvict是两个最常用的注解。本文将深入探讨这两个注解的工作原理、使用场景以及最佳实践。 1. Cacheable注解 基本概念 Cacheable…

[python] 函数3-python内置函数

一 内置函数 导入:import builtins 1.1 查看内置函数 大写字母开头的一般是内置变量小写的一般是内置函数 import builtins print(dir(builtins)) 1.2 abs() 求绝对值 print(abs(-10)) 1.3 sum()求和 不能直接用纯数字,因为不是可迭代对象 运算时只要一个是浮点数,结果就…

QT异步线程通信

在使用 QThreadPool 提交任务后&#xff0c;如果你需要知道任务何时完成&#xff0c;并且需要使用任务的执行结果&#xff0c;可以通过以下几种方式来实现&#xff1a; 1. 使用信号和槽 QRunnable 提供了一个 finished() 信号&#xff0c;当任务执行完成后会发出。你可以在任…

利用并行处理提高LabVIEW程序执行速度

在 LabVIEW 编程中&#xff0c;提升程序执行速度是优化系统性能的关键&#xff0c;而并行处理技术则是实现这一目标的有力武器。通过合理运用并行处理&#xff0c;不仅能加快程序运行&#xff0c;还能增强系统的稳定性和响应能力。下面将结合实际案例&#xff0c;深入探讨如何利…

机器学习第三讲:监督学习 → 带答案的学习册,如预测房价时需要历史价格数据

机器学习第三讲&#xff1a;监督学习 → 带答案的学习册&#xff0c;如预测房价时需要历史价格数据 资料取自《零基础学机器学习》。 查看总目录&#xff1a;学习大纲 关于DeepSeek本地部署指南可以看下我之前写的文章&#xff1a;DeepSeek R1本地与线上满血版部署&#xff1…

Open CASCADE学习|实现裁剪操作

1. 引言 Open CASCADE (简称OCC) 是一个功能强大的开源几何建模内核&#xff0c;广泛应用于CAD/CAM/CAE领域。裁剪操作作为几何建模中的基础功能&#xff0c;在模型编辑、布尔运算、几何分析等方面有着重要作用。本文将全面探讨Open CASCADE中的裁剪操作实现原理、应用场景及具…

【redis】分片方案

Redis分片&#xff08;Sharding&#xff09;是解决单机性能瓶颈的核心技术&#xff0c;其本质是将数据分散存储到多个Redis节点&#xff08;实例&#xff09;中&#xff0c;每个实例将只是所有键的一个子集&#xff0c;通过水平扩展提升系统容量和性能。 分片的核心价值 性能提…

RGB矩阵照明系统详解及WS2812配置指南

RGB矩阵照明系统详解及WS2812配置指南 一、RGB矩阵照明简介 RGB矩阵照明是一种强大的功能&#xff0c;允许使用外部驱动器驱动的RGB LED矩阵为键盘增添绚丽的灯光效果。该系统与RGBLIGHT功能无缝集成&#xff0c;因此您可以使用与RGBLIGHT相同的键码来控制它&#xff0c;操作…

[250509] x-cmd 发布 v0.5.11 beta:x ping 优化、AI 模型新增支持和语言变量调整

目录 X-CMD 发布 v0.5.11 beta&#x1f4c3;Changelog&#x1f9e9; ping&#x1f9e9; openai&#x1f9e9; gemini&#x1f9e9; asdf&#x1f9e9; mac✅ 升级指南 X-CMD 发布 v0.5.11 beta &#x1f4c3;Changelog &#x1f9e9; ping 调整 x ping 默认参数为 bing.com&a…

嵌入式开发学习日志Day17

第十一章 结构体与共用体 一、结构体 1、结构体 一般形式 【struct 标识符】 结构体中的标识符一般首字母大写&#xff1b; 【.】结构体成员运算符&#xff1b; 优先级 1 级 结合方向&#xff1a;从左至右&#xff1b; 【->】:指向结构体成员运算符&#x…

发那科机器人5(异常事件和程序备份加载+ROBOGUIDE离线仿真)

发那科机器人5(异常事件和程序备份加载+ROBOGUIDE离线仿真) 一,异常事件和程序备份加载1,常见异常事件2,零点复归介绍3,程序备份-加载(未整理)二,`ROBOGUIDE`离线仿真1,仿真软件简介及安装步骤(未整理)2,机器人==导入与工具==与==工件添加==2.1,机器人导入(未整…

青少年编程与数学 02-019 Rust 编程基础 01课题、环境准备

青少年编程与数学 02-019 Rust 编程基础 01课题、环境准备 一、Rust核心特性应用场景开发工具社区与生态 二、Rust 和 Python 比较1. **内存安全与并发编程**2. **性能**3. **零成本抽象**4. **跨平台支持**5. **社区与生态系统**6. **错误处理**7. **安全性**适用场景总结 三、…

Java反射 八股版

目录 一、核心概念阐释 1. Class类 2. Constructor类 3. Method类 4. Field类 二、典型应用场景 1. 框架开发 2. 单元测试 3. JSON序列化/反序列化 三、性能考量 四、安全与访问控制 1. 安全管理器限制 2. 打破封装性 3. 安全风险 五、版本兼容性问题 六、最佳…

操作系统的初步了解

目录 引言&#xff1a;什么是操作系统&#xff1f; 一、设计操作系统的目的 二、操作系统是做什么的&#xff1a; 操作系统主要有四大核心任务&#xff1a; 1. 管理硬件 2. 运行软件 3. 存储数据 4. 提供用户界面 如何理解操作系统的管理呢&#xff1f; 1. 什么是操作…