本附录汇总了全书中出现的所有关键设计模式。每个模式用一句话说明”是什么”,再用一两句话解释”为什么要这样做”。可以作为回顾全书知识点的速查表。
模式总览
| 模式 | 在哪里用 | 为什么用 | 章节 |
|---|---|---|---|
| Activity 栈 | UI 页面管理 | Android 式导航 | 第 8 章 |
| 双 FreeRTOS 任务 | 主循环 + 渲染 | 墨水屏刷新慢 | 第 7 章 |
| RAII 锁 | RenderLock, PowerLock, StorageLock | 自动加锁解锁 | 第 7 章 |
| 分块帧缓冲 | GfxRenderer | 对抗 380KB 堆碎片化 | 第 9 章 |
| 两遍字体渲染 | FontCacheManager | 按需解压字形 | 第 9 章 |
| 二进制缓存 | book.bin, section.bin, index.bin | 避免重复解析 | 第 11 章 |
| 流式处理 | InflateReader, Expat XML | 不在 RAM 完整展开 | 第 10 章 |
| FNV-1a 哈希 | ZipFile 文件查找 | 零字符串分配 | 第 10 章 |
| Liang 断字 Trie | 断字引擎 | Flash 常驻零堆分配 | 第 11 章 |
| 声明式设置表 | SettingsList.h | 一处定义多处使用 | 第 15 章 |
| MAC XOR 混淆 | 密码存储 | 绑定物理设备 | 第 15 章 |
| 双 OTA 分区 | 固件升级 | 失败自动回滚 | 第 14 章 |
| HAL 硬件抽象 | HalDisplay/GPIO/Power/Storage | 隔离硬件差异 | 第 4 章 |
| I2C 设备指纹 | X3/X4 识别 | 运行时适配硬件 | 第 4 章 |
| 动态调频 | HalPowerManager | 省电 vs 性能平衡 | 第 13 章 |
| 链接器劫持 | __wrap_panic_abort | 崩溃信息捕获 | 第 14 章 |
逐项解读
Activity 栈(第 8 章)
借鉴 Android 的 Activity 模型,用一个栈结构管理 UI 页面。打开新页面就 push,按返回键就 pop,栈顶始终是当前显示的页面。这让页面导航逻辑变得可预测——不需要手动维护”从哪来、回哪去”的状态,栈结构天然保证了正确的返回顺序。对于一个只有七个按键的设备来说,清晰的导航模型直接决定了用户体验。双 FreeRTOS 任务(第 7 章)
主任务处理按键输入和业务逻辑,渲染任务专门负责把画面推送到墨水屏。墨水屏刷新需要几百毫秒,如果在同一个任务里等待刷屏完成,按键响应就会冻结。双任务架构让两件事互不阻塞——主任务发完”该画了”的通知就继续处理输入,渲染任务慢悠悠地刷屏也不影响操作响应。RAII 锁(第 7 章)
利用 C++ 对象的生命周期自动管理锁的获取和释放。创建对象时自动加锁,对象离开作用域时自动解锁。不管函数以什么方式退出(正常 return、提前 return、异常),锁都一定会被释放。这从根本上避免了手动 lock/unlock 模式中”忘记解锁导致死锁”的经典 bug。分块帧缓冲(第 9 章)
800x480 分辨率的帧缓冲区需要约 48KB 连续内存。在 380KB 的 RAM 中,经过反复 malloc/free 后,很可能找不到这么大的连续空闲块。解决方案是把 48KB 拆成多个 8KB 小块分散分配——就像把一个大衣柜拆成零件分多次搬运。访问时通过简单的除法和取模计算定位到正确的块,额外开销微乎其微。两遍字体渲染(第 9 章)
中文页面可能包含几百个不同汉字,字形数据压缩存储在 Flash 中,按分组打包。如果渲染时”用一个解压一个”,同一个分组可能被反复解压。两遍方案先扫描出当前页面需要的所有字形(第一遍),然后集中批量解压(只解压涉及的分组,每组只解压一次),最后真正渲染(第二遍)。代价是执行两次渲染流程,但第一遍只做字符串记录,开销极小。二进制缓存(第 11 章)
EPUB 文件每次打开都要解压 ZIP、解析 XML、构建目录结构——这些操作在 160MHz 的 CPU 上相当耗时。解决方案是首次解析后把结果序列化为自定义的二进制格式(book.bin、section.bin、index.bin),下次打开直接读缓存。这是典型的”用空间换时间”——SD 卡容量充裕,多存几个缓存文件换来秒开体验。流式处理(第 10 章)
ZIP 解压和 XML 解析都不在 RAM 中完整展开数据,而是以”流”的方式逐块处理。InflateReader 每次只解压几 KB 数据供上层消费,Expat XML 解析器逐标签触发回调。这样即使处理一个 10MB 的 EPUB 文件,内存占用也只有几 KB 的缓冲区大小。FNV-1a 哈希(第 10 章)
在 ZIP 文件中查找某个文件名时,不存储和比较字符串本身,而是比较字符串的 FNV-1a 哈希值(一个 32 位整数)。这避免了为每个文件名分配堆内存来存储字符串,用一次 O(1) 的整数比较替代了 O(n) 的字符串比较。在内存极度紧张的环境中,“不分配”比”少分配”更有价值。Liang 断字 Trie(第 11 章)
英文排版需要在合适的位置断词换行。CrossPoint Reader 使用 TeX 的 Liang 断字算法,将断字规则编码为 Trie(前缀树)数据结构,用PROGMEM 标记存放在 Flash 中。查询时直接在 Flash 上遍历 Trie,不需要任何堆内存分配。这是”把计算量转移到 Flash 读取”的典型案例——Flash 读取比 RAM 慢,但 Flash 有 16MB 而 RAM 只有 380KB。
声明式设置表(第 15 章)
所有设置项集中定义在一张表中,每个条目包含显示名、数据指针、JSON 键名、分类和约束条件。设备 UI、Web API 和 JSON 持久化三套系统遍历同一张表自动完成各自的工作。新增设置只需加一行声明,彻底消除了”改了 UI 忘了 API”的同步问题。MAC XOR 混淆(第 15 章)
用设备唯一的 MAC 地址对密码做 XOR 运算后再 Base64 编码存储。不是加密,但让密码绑定到特定物理设备——SD 卡被拔走后,其他设备无法还原明文。方案极其轻量(几行代码,零外部依赖),安全级别匹配嵌入式阅读器的威胁模型。双 OTA 分区(第 14 章)
Flash 中维护两个固件分区(A 和 B)。OTA 升级时,新固件写入非活跃分区,写完后切换启动分区并重启。如果新固件有问题导致启动失败,芯片自动回滚到旧分区。这保证了即使升级过程中断电、网络中断或新固件有 bug,设备也不会”变砖”。HAL 硬件抽象(第 4 章)
将所有硬件操作封装在四个模块中(HalDisplay、HalGPIO、HalPowerManager、HalStorage),向上层提供与硬件无关的接口。X3 和 X4 两个硬件版本的差异(不同的 I2C 芯片、不同的引脚分配)被完全隔离在 HAL 层内部,上层的阅读器功能代码完全一致。I2C 设备指纹(第 4 章)
开机时通过 I2C 总线探测特定地址的芯片是否存在,以此判断当前硬件版本。两轮探测取共识保证可靠性,结果缓存到 NVS 避免每次启动都探测。这让同一份固件能自动适配不同的硬件版本,用户不需要手动选择。动态调频(第 13 章)
CPU 主频根据负载动态调整——活跃操作时全速运行(160MHz),空闲 3 秒后降到 10MHz。降频可以显著减少功耗,延长电池续航。通过HalPowerManager::Lock(RAII 模式)确保关键操作(如墨水屏刷新)期间不会降频。
链接器劫持(第 14 章)
使用 GCC 的__wrap 机制拦截 ESP-IDF 底层的 panic_abort 函数。当固件崩溃时,劫持函数在设备重启前把崩溃信息(调用栈、寄存器状态)保存到 Flash。下次启动后可以通过 Web 界面查看崩溃报告,方便远程诊断。这对于嵌入式设备尤其重要——你无法让用户”打开控制台看看报错信息”。