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);
}
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐