新增大书优化实施方案(内存与主线程、快速进入阅读器)和 WXRead 内存策略分析文档, 更新架构对比分析文档,完善章节级缓存运行时、串行加载、轻量磁盘摘要等设计细节。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
15 KiB
《凡人修仙传》快速进入阅读器方案
适用场景:
textReflowable路径打开超大正文 EPUB,典型样本为《凡人修仙传》精校版全本。 目标:把“进入阅读器前必须等全书分页完成”改成“先快速可读,再后台补齐全书能力”。 结论先行:当前首屏慢的主因不是单章分页太慢,而是 打开流程要求先完成全书RDEPUBTextBookBuilder.build()。
1. 当前慢在哪里
结合当前代码,打开 reflowable EPUB 的主链路是:
RDEPUBReaderController.viewDidLoad
-> RDEPUBReaderLoadCoordinator.loadPublication()
-> applyParsedPublication(...)
-> RDEPUBReaderPaginationCoordinator.paginatePublication()
-> if publication.readingProfile == .textReflowable
-> RDEPUBTextBookBuilder.build(...)
-> 遍历全部 spine item
-> 每章 render + paginate
-> 汇总成完整 RDEPUBTextBook
-> applyTextBook(...)
-> readerView.reloadData()
-> restoreReadingLocation(...)
关键事实:
- RDEPUBReaderPaginationCoordinator.swift 在
textReflowable路径里会先showLoading(),然后后台执行builder.build(...),构建完成前不会进入正文。 - RDEPUBTextBookBuilder.swift 的
build()会遍历全部publication.spine,逐章完成:- HTML 读取
DTCoreText渲染- CoreText 分页
- 页面模型组装
- 全书
RDEPUBTextBook汇总
- 也就是说,对《凡人修仙传》这种章节数多、正文长的大书,当前实际是“全书构建完成后才能看到第一页”。
这条链路的体验问题是:
- 首屏等待时间和“全书总字数/总章节数”线性相关,而不是和“当前阅读位置附近内容规模”相关。
- 即使用户只想看第一页,也要先支付整本书的 render + paginate 成本。
- 恢复到历史位置时也是同样问题,因为当前恢复逻辑依赖完整
RDEPUBTextBook的页码与位置映射。
2. 方案目标
用户目标
- 点击书籍后,阅读器应尽快进入正文页,而不是长时间停留在 loading。
- 即使全书尚未构建完成,也至少能:
- 看到当前章节
- 翻当前章节内的页
- 恢复到“接近上次阅读位置”的章节
技术目标
- 首屏进入从“全书 ready”改成“当前章节 ready”。
- 全书构建改为后台增量完成。
- 不破坏现有
RDEPUBReaderController/RDReaderView的主公开 API。 - 保留当前
RDEPUBTextBookCache的价值,但把缓存粒度从“整本书一次命中”扩展到“单章可命中”。
首期验收指标
- 大书首次打开时,进入正文的等待时间显著短于当前实现。
- 首屏进入只依赖“当前章节”或“当前位置附近章节”构建完成。
- 后台继续构建剩余章节时,不阻塞阅读。
3. 推荐方案:两阶段进入 + 章节级增量构建
阶段 A:快速进入
打开书后只做这些事情:
- 解析 EPUB 基础元数据、spine、TOC
- 确定恢复位置对应的
spineIndex - 只构建当前章节,必要时附带相邻
±1章 - 先生成一个“局部 TextBook / 局部 Snapshot”
- 立即进入阅读器并恢复到该章节内位置
这一阶段的原则是:
- 先解决“能进入”
- 不要求立刻具备全书搜索、全书目录页码、全书绝对页码精度
阶段 B:后台补全
进入正文后,再后台串行完成:
- 当前章节相邻章节预构建
- 剩余章节逐步构建
- 持续补齐全书:
RDEPUBTextBook.chaptersRDEPUBTextBook.pagesRDEPUBTextIndexTable- 全书页码/位置映射
- 构建完成后静默替换到完整模型
这样用户的体感是:
- 很快进书
- 越读越完整
- 而不是“先等很久,之后一次性全有”
4. 为什么这是最快可落地的方案
相比继续优化单次 build() 的 CPU 细节,这个方案收益更直接:
- 《凡人修仙传》的核心问题是“全书串行工作量太大”,不是“当前章节单章慢到不可接受”。
- 当前代码已经天然按“章节”组织:
publication.spineRDEPUBTextChapterRDEPUBChapterData- 每章
render + paginate
- 阅读器功能开发计划.md 里也已经明确把“增量构建”列为方向,说明这条路和现有架构一致。
换句话说,这不是推翻重做,而是把现有 RDEPUBTextBookBuilder.build() 从“必须一次性跑完整本书”拆成“可按章节单独执行”。
5. 具体改造点
5.1 构建层:把全书构建拆成章节级能力
当前:
- RDEPUBTextBookBuilder.swift 只有全书
build(...)
建议新增:
func buildChapter(
parser: RDEPUBParser,
publication: RDEPUBPublication,
spineIndex: Int,
pageSize: CGSize,
style: RDEPUBTextRenderStyle
) throws -> RDEPUBTextChapterBuildResult
建议返回:
chapter: RDEPUBTextChapterpaginationDiagnosticresourceDiagnosticsperformanceSample
这样做的好处:
- 章节构建逻辑可以和现有
build()共享 - 后续“首屏只构建一章”和“后台补全整本书”都走同一套实现
5.2 数据层:允许 TextBook 从“不完整”逐步变完整
当前:
RDEPUBTextBook默认假设chapters/pages/indexTable已经是完整全书
建议新增一个运行时模型,例如:
final class RDEPUBIncrementalTextBookStore
职责:
- 保存已完成构建的章节
- 维护
spineIndex -> chapter映射 - 动态生成当前可用的 pages snapshot
- 在“部分章节可用”时提供章节内阅读支持
建议暴露能力:
chapter(forSpineIndex:)availablePagesSnapshot()merge(chapter:)isChapterReady(_:)readyChapterRange(around:)
原因:
RDEPUBTextBook更适合“完整产物”- 增量加载需要一个“半成品但可读”的状态容器
5.3 分页协调层:把一次性 loading 改成两阶段 loading
当前:
- RDEPUBReaderPaginationCoordinator.swift 中
paginatePublication()直接把全书构建作为进入阅读器前置条件
建议改成:
Step 1:首次只构建目标章节
- 根据
restoreLocation算出目标spineIndex - 若无恢复位置,默认
spineIndex = 0 - 只构建该章,必要时加
±1章
Step 2:先应用局部 snapshot
readerView.reloadData()- 允许用户开始阅读
- tool chrome 可先显示,但某些依赖全书的能力先降级
Step 3:后台继续全书补建
- 串行队列逐章构建剩余章节
- 每完成一章,就 merge 进 store
- 必要时再刷新目录页码、搜索索引、页码总数
5.4 位置恢复:首期优先恢复“章节”,二期补齐“页内精度”
当前:
- 恢复逻辑大量依赖完整
RDEPUBTextBook的 pageNumber / location 映射
首期建议:
- 打开时先把恢复目标收敛到
spineIndex + fragment/rangeAnchor - 只要目标章节 ready,就先进入该章节
- 若该章节内页内恢复信息尚未完整,先恢复到该章节接近位置
这样能显著减少“为了恢复精确页码,必须先构建全书”的耦合。
5.5 缓存层:从整书缓存扩展到单章缓存
当前:
RDEPUBPaginationCacheCoordinator/RDEPUBTextBookCache更偏整书结果
建议:
- 缓存 key 增加
spineIndex - 支持单章 page ranges 命中
- 首次打开大书时,优先读取:
- 当前章节缓存
- 相邻章节缓存
收益:
- 同一本大书二次打开时,可直接秒开到当前章节
- 不用再等待整本书缓存完全重建
6. 首期最小可交付版本
为了最快解决《凡人修仙传》慢启动,建议首期只做下面这些:
- 为
RDEPUBTextBookBuilder抽出章节级构建接口 RDEPUBReaderPaginationCoordinator首次打开时只构建目标章节- 用“局部 pages snapshot”驱动
RDReaderView - 后台串行构建剩余章节
- 当前章节缓存命中优先
首期明确不做:
- 全书搜索实时可用
- 全书总页数一开始就准确
- 目录面板一开始就显示所有章节页码
这些能力可以在后台补建完成后逐步恢复。
7. UI 与交互建议
为了让“快速进入但后台仍在准备”体验自然,建议增加轻量提示:
阅读器内状态提示
- 首屏进入后不再是全屏 loading
- 改为顶部或底部轻提示:
正在准备后续章节...已进入阅读,可继续翻页
未就绪章节翻页策略
当用户快速翻到尚未构建的章节时:
- 优先命中后台预构建结果
- 如果还未就绪:
- 显示章节级 loading skeleton
- 不要退回全屏 blocking loading
目录与搜索降级
- 目录先显示标题,不强依赖页码
- 搜索面板可在全书索引未完成时显示:
正文已可阅读,全文搜索仍在准备中
8. 风险与应对
风险 1:现有很多 API 默认依赖完整 TextBook
例如:
pageNumber(for:)location(forPageNumber:)chapterData(forPageNumber:)
应对:
- 首期不要强行让这些 API 在“半本书”状态下也完整成立
- 先给增量模式增加“可用性边界”
- 在运行时根据
isFullyBuilt/isChapterReady分流
风险 2:局部 snapshot 和完整 snapshot 切换时页码跳动
应对:
- 局部模式优先用章节内位置恢复,不强调绝对页码稳定
- 后台切换到完整模型时,按
location而不是按pageNumber恢复
风险 3:后台补建打断当前交互
应对:
- 章节构建使用串行后台队列
- UI 合并更新节流,例如“每完成 N 章再刷新一次目录状态”
- 当前可见章节不重复重建
9. 推荐实施顺序
Phase 1:快速进入 MVP
- 抽
buildChapter(...) - 首次打开只构建目标章节
- 局部 snapshot 驱动阅读器
- 后台补建剩余章节
Phase 2:缓存加速
- 单章分页缓存
- 恢复位置附近章节优先命中
- 二次打开大书进一步提速
Phase 3:全书能力渐进恢复
- 全书搜索索引后台构建
- TOC 页码后台补齐
- 全书总页数在构建完成后更新
10. 建议验收方式
建议拿《凡人修仙传》精校版全本做专项验证,记录以下指标:
- 点击书籍到首屏可读的耗时
- 点击书籍到全书构建完成的耗时
- 首屏进入时用户是否已经可以翻当前章节
- 翻到下一章节时是否出现明显阻塞
- 二次打开同一本书时是否明显快于首次
重点不是只看“总构建时长”,而是看:
Time To First Readable PageTime To Full Book Ready
这两个指标在大书场景里要分开看。
11. 最终建议
对《凡人修仙传》这类超大正文书,最快见效的方案不是继续压榨单次全书分页性能,而是:
把阅读器打开流程从“全书先构建完”改成“当前章节先可读,剩余章节后台补齐”。
这是当前代码架构下收益最高、侵入性也相对可控的做法,因为:
- 现有数据天然按章节组织
RDEPUBTextBookBuilder已经具备章节级循环结构RDEPUBReaderPaginationCoordinator也已经是集中调度入口
如果只允许做一件事来解决《凡人修仙传》打开慢的问题,我建议优先做:
Phase 1:章节级增量构建 + 两阶段进入阅读器。
12. 当前落地状态(2026-06-02)
本轮已按 Phase 1 做了首期快速进入优化,当前实现状态如下。
已完成
RDEPUBTextBookBuilder已抽出章节级构建接口buildChapter(...),单章构建复用原有 render、分页、尾页规范化、诊断和分页缓存逻辑。RDEPUBReaderPaginationCoordinator的textReflowable路径已改为两阶段:- 第一阶段:按恢复位置优先构建可用章节,并立即应用局部
RDEPUBTextBook进入阅读器。 - 第二阶段:后台继续按章节增量构建,但增量结果会先暂存,只在用户空闲时再合并到当前可读内容,最终补齐为完整全书模型。
- 第一阶段:按恢复位置优先构建可用章节,并立即应用局部
- 快速进入阶段不只尝试单个 spine,而是按离恢复位置最近的可构建 HTML/XHTML spine 逐个尝试,避免封面、版权页、空白扉页导致首屏快速路径落空。
- 已修正阅读路径误判:普通静态 SVG 封面不再把整本小说误判为
webInteractive,避免错误掉回WKWebView分页路径。 - 首包策略已调整为“目标章节 + 后续 2 章”,默认先提供 3 章连续可读内容。
- 后台补齐策略已调整为“每新增 20 章生成一份新的局部结果”,并优先补当前可读窗口之后的章节,再回补前文。
- 增量结果不再一生成就立即
applyTextBook,而是只保留最近一份 stagedRDEPUBTextBook,等用户停止翻页且阅读器回到 idle 后再统一合并,减少翻页过程中的 UI reload 和主线程抖动。 - 已对后台分页任务做降干扰处理:章节补建切到较低优先级队列,用户刚翻页时后台任务会短暂停让,减少 pageCurl 翻页时的 CPU 抢占。
- 后台完整构建失败时,如果局部章节已经成功进入阅读器,则不再把用户退回阻塞式错误流程,只结束 loading;如果局部章节也未成功,则按原错误处理。
仍未完成
- 还没有引入独立的
RDEPUBIncrementalTextBookStore,当前首期实现采用“局部RDEPUBTextBook先应用,完整RDEPUBTextBook后替换”的 MVP 路径。 - 目录页码、全文搜索、全书总页数仍依赖后台完整构建完成后恢复。
- 尚未加入可视化的“后续章节准备中”轻提示。
当前预期效果
对《凡人修仙传》精校版全本这类章节多、正文长的大书,首屏进入不再等待整本书逐章 render + paginate 全部完成,而是先等待目标正文附近 3 章构建完成。全书分页仍会继续执行,但它从“进入阅读器前置条件”变成了“阅读器内后台补齐任务”;后台每补齐 20 章会生成一份新的局部结果,不过这份结果会先暂存,等用户空闲时再并入当前可读内容。
验证状态
已通过 CocoaPods workspace 构建 Demo,确认本轮快速进入优化可编译:
xcodebuild -workspace ReadViewDemo/ReadViewDemo.xcworkspace \
-scheme ReadViewDemo \
-configuration Debug \
-derivedDataPath /private/tmp/ReadViewDemoDerivedData \
CODE_SIGNING_ALLOWED=NO \
CODE_SIGNING_REQUIRED=NO \
build
结果:BUILD SUCCEEDED。
注意:直接构建 ReadViewDemo.xcodeproj 会绕开 Pods,导致 import RDReaderView 模块解析失败;验证时应使用 ReadViewDemo/ReadViewDemo.xcworkspace。
后续仍需要在真机或模拟器上用《凡人修仙传》精校版全本做实际打开耗时对比,重点记录 Time To First Readable Page 和 Time To Full Book Ready。