reMarkable Paper Pro Move 是一台墨水屏阅读/手写设备,官方没有给"批量管理电子书"这件事留太多余地——想往上传书,要么用官方 app 一本本传,要么接受它自带的云同步。cang-jie 项目里有一整套自建的"书架"系统,网页上传/抓取网文/整理,再投给设备上的两个阅读器:官方原生阅读器(适合精读、划线、写批注)和 KOReader(适合日常消遣、漫画)。
这篇笔记讲的不是"怎么做一个上传按钮",而是这套投递流程里几个真正踩出经验的设计点:状态该怎么记、书太大了怎么办、以及一次从"代码看着没问题"到"真机量出真问题"的内存排查。
三层模型:书先进"中转站",再决定去哪
最开始的设计冲动往往是"上传即完成"——选个文件,直接怼进阅读器。这套系统没有走这条路,中间硬生生插了一层:

这一层"母版库"看着像是多余的中间商,实际上解决了两个真实问题:
- 书可能要投给两个不同的阅读器,如果每次投递都重新处理一遍原始文件,等于同一件事做两遍,还容易两边处理得不一致。有一份公共的、处理好的底本,两边都从它取,天然一致。
- 书随时可能需要重投。设备重置了、换了台新设备、优化逻辑更新了想重新处理一遍——如果书投完就从源头消失,这些场景全都要用户重新上传一次。母版库的规矩是"投出去不等于删掉",永远留一份底。
两笔账:一笔靠脑子记,一笔写在纸上
同时"优化"和"投递"好多本书,怎么知道哪本正在处理、上次处理得怎么样?这里其实是两个独立的问题,混在一起想反而容易出 bug:

"正在忙"这件事,只需要活在程序运行的这一段时间里——程序意外重启了,"忙"的状态理应清零,因为确实没有任何操作还在后台跑,继续认为它"忙"反而是错的,会导致一本书被永久锁住、删不掉也点不动。这类状态放在内存里,简单直接,重启即忘,忘了也对。
"上次结果"是另一件事——用户想知道这本书昨天优化成功了没有、失败的原因是什么、现在进度到哪一步了,这些必须经得住重启,得写进磁盘,跟这本书的文件放在一起。
把这两件事分开之后,很多原本容易搞混的边界情况自己就理顺了:比如"程序被意外杀掉、重启后一本书显示卡在处理中",这在两笔账分开记的前提下,是一眼能判断出来的异常状态(忙锁没了但磁盘记录还停在"进行中"),而不是靠猜。
书太大传不进去:按卷拆开送
设备上原生阅读器的上传接口不是没有限制的——这是真机测出来的,不是文档里写的。用 curl 直传,从"确定能传"和"确定传不了"两个已知点开始做二分查找,一点点缩小区间,最终锁定在整数 1 亿字节(约 95MB)这条线上,超过它接口直接拒收断连。
这条线对普通书基本用不上——真正会撞上它的是大部头漫画合集,动辄两三百 MB。对这类书,投递流程会先判断"这是不是漫画、书里有没有自带的分卷/分章结构",如果有,就按结构拆开,一卷一卷单独投:

这里有个不算显眼但很关键的细节:传完一卷不是立刻传下一卷,而是先等设备真的把这一卷渲染出页数。最早的实现是传完就传下一份,结果真机上反复出现连接被对方掐断的情况——设备那边正忙着给刚收到的这一卷生成缩略图、建索引,来不及处理下一个请求,干脆把连接断了。改成"等这一卷真的有反馈了再传下一卷"之后,这个问题才彻底消失。这也是这类系统设计里经常低估的一点:接口能不能"扛住高频请求",跟接口本身逻辑对不对是两回事,得真拿设备去试。
一次真实的内存排查:从"看着对"到"真的对"
前面聊的是功能设计,接下来这段是一次纯粹的性能/稳定性故事。
投递一本不需要拆分的普通书(原生阅读器路径),原来的实现顺序是:整本读进内存 → 顺手拿这份内存里的数据算一个"这本书大概该有多少页"的估计值(用来投完之后跟设备实际渲染出的页数做核对,异常太大就说明这本书排版出问题了)→ 把这份数据交给上传逻辑,上传逻辑内部又克隆了一份拼网络请求包。
单看每一步,都是"读一下、算一下、传一下",没什么问题。真机测的时候拿一本 80MB 左右的书走一遍这条路径,观察进程实际用了多少内存——峰值冲到了 200MB 以上,是文件本身大小的两倍还多。

问题出在"整本读进内存"这一步被重复用了三次,每次用的方式还都不太一样——原始字节一份、算页数时因为要解压书内文件(包括图片)又整本展开了一份、上传时内部又克隆了一份准备网络包。三份叠在一起,峰值自然吓人。这台设备内存有限,且系统层面的内存限额压根没真正生效(后来才查出来这条限额规则本身没被系统认领,等于没设限),一旦叠加得多,风险不是"变慢"而是直接把设备逼近失控。
修法说起来简单:改成边读边传、边读边算,全程不落地成一份完整的字节数组。文件本身用流式方式直接喂给网络上传(读多少发多少,不预先攒一整份),估算页数的逻辑也改成只解压真正需要文字内容的部分,图片这类用不上的直接跳过、连解压都不做。改完之后同样这本书走一遍,进程内存峰值从两百多 MB 压到了几 KB 的量级——不是"降低了",是这条路径基本感觉不到这本书的存在了。
这次排查留下最有用的一条经验不是具体的代码改法,而是验证方式:功能测试通过、代码逻辑看着合理,都不等于内存峰值真的被压下去了。这条经验后来在另一处(图片处理那部分,写在下一篇优化引擎的笔记里)又验证了一遍——一版看似合理的修复方案,因为没有真的去量峰值数字,实际效果几乎跟没修一样,直到真机再次撞上才发现。
收尾:一点体验上的打磨
功能之外顺手做的两件小事,也值得提一句:
- 删除、批量清理这类有代价的操作,原来用的是浏览器自带的那种弹窗——样式跟页面完全不搭,还会打断整个页面的交互。换成了跟页面风格一致的自定义确认框。
- 处理中的书原来只显示一句不会变化的"正在处理,请稍候",现在换成了真实的进度条——能看具体进度的场景(比如按卷投递)画真百分比,看不到具体进度的场景至少给个"正在动"的滚动条,而不是一句死气沉沉的静态文字。
这两处单独看都不大,但都是"用户反馈一句话,回头查发现确实是能改的"这种典型的小闭环——技术决策之外,愿意为这类细节来回打磨,也是工程习惯的一部分。
下一篇讲 EPUB 优化引擎本身:一本随手下载的电子书,在真正被"喂"进阅读器之前经历了哪些清洗,以及一场靠反编译原厂程序才破的案——目录消失之谜。