ReadViewSDK/Doc/大书快速进入阅读器方案_凡人修仙传.md
shenlei 488350d956 docs: 大书优化方案文档与架构分析更新
新增大书优化实施方案(内存与主线程、快速进入阅读器)和 WXRead 内存策略分析文档,
更新架构对比分析文档,完善章节级缓存运行时、串行加载、轻量磁盘摘要等设计细节。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 21:16:50 +08:00

15 KiB
Raw Blame History

《凡人修仙传》快速进入阅读器方案

适用场景: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.swifttextReflowable 路径里会先 showLoading(),然后后台执行 builder.build(...),构建完成前不会进入正文。
  • RDEPUBTextBookBuilder.swiftbuild() 会遍历全部 publication.spine,逐章完成:
    • HTML 读取
    • DTCoreText 渲染
    • CoreText 分页
    • 页面模型组装
    • 全书 RDEPUBTextBook 汇总
  • 也就是说,对《凡人修仙传》这种章节数多、正文长的大书,当前实际是“全书构建完成后才能看到第一页”。

这条链路的体验问题是:

  • 首屏等待时间和“全书总字数/总章节数”线性相关,而不是和“当前阅读位置附近内容规模”相关。
  • 即使用户只想看第一页,也要先支付整本书的 render + paginate 成本。
  • 恢复到历史位置时也是同样问题,因为当前恢复逻辑依赖完整 RDEPUBTextBook 的页码与位置映射。

2. 方案目标

用户目标

  • 点击书籍后,阅读器应尽快进入正文页,而不是长时间停留在 loading。
  • 即使全书尚未构建完成,也至少能:
    • 看到当前章节
    • 翻当前章节内的页
    • 恢复到“接近上次阅读位置”的章节

技术目标

  • 首屏进入从“全书 ready”改成“当前章节 ready”。
  • 全书构建改为后台增量完成。
  • 不破坏现有 RDEPUBReaderController / RDReaderView 的主公开 API。
  • 保留当前 RDEPUBTextBookCache 的价值,但把缓存粒度从“整本书一次命中”扩展到“单章可命中”。

首期验收指标

  • 大书首次打开时,进入正文的等待时间显著短于当前实现。
  • 首屏进入只依赖“当前章节”或“当前位置附近章节”构建完成。
  • 后台继续构建剩余章节时,不阻塞阅读。

3. 推荐方案:两阶段进入 + 章节级增量构建

阶段 A快速进入

打开书后只做这些事情:

  1. 解析 EPUB 基础元数据、spine、TOC
  2. 确定恢复位置对应的 spineIndex
  3. 只构建当前章节,必要时附带相邻 ±1
  4. 先生成一个“局部 TextBook / 局部 Snapshot”
  5. 立即进入阅读器并恢复到该章节内位置

这一阶段的原则是:

  • 先解决“能进入”
  • 不要求立刻具备全书搜索、全书目录页码、全书绝对页码精度

阶段 B后台补全

进入正文后,再后台串行完成:

  1. 当前章节相邻章节预构建
  2. 剩余章节逐步构建
  3. 持续补齐全书:
    • RDEPUBTextBook.chapters
    • RDEPUBTextBook.pages
    • RDEPUBTextIndexTable
    • 全书页码/位置映射
  4. 构建完成后静默替换到完整模型

这样用户的体感是:

  • 很快进书
  • 越读越完整
  • 而不是“先等很久,之后一次性全有”

4. 为什么这是最快可落地的方案

相比继续优化单次 build() 的 CPU 细节,这个方案收益更直接:

  • 《凡人修仙传》的核心问题是“全书串行工作量太大”,不是“当前章节单章慢到不可接受”。
  • 当前代码已经天然按“章节”组织:
    • publication.spine
    • RDEPUBTextChapter
    • RDEPUBChapterData
    • 每章 render + paginate
  • 阅读器功能开发计划.md 里也已经明确把“增量构建”列为方向,说明这条路和现有架构一致。

换句话说,这不是推翻重做,而是把现有 RDEPUBTextBookBuilder.build() 从“必须一次性跑完整本书”拆成“可按章节单独执行”。


5. 具体改造点

5.1 构建层:把全书构建拆成章节级能力

当前:

建议新增:

func buildChapter(
    parser: RDEPUBParser,
    publication: RDEPUBPublication,
    spineIndex: Int,
    pageSize: CGSize,
    style: RDEPUBTextRenderStyle
) throws -> RDEPUBTextChapterBuildResult

建议返回:

  • chapter: RDEPUBTextChapter
  • paginationDiagnostic
  • resourceDiagnostics
  • performanceSample

这样做的好处:

  • 章节构建逻辑可以和现有 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

当前:

建议改成:

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. 首期最小可交付版本

为了最快解决《凡人修仙传》慢启动,建议首期只做下面这些:

  1. RDEPUBTextBookBuilder 抽出章节级构建接口
  2. RDEPUBReaderPaginationCoordinator 首次打开时只构建目标章节
  3. 用“局部 pages snapshot”驱动 RDReaderView
  4. 后台串行构建剩余章节
  5. 当前章节缓存命中优先

首期明确不做:

  • 全书搜索实时可用
  • 全书总页数一开始就准确
  • 目录面板一开始就显示所有章节页码

这些能力可以在后台补建完成后逐步恢复。


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 Page
  • Time To Full Book Ready

这两个指标在大书场景里要分开看。


11. 最终建议

对《凡人修仙传》这类超大正文书,最快见效的方案不是继续压榨单次全书分页性能,而是:

把阅读器打开流程从“全书先构建完”改成“当前章节先可读,剩余章节后台补齐”。

这是当前代码架构下收益最高、侵入性也相对可控的做法,因为:

  • 现有数据天然按章节组织
  • RDEPUBTextBookBuilder 已经具备章节级循环结构
  • RDEPUBReaderPaginationCoordinator 也已经是集中调度入口

如果只允许做一件事来解决《凡人修仙传》打开慢的问题,我建议优先做:

Phase 1章节级增量构建 + 两阶段进入阅读器。


12. 当前落地状态2026-06-02

本轮已按 Phase 1 做了首期快速进入优化,当前实现状态如下。

已完成

  • RDEPUBTextBookBuilder 已抽出章节级构建接口 buildChapter(...),单章构建复用原有 render、分页、尾页规范化、诊断和分页缓存逻辑。
  • RDEPUBReaderPaginationCoordinatortextReflowable 路径已改为两阶段:
    • 第一阶段:按恢复位置优先构建可用章节,并立即应用局部 RDEPUBTextBook 进入阅读器。
    • 第二阶段:后台继续按章节增量构建,但增量结果会先暂存,只在用户空闲时再合并到当前可读内容,最终补齐为完整全书模型。
  • 快速进入阶段不只尝试单个 spine而是按离恢复位置最近的可构建 HTML/XHTML spine 逐个尝试,避免封面、版权页、空白扉页导致首屏快速路径落空。
  • 已修正阅读路径误判:普通静态 SVG 封面不再把整本小说误判为 webInteractive,避免错误掉回 WKWebView 分页路径。
  • 首包策略已调整为“目标章节 + 后续 2 章”,默认先提供 3 章连续可读内容。
  • 后台补齐策略已调整为“每新增 20 章生成一份新的局部结果”,并优先补当前可读窗口之后的章节,再回补前文。
  • 增量结果不再一生成就立即 applyTextBook,而是只保留最近一份 staged RDEPUBTextBook,等用户停止翻页且阅读器回到 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 PageTime To Full Book Ready