嵌入式Linux——LVGL从段错误到 lv_async_call:LVGL后台线程通知UI的正确姿势
LVGL多线程指南:为什么应使用 lv_async_call (而非事件) 从后台线程通知UI
- 当我有一件特别耗时的事情不想在GUI线程里面去做时,我可以开启一个新的线程去处理,但是处理完如何通知GUI线程呢?
- 一开始想到的是事件通知,每次处理完就用
lv_obj_send_event通知屏幕对象有一个事件要处理,一开始使用没有问题,但是随着新的业务对应新的线程的增多,就有bug,偶尔是段错误,偶尔是这个通知开始莫名其妙通知到另外一个事件的处理逻辑去了,导致逻辑完全错误。那么这是为什么? - 解决方案:lvgl v9有一个函数是
lv_async_call,这个是LVGL官方提供的、专门为“从后台线程安全地通知UI线程”而设计的函数。
原因
核心概念:线程安全 与 GUI 的“单线程”规则
-
为了理解这一切,我们必须先建立一个核心概念:图形用户界面(GUI)库,比如LVGL,通常不是“线程安全”的,并且遵循“单线程”规则。
-
我们可以用一个“只有一个画家的画室”的比喻来理解:
-
主UI线程 (运行
lv_timer_handler的那个while(1)循环):是画室里唯一的那位画家。他负责所有的绘画工作(更新屏幕)、处理观众的指令(响应点击、滑动)、调整画架(管理UI对象)等。 -
后台线程 (比如
xxx_fetch_thread):是画室外的一位助手。他可以帮忙跑腿、取颜料(执行耗时操作如网络请求),但他绝对不能直接拿起画笔去修改画布上的内容。
-
-
为什么不能有两个画家? 想象一下,如果画家正在画布左边画一朵红花,同时助手突然跑过来,在画布右边画了一棵绿树。这会造成混乱:
-
数据竞争 (Race Condition):两人可能同时去抢同一支画笔或颜料(访问同一个内存变量),导致颜料洒了或者画笔断了(数据损坏)。
-
状态不一致:画家可能刚画完花蕊,助手就把整朵花擦掉了,导致最终画面残缺不全(UI显示异常)。
-
崩溃:更严重的情况是,两人操作冲突导致画架倒塌(程序崩溃)。
-
-
所以,GUI库通常规定:所有对UI元素的直接操作(创建、删除、修改位置、颜色、文本等),都必须由主UI线程(那位唯一的画家)来完成。
lv_obj_send_event 为什么失败?(助手抢画笔)
-
lv_obj_send_event(lv_scr_act(), LV_EVENT_READY, &loc_info);这个函数,虽然看起来只是“发送一个事件”,但它在LVGL内部的实现,很可能涉及到直接去修改与lv_scr_act()这个对象相关联的事件列表或其他内部数据结构。 -
发生了什么?:
-
助手(后台线程)在画室外完成了取颜料的任务。
-
他没有把颜料递给画家,而是直接冲进画室,试图在画家的**工作日志(事件列表)**上写下一条记录:“颜料已取回!”
-
冲突发生:此时,画家(主UI线程)可能正在全神贯注地绘画(处理其他UI事件或刷新屏幕)。助手的突然闯入和涂改,打断了画家的工作,或者把日志弄得一团糟。
-
结果:画家可能根本没注意到助手写了什么(事件丢失),或者因为被打扰而出错(潜在的崩溃风险)。
-
lv_async_call 为什么成功?(助手把颜料放在门口的“收件箱”)
-
lv_async_call(update_ui_with_location, loc_info);这个函数的设计,完美地遵守了“单线程”规则。它就像一个线程安全的“收件箱”。 -
发生了什么?:
-
助手(后台线程)在画室外完成了取颜料的任务。
-
他走到画室门口,把颜料(
loc_info数据)和一个便签(update_ui_with_location函数指针,上面写着“请用这个颜料更新画面”)一起放进了门口挂着的**“收件箱”** (lv_async_call内部维护的一个线程安全的队列)里。 -
助手完成投递,直接下班离开。他自始至终没有进入画室打扰画家。
-
-
画家的工作流程:
-
画家(主UI线程)一直在自己的画室里工作(执行
lv_timer_handler())。 -
他每完成一小段绘画任务,就会习惯性地去检查一下门口的“收件箱”。
-
当他发现收件箱里有助手留下的便签和颜料时,他会安全地把它们取出来。
-
然后,他亲自拿起颜料,按照便签上的指示(执行
update_ui_with_location函数),去更新画布上的内容(lv_label_set_text等)。 -
结果:所有对UI的修改,都是由画家本人、在他自己的工作节奏中完成的,绝对不会发生冲突。消息传递既安全又可靠。
-
#include "ui_LocationPage.h"
#include <string.h>
#include <pthread.h>
#include "../../../../common/sys_manager/sys_manager.h"
static lv_obj_t *page_obj = NULL;
static lv_obj_t *manual_set_cont; // 手动设置容器
static lv_obj_t *adcode_textarea; // adcode 输入框
static lv_obj_t *kb; // 数字键盘
static lv_obj_t *sw_label;
/**
* @brief 这是一个将在“主UI线程”中被安全执行的函数
* @param data 从后台线程传递过来的数据指针 (这里是 location_info_t*)
*/
static void update_ui_with_location(void *data)
{
if (data) { // data 不为 NULL,表示成功
location_info_t* info = (location_info_t*)data;
LV_LOG_USER("IP Location Successful! City: %s, Adcode: %s", info->city, info->adcode);
// 更新全局配置
strncpy(g_system_config.city, info->city, sizeof(g_system_config.city) - 1);
strncpy(g_system_config.adcode, info->adcode, sizeof(g_system_config.adcode) - 1);
// 更新UI显示
lv_textarea_set_text(adcode_textarea, info->adcode);
// 创建格式化字符串
char label_text[64]; // 确保缓冲区足够大
snprintf(label_text, sizeof(label_text), "IP Locating (Synced:%s)", info->adcode);
lv_label_set_text(sw_label, label_text);
// !!!重要!!! 释放由后台线程分配的内存
free(info);
} else { // data 为 NULL,表示失败
LV_LOG_WARN("IP Location Failed!");
lv_label_set_text(sw_label, "IP Locating (Failed)");
}
}
// --- 后台线程 ---
static void* location_fetch_thread(void* arg) {
// 【核心修改】我们在这里动态分配内存,以传递给UI线程
location_info_t* loc_info = (location_info_t*)malloc(sizeof(location_info_t));
if (loc_info && sys_manager_get_location_by_ip(loc_info) == 0) {
// 成功:调用 lv_async_call,将“更新UI”的任务和“数据”放入投递箱
lv_async_call(update_ui_with_location, loc_info);
} else {
// 失败:调用 lv_async_call,但传递一个 NULL 数据
lv_async_call(update_ui_with_location, NULL);
if(loc_info) free(loc_info); // 如果分配了内存但失败了,要释放
}
return NULL;
}
/**
* @brief 键盘自身的事件回调,用于处理“确定”和“关闭”
*/
static void keyboard_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
if(code == LV_EVENT_READY || code == LV_EVENT_CANCEL) {
// 当按下“确定”或“关闭”时
lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); // 隐藏键盘
lv_keyboard_set_textarea(kb, NULL); // 解除与文本框的绑定
}
}
/**
* @brief 文本框的事件回调,用于在点击时弹出键盘
*/
static void textarea_event_cb(lv_event_t * e)
{
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t * ta = lv_event_get_target(e); // 获取事件源,即文本框
if(code == LV_EVENT_FOCUSED) {
// 当文本框获得焦点时
if(kb == NULL) return;
lv_keyboard_set_textarea(kb, ta); // 将键盘与此文本框“配对”
lv_obj_clear_flag(kb, LV_OBJ_FLAG_HIDDEN); // 显示键盘
}
else if(code == LV_EVENT_DEFOCUSED) {
// 当文本框失去焦点时(比如用户点击了其他地方)
if(kb == NULL) return;
lv_keyboard_set_textarea(kb, NULL); // 解除绑定
lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); // 隐藏键盘
}
}
// --- 事件回调 ---
static void back_btn_event_cb(lv_event_t * e) {
// 在返回前,保存所有设置
// 比如,从输入框中读取用户输入的adcode并存入全局配置
strncpy(g_system_config.adcode, lv_textarea_get_text(adcode_textarea), sizeof(g_system_config.adcode) - 1);
sys_manager_save_config("utils/app_conf.ini");
lv_lib_pm_OpenPrePage(&page_manager);
}
static void switch_event_cb(lv_event_t * e) {
lv_obj_t * sw = lv_event_get_target(e);
if (lv_obj_has_state(sw, LV_STATE_CHECKED)) {
g_system_config.auto_location = true;
lv_obj_add_flag(manual_set_cont, LV_OBJ_FLAG_HIDDEN);
// 派发一个后台定位任务
lv_label_set_text(sw_label, "IP Locating (Syncing...)");
pthread_t tid;
pthread_create(&tid, NULL, location_fetch_thread, NULL);
pthread_detach(tid); // 将线程设置为分离状态,线程结束后自动回收资源,避免主线程去 joi
} else {
g_system_config.auto_location = false;
lv_obj_clear_flag(manual_set_cont, LV_OBJ_FLAG_HIDDEN);
lv_label_set_text(sw_label, "Auto IP Locating");
}
}
// --- UI初始化 ---
void ui_LocationPage_init(void) {
// 1. 创建页面的根容器,并设置垂直Flex布局
page_obj = lv_obj_create(lv_scr_act());
lv_obj_remove_style_all(page_obj);
lv_obj_set_size(page_obj, lv_pct(100), lv_pct(100));
lv_obj_set_layout(page_obj, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(page_obj, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(page_obj, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_row(page_obj, 20, 0);
lv_obj_set_style_pad_hor(page_obj, 20, 0);
// 2. 创建“IP Locating”行 (水平Flex子容器)
lv_obj_t * auto_loc_cont = lv_obj_create(page_obj);
lv_obj_remove_style_all(auto_loc_cont);
lv_obj_set_size(auto_loc_cont, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_layout(auto_loc_cont, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(auto_loc_cont, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(auto_loc_cont, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
sw_label = lv_label_create(auto_loc_cont);
lv_label_set_text(sw_label, "Auto IP Locating");
lv_obj_t * sw = lv_switch_create(auto_loc_cont);
lv_obj_add_event_cb(sw, switch_event_cb, LV_EVENT_VALUE_CHANGED, NULL);
if(g_system_config.auto_location) lv_obj_add_state(sw, LV_STATE_CHECKED);
// 3. 创建“手动设置 adcode”容器
manual_set_cont = lv_obj_create(page_obj);
lv_obj_remove_style_all(manual_set_cont);
lv_obj_set_size(manual_set_cont, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_layout(manual_set_cont, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(manual_set_cont, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(manual_set_cont, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(manual_set_cont, 10, 0); // 设置标签和输入框的间距
lv_obj_t* manual_label = lv_label_create(manual_set_cont);
lv_label_set_text(manual_label, "Adcode Set:");
adcode_textarea = lv_textarea_create(manual_set_cont);
lv_textarea_set_one_line(adcode_textarea, true); // 设置为单行输入
lv_textarea_set_placeholder_text(adcode_textarea, "e.g. 440100"); // 设置占位符提示
lv_obj_set_width(adcode_textarea, LV_PCT(60)); // 设置宽度
lv_textarea_set_text(adcode_textarea, g_system_config.adcode);
lv_obj_add_event_cb(adcode_textarea, textarea_event_cb, LV_EVENT_ALL, NULL); // 唤醒键盘
if(g_system_config.auto_location) lv_obj_add_flag(manual_set_cont, LV_OBJ_FLAG_HIDDEN);
// 创建返回按钮
lv_obj_t *btn = lv_btn_create(page_obj);
lv_obj_align(btn, LV_ALIGN_BOTTOM_LEFT, 20, -20);
lv_obj_add_event_cb(btn, back_btn_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *btn_label = lv_label_create(btn);
lv_label_set_text(btn_label, "Back");
lv_obj_center(btn_label);
// 创建键盘对象,父对象是屏幕,这样它能覆盖在所有内容之上
kb = lv_keyboard_create(lv_scr_act());
lv_keyboard_set_mode(kb, LV_KEYBOARD_MODE_NUMBER); // 设置为纯数字模式
lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN);
lv_obj_add_event_cb(kb, keyboard_event_cb, LV_EVENT_READY, NULL);
lv_obj_add_event_cb(kb, keyboard_event_cb, LV_EVENT_CANCEL, NULL);
}
void ui_LocationPage_deinit(void) {
// lv_obj_remove_event_cb(lv_scr_act(), location_result_event_cb);
if(page_obj) lv_obj_del(page_obj);
}
更多推荐



所有评论(0)