上一篇聊的是书怎么"送"到设备上;这一篇往前退一步,聊送之前发生的事——一本从各种渠道搜集来的 EPUB,格式千奇百怪,直接扔给阅读器,轻则排版乱七八糟,重则目录整个消失、大部头漫画把设备内存吃到失控。cang-jie 项目里有一个专门干这件事的"优化引擎",这篇笔记讲它长什么样,以及两个真正值得记下来的教训——一个关于反编译破案,一个关于"估算"和"实测"之间的巨大鸿沟。
两步走:先摸底,再动图片
这套引擎处理一本书分两步,不是图省事,是被一次真实的内存事故逼出来的架构:

第一步只处理文字相关的内容——这部分体积通常不大,一本书哪怕几百页,纯文字也就是几百 KB 到几 MB。第二步才轮到图片,而且是一张处理完、写完、彻底忘掉,才轮到下一张,绝不会同时有好几张图片的数据一起堆在内存里。
这个设计是被一本真实的 552MB 漫画合集逼出来的。旧版本的做法更直觉:整本读进内存,一次性处理完再写出去。这本书真机测试的时候,内存直接冲到 1.4GB 以上,逼近设备失控的边缘,被现场手动叫停。改成两步走之后,同一本书处理内存全程稳定在几十 MB,跟书本身多大没什么关系了——这也是这类系统设计一条挺重要的经验:处理能力不该随文件体积线性下降,得想办法让"同时存在的数据量"跟文件总大小脱钩。
清洗:把五花八门的制作习惯拉回同一条线
第一步里最主要的工作是"清洗",处理的大多是兼容性问题——不同工具、不同制作者留下的各种小毛病:

这里有条特别反直觉的硬规矩:这台设备的阅读器只认写在独立样式文件里的排版规则,完全无视直接写在文章内部的行内样式。早期调试的时候,明明代码里排版规则一个字没错,真机上就是不生效,查了很久才发现是这条——改成外链样式表之后,这类"写对了却不生效"的问题才算彻底翻篇。
还有一点值得单独说一句:清洗只做"让设备能正确显示"这件事,不会改变作者原本的排版意图——不改字体颜色,不改加粗强调,只解锁字号大小(防止有些书把字号锁死到设备默认字号根本调不动)。工具的边界感在这——能力上做得到的事,不代表都该做。
目录消失之谜:一场靠反编译才破的案
清洗步骤里有一项是"检查目录",这背后藏着这套系统开发过程里最像侦探小说的一段经历。
有一本书,排版规范、结构完全没问题,但投到设备上之后,原生阅读器的目录按钮整个消失了——不是目录是空的,是这本书被判定成"压根没有目录"。第一个怀疑方向是书内部两处应该一致的编号对不上,改完真机复测,问题依旧。第二个怀疑方向是目录文件里带了一处不该有的外部引用,改完真机复测,问题还是没解决。
两次假设都错了之后,唯一剩下的办法是直接把设备的原厂阅读程序拆开看——它是一个没有源码、没有官方文档的闭源程序,只能靠反编译工具逐条指令核对:

真相是:电子书标准里,目录文件叫什么名字、内部 id 起什么都是作者自由决定的,标准要求的只是"正文索引里得清楚地指向它",两边对得上就行。这本书完全是这么做的,合规。但设备原厂程序压根不看索引里指向的是什么,代码里死认死一个写死的固定字符串——只有目录条目的 id 恰好叫这个名字,才会被认成"这本书有目录"。这本书的 id 起了个更符合语义的名字,不叫这个固定值,于是被判定成没有目录,跟内容和结构完全无关,纯粹是撞上了一处没写进任何文档、只存在于二进制里的隐藏规则。
修法本身很轻——不动书的任何实际内容,把目录条目的 id 顺手改成设备认的那个固定值就行。真机复测,这本书 94 条章节标题一次性全部恢复。这次排查最大的收获不是这一行修复,而是排查方法论本身:遇到闭源黑盒程序,"看起来应该是这样"永远不能替代"真的去看它到底是怎么写的",前两次基于合理猜测的尝试都扑了空,直到肯下功夫去读二进制才找到真正的答案。
图片瘦身:一次差点白修的教训
图片处理这部分要解决的问题很直接——书里常带着 2000 到 4000 像素宽的高清原图,远超设备屏幕实际分辨率,纯属浪费空间和加载时间,得按屏幕尺寸预先缩小。这部分逻辑本身没什么争议,真正出问题的是一处"安全阀"——防止极端情况下一张图片本身分辨率高到离谱,处理它需要的内存把设备顶爆。
第一版的做法是按公式估算:一个像素点解码后占几个字节,乘以总像素数,得出一个"安全上限"。算出来大概两千五百万像素是安全线,留了余量,看着挺稳妥。
真机上线不久,用户真实投递了一套大部头漫画,设备内存又一次冲高,峰值几乎跟完全没做防护时一样高。这时候才意识到,公式本身没有算错,错的是这个公式压根没考虑到,一张图片在被处理的整个过程里,会同时存在好几份不同状态的临时数据——解码出来一份、裁边处理再复制一份、缩放的时候源图和目标图又同时存在……公式只算了"最终数据占多大",没算"过程中同时活着的有多少份"。
补救的办法不是继续猜一个更保守的数字,而是干脆真机上跑一遍,在不同分辨率下,把真实的内存峰值一个个量出来:

数据摆出来之后,安全线该定在哪一目了然——不是拍脑袋往下调一点,是拿真实测出来的曲线去找一个真正安全的分辨率区间。改完之后,同样构造一本带有超高分辨率图片的测试书,走一遍完整流程,内存峰值稳稳压在个位数 MB。
这段经历后来变成了一条明确的工作习惯:跟内存/性能相关的安全阈值,靠公式估算只能当第一版草稿,真正拍板之前必须真机量出实际数字——哪怕只是写个简单的测试脚本、在几个不同规模下跑一遍看峰值,这一步不能省。省了这一步的代价,是一版看着"已经修好了"的代码,实际效果跟完全没修差不了太多,直到用户真实的下一次操作才暴露出来。
结语:优化不是"变小",是"配得上这台设备"
整条清洗+优化流水线背后的态度其实很朴素:尽量不改变书原本的样子,只解决"这台设备接不住"的那部分——排版规则该保留的保留,颜色加粗不去动它,图片该缩小的缩小但漫画画质不将就,遇到解决不了的极端情况(比如那张分辨率离谱的图),宁可原样放过,也不做可能拖垮整个处理过程的冒险动作。
技术上最扎实的两个收获,一个是"遇到黑盒程序别猜、去拆开看",一个是"内存相关的判断别算、去真机量"——两条路径不一样,但指向同一件事:真实世界给出的答案,永远比脑子里推演出来的更可信。