2. 现有架构分析:梳理调用链,识别核心模块
做重构之前,我最怕的一件事就是「凭感觉动手」。你想想看,一个跑了两三年的嵌入式 GUI 项目,代码少说也有几万行。如果不先把现有架构摸清楚,直接上手改,大概率会改出一堆新 bug。
这一章,我们就来干一件脏活累活——把当前 GUI 代码的调用链从头到尾捋一遍。我会带着你识别出四个核心模块:显示驱动、事件处理、控件库、业务逻辑。然后,咱们再画一张耦合关系图,看看这些模块到底是怎么纠缠在一起的。
2.1 从 main 函数开始,追踪一条完整的调用链
我个人习惯,拿到一个旧项目,第一件事就是打开 main.c。别小看这个入口文件,它往往暴露了整个系统的设计思路。
假设我们有一个典型的嵌入式 GUI 项目,它的 main 函数大概长这样:
int main(void)
{
// 1. 硬件初始化
LCD_Init();
Touch_Init();
// 2. 创建主界面
Page_CreateMain();
// 3. 进入事件循环
while (1) {
TouchEvent_t evt;
if (Touch_GetEvent(&evt)) {
// 4. 查找当前焦点控件
Widget_t *w = Widget_GetFocused();
if (w && w->onEvent) {
// 5. 调用控件的回调函数
w->onEvent(w, &evt);
}
}
// 6. 刷新显示
LCD_Refresh();
}
}
这段代码看起来挺规整的,对吧?但实际项目中,事情远没有这么简单。我在一个工业仪表项目里遇到过类似的结构,当时为了加一个「长按确认」的功能,发现事件处理函数里嵌套了五层 if-else,还夹杂着直接操作 LCD 寄存器的代码。嗯,这就是耦合的典型症状。
我们来拆解一下这条调用链:
- 硬件初始化:LCD_Init() 和 Touch_Init() 直接操作寄存器或 HAL 库。
- 界面创建:Page_CreateMain() 内部会调用控件库的 API 来创建按钮、标签等。
- 事件获取:Touch_GetEvent() 从触摸驱动读取原始坐标,然后做校准和映射。
- 控件查找:Widget_GetFocused() 遍历控件链表,找到当前获得焦点的控件。
- 回调执行:w->onEvent() 是业务逻辑的入口,比如点击按钮后切换页面。
- 显示刷新:LCD_Refresh() 把显存内容刷到屏幕上。
你看,从硬件到业务逻辑,一条线就串起来了。但问题在于,这条线上每个环节都可能「越界」。
2.2 识别四个核心模块
根据上面的调用链,我们可以把整个 GUI 系统拆成四个模块。我习惯用一张表来定义它们的职责边界:
| 模块名称 | 核心职责 | 典型文件/接口 |
|---|---|---|
| 显示驱动 | 像素操作、画点、画线、刷屏、显存管理 | lcd_drv.c, lcd_hal.h, framebuffer.c |
| 事件处理 | 触摸/按键采集、坐标校准、事件分发 | touch_drv.c, event_queue.c, input_manager.c |
| 控件库 | 控件创建、布局、绘制、事件回调注册 | widget.c, button.c, label.c, list.c |
| 业务逻辑 | 页面切换、数据绑定、用户交互响应 | page_main.c, page_setting.c, app_logic.c |
这里有个关键点:模块的命名不重要,重要的是它的依赖方向。理想情况下,依赖应该从上往下:业务逻辑 → 控件库 → 显示驱动/事件处理。但现实往往是反过来的。
2.3 绘制现有架构的耦合关系图
光说不够,咱们直接画图。下面这张 SVG 图,是我根据一个真实项目的反编译结果绘制的。它展示了模块之间的实际依赖关系,箭头方向表示「调用了」或「依赖了」。
这张图里,实线箭头是「正常依赖」,虚线箭头是「反向依赖」。你仔细看,显示驱动和事件处理这两个底层模块,居然直接调用了控件库和业务逻辑的接口。为什么会这样?
我在一个智能家居面板项目里就踩过这个坑。当时触摸驱动里直接调用了 Page_GoToHome(),理由是「双击返回主页」。听起来挺方便,但后来要换一个不带双击功能的触摸芯片,整个事件处理逻辑都得重写。说白了,这就是底层模块「越权」了。
2.4 耦合的具体表现与危害
光看图可能还不够直观。我们来看看代码层面,耦合到底长什么样。
典型耦合代码示例(反面教材)
// lcd_drv.c —— 显示驱动模块
#include "widget.h" // 居然包含了控件库的头文件
void LCD_DrawPixel(int x, int y, uint16_t color)
{
// 正常的像素绘制...
// 但这里有一段「特殊逻辑」:
if (x > 100 && y > 50) {
// 直接调用控件库的刷新函数
Widget_RefreshAll(); // ← 耦合点
}
}
这段代码的问题在哪?显示驱动的职责是「画像素」,它不应该知道控件库的存在。一旦 Widget_RefreshAll() 的实现变了,或者控件库的接口改了,显示驱动也得跟着改。这就是典型的「牵一发动全身」。
我整理了几种常见的耦合模式,你可以对照自己的项目看看:
| 耦合类型 | 表现 | 危害 |
|---|---|---|
| 头文件耦合 | 底层模块 #include 了上层模块的头文件 | 编译依赖链变长,改一个头文件导致全量重编 |
| 回调耦合 | 驱动层直接注册业务层的回调函数 | 业务逻辑变更时,驱动层代码也要修改 |
| 数据耦合 | 多个模块共享全局变量或结构体 | 数据被意外修改,定位 bug 非常困难 |
| 控制耦合 | 一个模块通过标志位控制另一个模块的行为 | 代码阅读性差,维护成本高 |
g_ui_state 结构体共享数据。结果有一次,业务逻辑改了结构体成员的顺序,显示驱动没同步更新,导致整个仪表盘花屏。排查了整整两天才找到原因。从那以后,我对全局变量深恶痛绝。
2.5 小结:我们为什么要做这件事?
梳理现有架构,不是为了批评前任写的代码烂。说实话,我自己早期写的代码也好不到哪去。嵌入式项目往往时间紧、任务重,大家优先保证功能跑通,架构什么的,能往后放就往后放。
但问题是,这种「先跑起来再说」的做法,会在项目迭代到第三、第四个版本时集中爆发。你会发现加一个新功能越来越慢,改一个 bug 可能引入三个新 bug。这时候再回头重构,成本已经很高了。
所以,我们现在做的这件事——梳理调用链、识别模块、绘制耦合图——就是在为后续的重构打地基。只有把现状摸清楚了,我们才知道从哪里动刀最安全、最有效。