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

439 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 《凡人修仙传》快速进入阅读器方案
> 适用场景:`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`