# 《凡人修仙传》快速进入阅读器方案 > 适用场景:`textReflowable` 路径打开超大正文 EPUB,典型样本为《凡人修仙传》精校版全本。 > 目标:把“进入阅读器前必须等全书分页完成”改成“先快速可读,再后台补齐全书能力”。 > 结论先行:当前首屏慢的主因不是单章分页太慢,而是 **打开流程要求先完成全书 `RDEPUBTextBookBuilder.build()`**。 --- ## 1. 当前慢在哪里 结合当前代码,打开 reflowable EPUB 的主链路是: ```text RDEPUBReaderController.viewDidLoad -> RDEPUBReaderLoadCoordinator.loadPublication() -> applyParsedPublication(...) -> RDEPUBReaderPaginationCoordinator.paginatePublication() -> if publication.readingProfile == .textReflowable -> RDEPUBTextBookBuilder.build(...) -> 遍历全部 spine item -> 每章 render + paginate -> 汇总成完整 RDEPUBTextBook -> applyTextBook(...) -> readerView.reloadData() -> restoreReadingLocation(...) ``` 关键事实: - [RDEPUBReaderPaginationCoordinator.swift](/Users/shenlei/Work/ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift) 在 `textReflowable` 路径里会先 `showLoading()`,然后后台执行 `builder.build(...)`,构建完成前不会进入正文。 - [RDEPUBTextBookBuilder.swift](/Users/shenlei/Work/ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift) 的 `build()` 会遍历全部 `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](/Users/shenlei/Work/ReadViewSDK/Doc/阅读器功能开发计划.md) 里也已经明确把“增量构建”列为方向,说明这条路和现有架构一致。 换句话说,这不是推翻重做,而是把现有 `RDEPUBTextBookBuilder.build()` 从“必须一次性跑完整本书”拆成“可按章节单独执行”。 --- ## 5. 具体改造点 ## 5.1 构建层:把全书构建拆成章节级能力 当前: - [RDEPUBTextBookBuilder.swift](/Users/shenlei/Work/ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift) 只有全书 `build(...)` 建议新增: ```swift 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` 已经是完整全书 建议新增一个运行时模型,例如: ```swift final class RDEPUBIncrementalTextBookStore ``` 职责: - 保存已完成构建的章节 - 维护 `spineIndex -> chapter` 映射 - 动态生成当前可用的 pages snapshot - 在“部分章节可用”时提供章节内阅读支持 建议暴露能力: - `chapter(forSpineIndex:)` - `availablePagesSnapshot()` - `merge(chapter:)` - `isChapterReady(_:)` - `readyChapterRange(around:)` 原因: - `RDEPUBTextBook` 更适合“完整产物” - 增量加载需要一个“半成品但可读”的状态容器 ## 5.3 分页协调层:把一次性 loading 改成两阶段 loading 当前: - [RDEPUBReaderPaginationCoordinator.swift](/Users/shenlei/Work/ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/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. 首期最小可交付版本 为了最快解决《凡人修仙传》慢启动,建议首期只做下面这些: 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、分页、尾页规范化、诊断和分页缓存逻辑。 - `RDEPUBReaderPaginationCoordinator` 的 `textReflowable` 路径已改为两阶段: - 第一阶段:按恢复位置优先构建可用章节,并立即应用局部 `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,确认本轮快速进入优化可编译: ```text 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`。