嵌入式开发从Demo到产品化:分层架构与工程能力进阶

发布时间:2026/9/3 11:10:22
嵌入式开发从Demo到产品化:分层架构与工程能力进阶 很多嵌入式工程师做过的“项目”本质上是把一个开发板自带的 demo 例程改了一下引脚、换了一个外设、调了一组参数然后录个视频、截几张图、写进简历。这类工作不是没有价值但它和工业级嵌入式开发之间隔着一整条产品化链路。这篇文章不劝退只讲清楚一件事从“跑通 demo”到“能交付一个可靠、可维护、可扩展的嵌入式系统”你缺的到底是什么以及应该怎么补。如果你的日常是 STM32、嵌入式 Linux、单片机裸机或 RTOS 开发但又觉得“代码能跑就行”“文档没什么用”“架构是芯片厂家的事”那这篇内容值得看完。我会从软件分层、状态机与事件驱动、驱动设计、嵌入式 Linux 内核模块、稳定性排查和工程能力成长这几个维度展开。每一条都会给出可参考的代码结构和判断标准而不是泛泛讲道理。1. 为什么“嵌入式别再做 demo 项目了”先说一个矛盾现象嵌入式岗位招聘常年强调“有实际项目经验”但很多候选人简历上的项目放在面试官眼里一眼就能看出是 demo。为什么能看出来因为 demo 项目有几个共同的软肋只有一个主循环所有业务逻辑堆在main.c里。外设初始化来自厂商例程几乎没改过配置逻辑。没有协议解析状态机直接按字节数组处理。没有错误处理、没有日志、没有看门狗、没有低功耗策略。模块之间靠全局变量通信。换一块板子、换一个芯片型号代码基本要重写。demo 项目擅长证明“开发板上的外设能工作”但真实嵌入式产品要在功耗、成本、实时性、可靠性、可维护性之间做取舍。一个“能点灯”的例子和“灯在 10 万次异常重启后仍然能正确响应”之间差了完整的系统设计。从能力成长角度说长期停留在 demo 层面还有一个隐藏代价你积累的不是“设计能力”而是“套用能力”。一旦遇到没有现成例程的新芯片、新外设、新业务场景就会立刻失去方向。真正值钱的不是你把哪个型号的芯片跑通了而是你能把一个不确定的需求拆成确定的功能模块再把功能模块落到硬件资源有限的芯片上。下面先用一张表格对比 demo 阶段和工程化阶段的核心差异。你可以拿它当自检清单。维度demo 阶段工程化阶段代码组织单文件、一个主循环分层驱动层、服务层、业务层模块通信全局变量接口函数、消息队列、事件回调状态处理循环轮询标志位状态机、事件驱动、RTOS 任务模型硬件适配直接改厂商例程寄存器通过配置文件或板级描述抽象硬件差异错误处理几乎不处理带返回值、错误码、恢复策略稳定性能跑通即可看门狗、异常捕获、日志、压力测试扩展性加功能靠复制粘贴加设备靠注册驱动/添加模块交付范围源码和演示视频文档、协议、测试报告、升级方案2. 先判断一下自己有没有“demo 病”想摆脱 demo 思维先要承认它存在。下面的问题不用全中但如果你中了三条以上说明当前的项目习惯已经在限制你的能力边界了。新项目复制旧工程的main.c然后把不用的代码注释掉。一个状态标志位可能被中断、主循环、定时器三个地方修改但没有加保护。串口收到数据就开始处理从不校验帧头和帧尾也不考虑粘包/半包。任务之间的先后关系靠delay_ms()强行安排。很少使用断点调试出现问题先改代码试一下。芯片型号或 HAL 库版本一变代码就要大面积重构。关于变量生命周期、栈大小、堆分配策略几乎没有概念。认为操作系统和事件驱动是“性能不够”才需要的东西。每次改完代码只在“正常路径”验证一次从不做异常输入测试。产品交付后出了问题只能靠客户截图和反复复现定位。这几条都不是技术细节问题而是思维方式问题。demo 思维的核心是“证明它曾经能工作”工程思维的核心是“保证它在约束条件下持续可靠工作”。如果你已经意识到了这种差距下一步就是怎么把一个 demo 改造成真正能用的工程结构。3. 第一道门槛从单文件主循环到分层架构最常见的 demo 代码长这样int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); while (1) { if (flag_recv 1) { flag_recv 0; HAL_UART_Transmit(huart1, rx_buffer, rx_len, 1000); } } }这段代码如果只是演示串口回环没有问题。但如果你要在这个基础上做一个小型传感器采集节点比如“通过 RS485 读取温湿度上传到上位机同时本地驱动一个继电器”就不能继续往上堆逻辑。原因是中断回调里会塞进业务处理主循环会变成一堆if和标志位任何模块想复用一段逻辑只能复制。工程化的第一步是分层。以一个小型 MCU 项目为例最基础的分层结构如下app/ /* 业务层采集任务、上传策略、控制逻辑 */ sensor_task.c upload_task.c relay_task.c service/ /* 服务层协议栈、数据缓存、状态机 */ modbus_slave.c ring_buffer.c protocol_parser.c driver/ /* 驱动层芯片外设的直接操作 */ uart_driver.c gpio_driver.c adc_driver.c relay_driver.c bsp/ /* 板级支持引脚映射、时钟配置 */ board_config.c board_config.h common/ /* 通用工具 */ error_code.h log.c utils.c分层带来的第一个改变是驱动层可以被替换。比如把传感器从硬件 UART 换成模拟 UART或者从 STM32 平台迁移到 GD32、国民技术等兼容平台只要驱动层提供相同的接口上层代码不需要修改。第二个改变是可测试性提高了。协议解析器不再直接依赖串口寄存器而是接收一个字节流缓冲区输入输出边界清楚就能在 PC 上做单元测试。第三个改变是人的协作效率提高。两个人同时开发一个项目一个改驱动一个写应用只要接口约定好互不阻塞。这个阶段最值得掌握的技巧是“接口驱动开发”。驱动层头文件只暴露稳定的 API不暴露内部寄存器细节。上层代码不关心底层是寄存器操作还是 HAL 库函数只关心返回值。/* driver/uart_driver.h */ #ifndef UART_DRIVER_H #define UART_DRIVER_H #include stdint.h typedef void (*uart_rx_callback_t)(uint8_t byte); int uart_driver_init(void); int uart_driver_send(const uint8_t *data, uint16_t len); int uart_driver_set_rx_callback(uart_rx_callback_t cb); void uart_driver_rx_byte_handler(uint8_t byte); #endif当你能做到“自己定义外设能力接口、自己设计调用关系”的时候就已经离开了 demo 的舒适区。接下来要考虑的是更大颗粒度的问题多个任务在裸机上如何“同时”运行。4. 应用架构演进超级循环、状态机与事件驱动嵌入式圈经常讨论“从超级大循环到事件驱动”这个演进不是单纯追新而是由业务复杂度推动的。当一个系统只有两个状态时用while(1)加轮询完全可以。但当一个系统有启动、待机、运行、故障、升级五个大状态且每个大状态下还有若干子状态时继续用标志位和if组合代码会变成一团乱麻。一个典型的上位机通信协议解析就适合用状态机解决。比如一帧数据格式是AA 55 LEN CMD PAYLOAD... CHECK如果按顺序接收并解析一个可靠的状态机可以这样写typedef enum { FRAME_IDLE, FRAME_HEAD1, FRAME_HEAD2, FRAME_LEN, FRAME_CMD, FRAME_PAYLOAD, FRAME_CHECK } frame_state_t; frame_state_t state FRAME_IDLE; uint8_t frame_buf[128]; uint16_t frame_len 0; uint16_t payload_cnt 0; int protocol_rx_byte(uint8_t byte) { int result -1; switch (state) { case FRAME_IDLE: if (byte 0xAA) state FRAME_HEAD1; else state FRAME_IDLE; break; case FRAME_HEAD1: if (byte 0x55) state FRAME_HEAD2; else state FRAME_IDLE; break; case FRAME_HEAD2: frame_len byte; if (frame_len sizeof(frame_buf)) state FRAME_IDLE; else state FRAME_LEN; break; case FRAME_LEN: frame_buf[0] byte; payload_cnt 1; state (payload_cnt frame_len) ? FRAME_PAYLOAD : FRAME_CHECK; break; case FRAME_PAYLOAD: frame_buf[payload_cnt] byte; if (payload_cnt frame_len) state FRAME_CHECK; break; case FRAME_CHECK: /* 实际项目在此校验 CRC/LRC */ result 0; state FRAME_IDLE; break; default: state FRAME_IDLE; break; } return result; }状态机的优点是程序每一次只依据“当前状态 当前输入”决定下一步动作没有跨模块的全局变量纠缠问题复现和定位难度显著下降。再往上走就是事件驱动架构。事件驱动不是 RTOS 的专利裸机协作式调度也可以实现。核心思路是外设产生事件事件进入队列主循环从队列取事件并分发到不同模块。这样串口中断、按键中断、定时器中断都只负责“产生事件”不负责“处理业务”。事件驱动的最小实现并不复杂#define EVENT_QUEUE_SIZE 16 typedef struct { uint16_t event_id; uint32_t param; } event_t; event_t event_queue[EVENT_QUEUE_SIZE]; volatile uint8_t event_head 0; volatile uint8_t event_tail 0; int event_post_from_isr(uint16_t id, uint32_t param) { uint8_t next (event_tail 1) % EVENT_QUEUE_SIZE; if (next event_head) { return -1; /* 队列满 */ } event_queue[event_tail].event_id id; event_queue[event_tail].param param; event_tail next; return 0; } int event_poll(event_t *evt) { if (event_head event_tail) { return -1; } *evt event_queue[event_head]; event_head (event_head 1) % EVENT_QUEUE_SIZE; return 0; }使用这个队列后中断回调里只入队不处理void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart huart1) { event_post_from_isr(EVENT_UART_RX_DONE, rx_byte); HAL_UART_Receive_IT(huart1, rx_byte, 1); } }主循环里统一取事件while (1) { while (event_poll(evt) 0) { switch (evt.event_id) { case EVENT_UART_RX_DONE: protocol_rx_byte((uint8_t)evt.param); break; case EVENT_BTN_PRESSED: relay_control_toggle(); break; default: break; } } /* 低功耗模式或空闲处理 */ }事件驱动架构带来的收益是中断上下文尽量短业务处理集中模块之间通过事件解耦。当功能从 5 个增加到 20 个时主循环依然是“取事件、分发、处理”三段式而不是按下葫芦浮起瓢。5. 从驱动到可管理以 STM32 RS485 为例RS485 是工业通信里绕不开的接口。很多人在串口 demo 上加一个DIR引脚控制收发方向就认为自己掌握了 RS485。但这只是第一步。真实 RS485 产品设计至少涉及三类问题物理层收发方向控制、一主多从的时序管理、抗干扰和总线异常恢复。先看一个常见的错误写法发送数据前拉高DE引脚发送完立刻拉低。问题在于 UART 发送完最后一个字节后移位寄存器可能还有数据没送完如果立刻切换方向总线上的最后一帧会被截断。一个相对稳妥的控制方式是等待发送完成事件后再拉低DEvoid rs485_send(uint8_t *data, uint16_t len) { RS485_DE_ENABLE(); HAL_UART_Transmit(huart_rs485, data, len, 100); /* 部分场景需要额外延时等待移位寄存器清零 */ RS485_DE_DISABLE(); }更严格的方案是发送完成后在中断里处理或者根据波特率计算位时间延时。这段代码本身不复杂但它说明一个道理demo 只需要功能通而工程需要处理“最后一笔数据到底有没有发完”这种边界问题。RS485 更常见的是主从协议。主机发请求从机应答。此时要注意从机收到请求后需要一定处理时间不能要求它立刻应答。主机如果没收到应答要考虑重试次数、重试间隔和错误上报。如果总线上有多个从机地址匹配失败的数据帧不能阻塞主循环。异常断电恢复后从机要能回到已知状态。如果把协议处理直接写在串口中断里一旦数据帧变长、从机数量变多中断耗时会拖累整个系统。所以刚才的事件队列和协议状态机在这里才真正发挥作用串口中断只负责把字节放进队列协议协处理逻辑在主循环里跑这样系统随时可以响应按键、看门狗等更高优先级的任务。这段经验放在任何带通信接口的 MCU 项目里都成立包括常见的 Modbus、自定义私有协议、CAN 总线帧处理。先做好一主多从的管理再谈实时性优化。6. 中断、阻塞与并发裸机开发者的核心考验很多从 demo 走向产品的工程师遇到的第一个崩溃类故障都和中断使用不当有关。中断回调里调用了耗时函数、中断里操作了非原子的共享变量、两个不同优先级的中断同时访问同一个外设。这类 Bug 的可怕之处在于它不总是复现可能几十个小时才出现一次出现后还无法用串口打印定位。处理中断的几条铁律中断服务函数尽量只做置标志、入队列、清中断这三件事。中断和主循环共享的变量要么用临界区保护要么保证原子访问。不要在中断里调用HAL_UART_Transmit这类阻塞型函数。不要在中断里使用printf如果必须打印用非阻塞日志缓冲。注意编译器优化对 volatile 变量的影响。下面是一个真实的故障案例模型两个中断都修改同一个 16 位全局计数变量。在主循环读取时可能读到一个“被两次写操作撕裂”的中间值。在 Cortex-M 平台上32 位及其以下的对齐访问通常是原子的但如果变量是 64 位或者被编译器拆成多次读写就会出问题。处理方式是进入临界区uint32_t saved_prims __get_PRIMASK(); __disable_irq(); local_count global_count; __set_PRIMASK(saved_prims);这段写法在裸机环境里很实用。但更推荐的做法是从设计上减少共享变量每个外设模块的数据由该模块独占其他模块通过函数接口来获取结果。例如温度采样模块维护自己的缓冲区协议层需要温度时不直接读取计量模块的全局数组而是调用temperature_get_last_value()。7. 从裸机到嵌入式 Linux会改例程不够要读懂内核交互如果单片机层级的工程化能力是“提升代码组织质量”那么嵌入式 Linux 项目要解决的问题完全是另一个量级你没有 root 权限的顾虑、有多线程和进程地址空间隔离、有复杂的存储布局和启动链路。很多人的嵌入式 Linux 经验停留在“按照教程交叉编译、烧录镜像、跑通 demo 程序”但真要写一个驱动或者排查一个内存泄漏立刻无从下手。从“能启动”到“能掌控”我建议至少掌握下面四条主线。7.1 设备树硬件的描述性语言不管用的是 NXP i.MX 系列、瑞芯微平台还是全志平台设备树已经是内核和板级硬件之间的标准接口。你能读懂设备树里的compatible、reg、interrupts、clocks这些属性才能理解内核为什么能自动匹配驱动。/ { gpio-key { compatible gpio-keys; autorepeat; key-power { label POWER; linux,code KEY_POWER; gpios gpio4 10 GPIO_ACTIVE_LOW; }; }; rs485-ctrl { compatible mycompany,rs485-ctrl; reg 0x0 0x10; de-gpios gpio5 3 GPIO_ACTIVE_HIGH; }; };会写设备树不等于会写驱动但不会看设备树你在嵌入式 Linux 板子上几乎寸步难行。每次拿到新板卡第一步就是看在设备树里怎么描述外设再决定用户态操作还是写内核驱动。7.2 内核模块最小的可加载驱动一个最小字符设备驱动涉及 file_operations 的注册、class_create、device_create、 module_init 和 module_exit 等机制。很多人觉得内核开发门槛高其实可以先从一个/dev/rs485字符设备开始把用户态读写映射到 GPIO 和 UART 操作上#include linux/init.h #include linux/module.h #include linux/cdev.h #include linux/fs.h #include linux/uaccess.h #include linux/device.h #define DEV_NAME rs485_dev static int rs485_open(struct inode *inode, struct file *filp) { return 0; } static ssize_t rs485_read(struct file *filp, char __user *buf, size_t count, loff_t *offset) { char kbuf[32] rs485 ready; size_t len strlen(kbuf) 1; if (count len) return -EINVAL; if (copy_to_user(buf, kbuf, len)) return -EFAULT; return len; } static const struct file_operations rs485_fops { .owner THIS_MODULE, .open rs485_open, .read rs485_read, }; static int __init rs485_init(void) { return 0; } module_init(rs485_init);这个例子故意省略了实际注册逻辑但核心思维是用户态通过/dev/rs485访问设备内核驱动负责操作真正的硬件资源。这样应用层只做业务不需要知道寄存器地址、时钟频率等底层信息。完整的内核模块开发还需要处理copy_to_user/copy_from_user的安全检查、并发访问、内存分配释放、设备树匹配等内容。但只要你理解“内核驱动是硬件能力和用户态应用之间的中间层”这条路就能走通。7.3 用户态与内核态的分工不是所有硬件操作都必须写驱动。比如一个 GPIO 控制的继电器在多数工业 Linux 平台下可以通过/sys/class/leds或 gpiod 接口直接从用户态控制根本不需要写内核模块。合理划分用户态和内核态边界是工程能力的体现。需要高性能、中断上下文、DMA 缓存管理的放内核模块。只做业务策略、时序控制、协议解析的放用户态进程。实时要求极高的运动控制最好交给独立 MCU 或 RTOS而不是嵌入式 Linux。7.4 内存泄漏与进程稳定性排查嵌入式 Linux 上最常见的稳定性问题不是驱动崩溃而是用户态进程内存缓慢增长。Qt 程序和其他 C 程序容易出现生命周期管理不当导致的内存泄漏。排查思路是按命令逐层检查cat /proc/meminfo cat /proc/$PID/status cat /proc/$PID/smaps top -d 1 -p $PID如果是 Qt 程序先检查QObject父子对象是否设置正确、信号槽连接是否反复创建、QTimer 是否停止并释放。如果进程跑几天后 RSS 持续上涨且不回落建议先用 ASAN 在测试环境编译一次再配合valgrind查泄漏点。内存问题一旦到产品现场再排查代价极高。8. 稳定性设计不是把代码写对而是让系统在异常下仍然可恢复真实嵌入式产品和 demo 之间最明显的差距在异常处理。demo 程序只在正常条件下跑一遍而产品要在电压波动、电磁干扰、通信超时、固件升级中断、用户误操作下保持可恢复状态。工程上至少要做到五件事。8.1 错误码与返回值约定C 语言没有异常机制错误处理只能靠返回值、错误码和断言。驱动层函数建议统一返回标准错误码而不是散落的0和-1typedef enum { ERR_OK 0, ERR_PARAM -1, ERR_TIMEOUT -2, ERR_BUSY -3, ERR_OVERRUN -4, ERR_NO_MEM -5, } err_code_t;上层调用时忽略错误码是一个危险信号。如果传感器读取失败后你还继续用旧数据进行控制这种错误累积会在某个临界点引发事故。8.2 看门狗的正确使用独立看门狗IWDG不能只是初始化后一直喂狗否则程序卡死在主循环里也会被看门狗误认为“活着”。正确的做法是只在完成一轮主任务后喂狗。裸机系统可以设置“喂狗点”RTOS 系统可以设置一个独立监控任务监控所有业务任务的运行标志。8.3 异常记录与日志分级很多资源受限 MCU 上没有文件系统但不代表不能留日志。可以把异常码写入备份寄存器、EEPROM 或 Flash 末页下次上电时先读取再上报。日志分级能帮助你快速区分“噪音信息”和“故障关键信息”ERROR要立刻处理DEBUG只需要在开发阶段打印。不加分级地到处打印日志本身会变成噪音。8.4 协议层的非法输入防护设计串口/CAN/网络协议时要假设对端不是你的开发板而是一个会发任意字节序列的异常设备。帧长度超过缓冲区、地址不匹配、校验失败、接收一半超时这些都要有处理分支。用一个协议状态机 环形缓冲区的组合能覆盖绝大部分异常输入场景。8.5 低功耗管理做电池供电设备。demo 阶段为了省事可能直接禁用低功耗模式但产品续航不过关时只能重新梳理外设唤醒策略。低功耗的本质是“在不需要的时候关掉时钟和外设电源在需要的时候快速唤醒”。没有统一功耗管理策略的系统往往是在某个外设回调里反复唤醒 MCU导致平均功耗居高不下。9. 常见问题与排查方法下面这些内容来自实际项目中的高发故障不是理论推演。遇到问题先按表格检查超过八成问题能定位到根因。问题现象可能原因排查方式解决方案程序偶发进入 HardFault栈溢出、野指针、数组越界查看 Fault 寄存器、反汇编定位 PC 值加大栈空间、审查数组边界、启用 MPU 保护串口数据出现乱码波特率配置错误、地线不稳、信号干扰示波器测量波形、降低波特率测试校准时钟、改善硬件电路或启用校验RS485 通信偶发丢帧DE 方向切换过快、收发切换时序不对抓波形观察最后一个字节是否被截断在发送完成中断后再切换方向设备上电偶尔无法启动供电时序异常、外部复位源触发检查电源曲线和复位引脚波形调整上电时序、增加去抖电容Linux 进程运行几天后卡死内存泄漏、句柄泄漏、线程死锁查看/proc/PID/fd、内存占用、线程栈修复泄漏、引入超时机制设备在强电磁干扰环境下重启电源跌落、复位线干扰、程序跑飞检查复位原因寄存器、加宽看门狗周期硬件加防护、软件增加异常恢复流程产品代码换芯片型号无法移植寄存器操作和业务逻辑耦合审查是否分层、外设接口是否统一按驱动层/服务层/应用层重构日志太多导致系统卡顿同步打印阻塞了主循环关闭 DEBUG 日志对比响应时间使用异步日志缓冲或分级打印修改一个模块导致其他模块异常全局变量耦合、模块边界不清审查全局变量引用清单用接口函数替代直接访问10. 最佳实践嵌入式工程师怎么完成项目升级结合前面的分层、状态机、事件驱动、驱动设计和稳定性设计我建议把自己手头的项目按下面六个方向逐步改造。第一重构代码目录。哪怕是一个旧项目先按 driver/service/app 拆分至少把main.c里的业务逻辑迁出去。第一次重构会很难受但之后每次改动都会更快。第二规范接口头文件。每个驱动模块给出稳定的接口禁止业务层直接操作寄存器。寄存器访问这类操作收敛到驱动内部不要让它们散落在应用代码里。第三引入状态机处理复杂协议。如果还在用一堆if判断接收状态花两个晚上改成状态机版本。状态机的收益不只是代码漂亮更是可测试性大幅提升。第四为关键模块写单元测试。协议解析和业务算法不依赖硬件可以在 PC 上编译测试。热词里有“unity 嵌入式单元测试”Unity 测试框架是一个很好的起点。对于 MCU 工程可以用make test在本地主机环境跑测试不需要每次下载到板子。第五排查代码里的所有全局变量。写一个脚本扫描.c文件中未加static的全局变量审视每个全局变量是否真的需要跨文件可见。全局变量是模块耦合的最常见来源。第六增加错误日志和异常恢复流程。从“代码能跑”到“跑错了能告诉你哪里错了”是产品化和 demo 化最大的分水岭。11. 嵌入式开发学习路线与面试建议如果你还是学生或者刚入行不要急着追求“会更多芯片型号”。芯片型号会过时工程方法论不会。最稳妥的学习路线是这样的第一步在一款主流 MCU比如 STM32上跑透裸机GPIO、外部中断、定时器、UART、I2C、SPI、ADC、DMA。第二步不看厂商例程自己写一个“传感器 通信 控制”的小系统强制自己分层。第三步引入状态机和事件队列完成一套带错误恢复的协议栈。第四步上一款 RTOS做任务优先级划分和资源管理理解信号量、互斥锁、消息队列。第五步进入嵌入式 Linux 领域先熟悉交叉编译、启动流程、设备树再写一个完整的内核模块。第六步做可测试性设计和自动化回归测试把代码质量纳入交付标准。面试时“八股文”能让你通过第一轮但真正区分水平的是你能不能讲清楚设计取舍为什么选择裸机而不是 RTOS为什么任务划分成三个而不是五个这个协议错误恢复怎么做的这些问题都回答好了已经不再是“只会做 demo”的候选人。12. 总结从 demo 到产品的转折点回到标题嵌入式别再做 demo 项目了。这句话的真正含义不是让你不做小项目而是做完一个 demo 后继续往下做一层、两层、三层补上错误处理、补上协议状态机、补上驱动接口、补上测试用例、补上稳定性设计。真正值得写进简历的项目不是“我调通了某某外设”而是“我在有限的 Flash、RAM、功耗和成本约束下设计了一个能持续运行的系统”。下一步最值得做的事也很明确选一个你已经跑通的 demo不增加任何新硬件用本文提到的分层、状态机、事件队列、错误码、日志记录这套方法把它完整重构一遍。重构完你会发现自己看的不是代码能不能运行而是代码值不值得长期维护。这篇文章里的代码示例和排查表可以直接当改造清单用。如果你正在被某个 Bug 或架构问题卡住建议先对照第 9 节排查一遍再动手重构。希望少一点“只会演示”多一点“能交付”。