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 事件数据结构关系图
下面这张图展示了事件数据结构各要素之间的关系:
3.6 避坑指南
最后,分享几个我踩过的坑:
- 参数生命周期管理:我曾经在事件处理函数里释放了参数内存,但事件队列还在用,结果野指针崩溃。后来统一规定:事件队列负责释放参数,处理函数只管读。
- 优先级反转:低优先级事件持有某个资源,高优先级事件等这个资源,结果中优先级事件插队执行,高优先级反而被阻塞。解决方案:事件处理函数里不要持有锁,或者用优先级继承协议。
- 事件丢失:队列满了,低优先级事件直接被丢弃。如果这个事件是“保存数据”,那数据就丢了。我建议:关键事件(比如保存、报警)即使优先级低,也要保证队列有足够空间。
嗯,事件数据结构设计就聊到这儿。记住一句话:结构体是骨架,优先级是规则,参数是血肉。三者配合好了,事件系统才能跑得稳、跑得快。