3. 事件数据结构设计:结构体定义、优先级与参数传递

事件数据结构,说白了就是整个GUI系统的“信使”。信使长什么样、跑多快、能带多少东西,直接决定了系统能不能高效运转。我在做第一个嵌入式GUI项目时,就因为事件结构设计得太随意,后来改得头皮发麻——嗯,今天咱们就把这块彻底聊透。

3.1 事件结构体定义——骨架要稳

一个事件结构体,至少得包含三样东西:事件类型、事件来源、事件参数。我个人习惯把这三样放在最前面,后面再根据需求扩展。

先看一个最基础的定义:

typedef struct {
    uint16_t type;       // 事件类型,比如按键按下、触摸抬起
    uint16_t source;     // 事件来源,比如哪个按键、哪个触摸区域
    uint32_t timestamp;  // 事件发生的时间戳
    void *data;          // 指向具体参数数据的指针
    uint8_t data_len;    // 参数数据长度
} Event_t;

你可能会问:为什么不用联合体?其实联合体也行,但我更推荐指针+长度这种模式。原因很简单——灵活。不同类型的参数大小不一样,用指针可以动态分配,不浪费内存。

核心原则:事件结构体要足够通用,但又不能太臃肿。通用性靠指针实现,紧凑性靠固定头部保证。

我在项目中遇到过一种情况:有人把事件结构体定义得特别大,里面塞了各种类型的参数联合体。结果每个事件都占几十个字节,小内存MCU直接扛不住。后来改成指针传递,内存占用降了60%。

3.2 事件优先级——谁先谁后

事件优先级,说白了就是决定“谁插队”。嵌入式系统里,有些事件必须立刻处理,比如触摸中断;有些可以稍后再说,比如定时器超时。

我一般把优先级分成三级:

优先级等级 数值 典型事件 处理要求
高优先级 0 触摸按下、按键中断、紧急报警 立即处理,不可阻塞
中优先级 1 触摸抬起、滑动事件、定时器超时 尽快处理,可排队
低优先级 2 重绘请求、状态更新、日志输出 空闲时处理,可丢弃

优先级怎么用?我建议在事件结构体里加一个字段:

typedef struct {
    uint16_t type;
    uint16_t source;
    uint8_t  priority;   // 0=高, 1=中, 2=低
    uint32_t timestamp;
    void    *data;
    uint8_t  data_len;
} Event_t;

然后事件队列按优先级排序。高优先级事件永远插到队列最前面。中优先级按时间顺序排。低优先级嘛……如果队列满了,直接丢弃。

注意:优先级不能滥用。我曾经见过一个项目,所有事件都设成高优先级,结果队列形同虚设,系统响应反而变慢了。优先级是稀缺资源,只在真正需要“插队”的时候才用。

3.3 事件参数传递——怎么带东西

事件参数传递,是事件系统里最容易出bug的地方。我总结了几种常见方式:

3.3.1 直接值传递

适合小数据,比如一个坐标、一个按键码:

typedef struct {
    uint16_t type;
    uint16_t source;
    uint8_t  priority;
    uint32_t timestamp;
    union {
        uint16_t key_code;      // 按键码
        struct {
            int16_t x;
            int16_t y;
        } point;                // 触摸坐标
        uint32_t timer_id;      // 定时器ID
    } param;
} Event_t;

这种方式快,但扩展性差。参数大小固定,想加新类型就得改联合体。

3.3.2 指针传递

适合大数据,比如一个字符串、一个结构体:

typedef struct {
    uint16_t type;
    uint16_t source;
    uint8_t  priority;
    uint32_t timestamp;
    void    *data;       // 指向堆或静态区的参数
    uint8_t  data_len;   // 参数长度
} Event_t;

这种方式灵活,但要注意内存管理。谁分配谁释放?我建议:事件发送者分配,事件处理者释放。如果处理者不释放,就由事件队列在销毁事件时统一释放。

小技巧:在低内存系统里,可以用静态缓冲区+内存池来管理事件参数。我习惯预分配一个固定大小的参数池,事件来了从池里取一块,处理完再还回去。这样既避免了动态分配的开销,又不会产生内存碎片。

3.3.3 回调参数传递

有时候事件本身不需要参数,但处理函数需要上下文。这时候可以用回调参数:

typedef struct {
    uint16_t type;
    uint16_t source;
    uint8_t  priority;
    uint32_t timestamp;
    void    *context;    // 回调上下文指针
    void   (*handler)(void *context);  // 处理函数指针
} Event_t;

这种方式在事件驱动框架里很常见。事件队列只管调度,具体怎么处理由回调函数决定。

3.4 完整的事件数据结构设计

综合以上讨论,我给出一个比较实用的设计:

/* 事件类型定义 */
typedef enum {
    EVT_NONE = 0,
    EVT_KEY_DOWN,
    EVT_KEY_UP,
    EVT_TOUCH_DOWN,
    EVT_TOUCH_UP,
    EVT_TOUCH_MOVE,
    EVT_TIMER,
    EVT_PAINT,
    EVT_USER_START = 0x100   // 用户自定义事件起始
} EventType_t;

/* 事件优先级 */
typedef enum {
    PRIO_HIGH = 0,
    PRIO_MID  = 1,
    PRIO_LOW  = 2
} EventPriority_t;

/* 事件参数联合体 */
typedef union {
    uint16_t key_code;
    struct { int16_t x; int16_t y; } point;
    struct { int16_t x; int16_t y; int16_t dx; int16_t dy; } move;
    uint32_t timer_id;
    void    *user_data;
} EventParam_t;

/* 事件结构体 */
typedef struct {
    EventType_t     type;
    uint16_t        source;
    EventPriority_t priority;
    uint32_t        timestamp;
    EventParam_t    param;
    uint8_t         param_valid;  // 参数是否有效
} Event_t;

这个设计兼顾了效率和灵活性。对于常见的小参数,直接用联合体;对于大数据,用user_data指针。你想想看,是不是既简单又实用?

3.5 事件数据结构关系图

下面这张图展示了事件数据结构各要素之间的关系:

事件数据结构关系图 事件数据结构关系图 Event_t 事件结构体 type + source priority timestamp param (联合体/指针) 直接值(小数据) 指针(大数据) 回调+上下文 设计要点 • 固定头部 + 灵活参数,兼顾效率和扩展性 • 优先级只设三级,避免过度设计

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 参数生命周期管理:我曾经在事件处理函数里释放了参数内存,但事件队列还在用,结果野指针崩溃。后来统一规定:事件队列负责释放参数,处理函数只管读。
  • 优先级反转:低优先级事件持有某个资源,高优先级事件等这个资源,结果中优先级事件插队执行,高优先级反而被阻塞。解决方案:事件处理函数里不要持有锁,或者用优先级继承协议。
  • 事件丢失:队列满了,低优先级事件直接被丢弃。如果这个事件是“保存数据”,那数据就丢了。我建议:关键事件(比如保存、报警)即使优先级低,也要保证队列有足够空间。

嗯,事件数据结构设计就聊到这儿。记住一句话:结构体是骨架,优先级是规则,参数是血肉。三者配合好了,事件系统才能跑得稳、跑得快。