feat: 架构整改 — Context拆分、Runtime拆分、异步章节加载、UI测试覆盖
Phase 1: Context 拆分 - 新增 RDEPUBReaderState/RDEPUBReaderEnvironment/RDEPUBReaderServices - RDEPUBReaderContext 改为过渡门面,代理到 State/Environment/Services Phase 2: Runtime 拆分 - 新增 RDEPUBPresentationRuntime 处理分页状态管理 - 新增 RDEPUBChapterWarmupOrchestrator 处理章节预热与加载编排 - RDEPUBReaderRuntime 从 1277 行收缩,公共 API 转发到新 facade Phase 0.5: 性能优化 - prepareOnDemandChapter 支持异步模式(allowSynchronousLoad: false) - extendPartialBookPageMapIfNeeded 改为 DispatchGroup 并发加载 - RDEPUBChapterOffsetMap.cfiMap 加 NSLock 保护数据竞争 - CFI Map 构建延迟到后台队列(scheduleDeferredCFIMapBuildIfNeeded) - RDEPUBTextPageRenderView 引入静态位图缓存 - RDEPUBTextContentView 新增 loadingSpinner 占位页 Phase 3: 状态机 - 新增 RDEPUBNavigationStateMachine(含 DEBUG 合法转换校验) - 新增 RDEPUBPaginationState 记录分页来源 Review 修复 - makeSummary 重复方法合并 - ensureNavigationTargetAvailable 同步路径加注释标记 UI 测试 - 新增 AsyncChapterLoadingTests(20 个测试,覆盖全部架构整改场景) - 跨章节翻页、延迟 CFI、状态机、内存警告、预加载、位置恢复等
This commit is contained in:
+70
-31
@@ -1,6 +1,6 @@
|
||||
# ReadViewSDK 系统架构文档
|
||||
|
||||
> 最后更新:2026-06-09
|
||||
> 最后更新:2026-06-22
|
||||
|
||||
---
|
||||
|
||||
@@ -201,43 +201,82 @@ struct RDEPUBChapterCacheKey: Hashable {
|
||||
|
||||
---
|
||||
|
||||
## 5. 章节按需加载架构
|
||||
## 5. ReaderController 组合架构
|
||||
|
||||
```
|
||||
RDEPUBReaderController
|
||||
│
|
||||
├─ RDEPUBReaderContext // 共享状态中心
|
||||
│ ├─ parser, publication, readingSession
|
||||
│ ├─ configuration, persistence
|
||||
│ └─ 便捷方法 (renderStyle, layoutConfig, cacheKey)
|
||||
├─ RDEPUBReaderContext(过渡门面)
|
||||
│ ├─ RDEPUBReaderState
|
||||
│ │ ├─ parser / publication / readingSession
|
||||
│ │ ├─ textBook / bookPageMap / pendingFullPageMap
|
||||
│ │ ├─ activeBookmarks / activeHighlights / searchState
|
||||
│ │ └─ currentSelection / paginationToken / snapshot
|
||||
│ │
|
||||
│ ├─ RDEPUBReaderEnvironment
|
||||
│ │ ├─ viewport / safeArea / traitCollection 抽象
|
||||
│ │ ├─ brightness / fallbackViewportSize
|
||||
│ │ └─ renderStyle / layoutConfig 推导
|
||||
│ │
|
||||
│ └─ RDEPUBReaderServices
|
||||
│ ├─ parser / paginator / builder factory
|
||||
│ ├─ renderer factory
|
||||
│ └─ chapter summary disk cache factory
|
||||
│
|
||||
├─ RDEPUBReaderRuntime // 运行时协调器集合(Facade 模式)
|
||||
│ ├─ chapterLoader // 章节加载器
|
||||
│ ├─ chapterRuntimeStore // 内存缓存
|
||||
│ ├─ summaryDiskCache // 磁盘摘要缓存
|
||||
│ ├─ pageResolver // 页码解析器
|
||||
│ ├─ loadCoordinator // 加载协调器
|
||||
│ ├─ paginationCoordinator // 分页协调器
|
||||
│ ├─ locationCoordinator // 位置协调器
|
||||
│ ├─ searchCoordinator // 搜索协调器
|
||||
│ ├─ chromeCoordinator // 工具栏协调器
|
||||
│ ├─ annotationCoordinator // 标注协调器
|
||||
│ └─ viewportMonitor // 视口变化监控
|
||||
│
|
||||
├─ RDEPUBReaderPaginationCoordinator // 分页协调器
|
||||
│ ├─ paginatePublication() // 入口
|
||||
│ ├─ paginateMetadataOnly() // 后台元数据解析
|
||||
│ └─ restoreBookPageMapIfPossible() // 缓存恢复
|
||||
│
|
||||
├─ RDEPUBChapterLoader // 章节加载器
|
||||
│ ├─ loadChapter() // 异步加载(Tier1→Tier2→全量构建)
|
||||
│ └─ loadChapterSynchronouslyForMigration() // 同步加载(快速打开用)
|
||||
│
|
||||
└─ RDEPUBBookPageMap // 轻量页码映射
|
||||
├─ ~100KB/1000章,不持有 NSAttributedString
|
||||
└─ 支持增量刷新 (Builder pattern)
|
||||
└─ RDEPUBReaderRuntime(兼容门面)
|
||||
├─ load / pagination / location / search / chrome / annotation coordinator
|
||||
├─ RDEPUBPresentationRuntime
|
||||
├─ RDEPUBChapterWarmupOrchestrator
|
||||
└─ 旧 API 转发
|
||||
```
|
||||
|
||||
当前 `RDEPUBReaderContext` 仍存在,但主要作用已经收缩为过渡门面:
|
||||
|
||||
- 对外维持兼容访问面
|
||||
- 对内把纯状态、环境推导、工厂依赖拆开
|
||||
- 避免新逻辑继续把 `Context` 当作全能对象扩散
|
||||
|
||||
---
|
||||
|
||||
## 6. 章节按需加载与分页窗口架构
|
||||
|
||||
```
|
||||
RDEPUBReaderController
|
||||
│
|
||||
├─ RDEPUBReaderPaginationCoordinator // 分页入口与后台元数据解析
|
||||
│ ├─ paginatePublication()
|
||||
│ ├─ paginateMetadataOnly()
|
||||
│ └─ restoreBookPageMapIfPossible()
|
||||
│
|
||||
├─ RDEPUBPresentationRuntime // 分页状态与窗口替换
|
||||
│ ├─ applyBookPageMap()
|
||||
│ ├─ refreshBookPageMapInPlace()
|
||||
│ ├─ applyPendingFullPageMapIfNeeded()
|
||||
│ └─ RDEPUBNavigationStateMachine
|
||||
│
|
||||
├─ RDEPUBChapterWarmupOrchestrator // 按需章节预热与边界预取
|
||||
│ ├─ prepareOnDemandChapter()
|
||||
│ ├─ extendPartialBookPageMapIfNeeded()
|
||||
│ ├─ prefetchForwardChaptersAfterInitialOpen()
|
||||
│ └─ ensureNavigationTargetAvailable()
|
||||
│
|
||||
├─ RDEPUBChapterLoader // 章节构建与缓存门面
|
||||
│ ├─ loadChapter() // 异步加载(主路径)
|
||||
│ └─ loadChapterSynchronouslyForMigration()
|
||||
│
|
||||
└─ RDEPUBBookPageMap / RDEPUBPaginationState
|
||||
├─ 当前激活窗口
|
||||
├─ 待接管完整 page map
|
||||
└─ 来源与切换状态
|
||||
```
|
||||
|
||||
这轮整改后,跨章节主路径的关键变化是:
|
||||
|
||||
- 普通翻页时不再要求 UI 主线程同步等待章节构建
|
||||
- 章节边界会前移预热相邻章节与 lookahead 章节
|
||||
- `bookPageMap` 的 partial extension 和 full replacement 统一经过 `RDEPUBPresentationRuntime`
|
||||
- `RDEPUBReaderRuntime` 不再内联维护整套预热/扩窗实现
|
||||
|
||||
---
|
||||
|
||||
## 6. WebView 渲染架构(Web 路径)
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
# ReadViewSDK 架构整改路线图
|
||||
|
||||
> 最后更新:2026-06-22
|
||||
> 适用范围:`Sources/RDReaderView/` 下的 EPUB 阅读器 SDK 主体代码
|
||||
> 目标性质:可执行整改方案,而不是纯讨论文档
|
||||
|
||||
---
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文将当前 SDK 的架构问题整理为一份可逐阶段推进的整改路线图,目标是解决以下五类核心问题:
|
||||
|
||||
1. 共享状态与 UI 环境混杂,导致隐藏依赖过多。
|
||||
2. 运行时协调中心过大,新增需求持续回流到单一大类。
|
||||
3. 章节加载、分页、定位、选区等关键链路存在同步边界与模型重复。
|
||||
4. 阅读容器层、业务控制层、渲染能力层之间的职责边界还不够稳定。
|
||||
5. 用户感知最强的性能问题缺少独立的优先修复阶段。
|
||||
|
||||
本文不追求一次性重写,而强调:
|
||||
|
||||
- 先收口依赖方向
|
||||
- 再拆核心状态与服务
|
||||
- 最后统一抽象与模块边界
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前架构问题摘要
|
||||
|
||||
基于当前代码,主要问题集中在以下对象与层次:
|
||||
|
||||
### 2.1 `RDEPUBReaderContext` 过重
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift`
|
||||
|
||||
当前同时承担:
|
||||
|
||||
- 共享状态容器
|
||||
- UI 环境查询入口
|
||||
- service locator
|
||||
- render/layout 参数推导
|
||||
- controller/runtime 反向跳转
|
||||
|
||||
这会让下层对象表面上只依赖 `context`,实际上隐式依赖整棵 UI 树与运行时生命周期。
|
||||
|
||||
### 2.2 `RDEPUBReaderRuntime` 过大
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderRuntime.swift`
|
||||
|
||||
当前问题:
|
||||
|
||||
- 持有大量 coordinator
|
||||
- 继续承接大量 facade API
|
||||
- 同时编排分页、章节窗口、选区、书签、高亮、设置预览、page map 替换等多领域逻辑
|
||||
|
||||
结果是 runtime 已经成为事实上的架构中心点。
|
||||
|
||||
### 2.3 存在危险同步边界
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift`
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift`
|
||||
|
||||
当前风险:
|
||||
|
||||
- 同步章节加载仍保留在系统内部
|
||||
- 后台线程获取分页尺寸时可能回主线程同步查询
|
||||
- 长链路上仍可能形成“主线程等后台,后台等主线程”的死锁型结构
|
||||
|
||||
### 2.4 分页状态模型重复
|
||||
|
||||
涉及对象:
|
||||
|
||||
- `bookPageMap`
|
||||
- `pendingFullPageMap`
|
||||
- `chapter window snapshot`
|
||||
- `readingSession.activePages/activeChapters`
|
||||
|
||||
当前问题:
|
||||
|
||||
- 多套近似模型并存
|
||||
- takeover/reconciliation 规则分散
|
||||
- 维护“当前阅读窗口”的逻辑被多个对象共同持有
|
||||
|
||||
### 2.5 `RDReaderView` 既是容器又是兼容层
|
||||
|
||||
涉及文件:
|
||||
|
||||
- `Sources/RDReaderView/ReaderView/RDReaderView.swift`
|
||||
- `Sources/RDReaderView/ReaderView/RDReaderViewProtocols.swift`
|
||||
|
||||
当前问题:
|
||||
|
||||
- 同时承载 page curl / scroll / dual-page / preload / tool chrome
|
||||
- 同时兼容 `RDReaderDataSource` 与 `RDReaderPageProvider`
|
||||
- 既做分页容器,又承担历史 API 适配职责
|
||||
|
||||
---
|
||||
|
||||
## 3. 整改原则
|
||||
|
||||
整改过程中遵循以下原则:
|
||||
|
||||
1. 不做一次性全量重写,采用分阶段替换。
|
||||
2. 优先解决依赖方向错误,再解决类过大问题。
|
||||
3. 所有阶段都必须保持 Demo 可运行、SDK 公共 API 尽量兼容。
|
||||
4. 先抽象内部接口,再清理外部旧接口。
|
||||
5. 每个阶段结束时必须有可验证的稳定输出。
|
||||
|
||||
---
|
||||
|
||||
## 4. 总体阶段划分
|
||||
|
||||
建议分为 6 个阶段推进:
|
||||
|
||||
1. Phase 0:建立基线与护栏
|
||||
2. Phase 0.5:优先消除主线程同步加载与跨章节翻页卡顿
|
||||
3. Phase 1:收缩 `Context`,切开环境与状态
|
||||
4. Phase 2:拆分 `Runtime` 与章节加载编排
|
||||
5. Phase 3:统一分页状态模型与导航状态机
|
||||
6. Phase 4:收敛 `ReaderView` 抽象与模块边界
|
||||
|
||||
建议执行顺序不可颠倒。
|
||||
|
||||
原因:
|
||||
|
||||
- 如果不先建立基线,后续性能与结构整改缺少可验证参照。
|
||||
- 如果不先把最强用户痛点独立处理,后续阶段虽然架构更干净,但用户体感改善会滞后。
|
||||
- `Context` 拆分是中期结构整改前置条件,但不是修复同步加载卡顿的前置条件。
|
||||
- 如果不先拆 `Runtime`,分页模型和容器抽象重构会继续回流到 runtime。
|
||||
- 如果不先统一分页状态模型,后续 UI/容器抽象无法稳定。
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 0:建立基线与护栏
|
||||
|
||||
### 5.1 目标
|
||||
|
||||
在重构前建立可回归的技术基线,避免“结构越改越好,但行为逐渐漂移”。
|
||||
|
||||
### 5.2 主要工作
|
||||
|
||||
1. 建立架构整改分支与阶段文档索引。
|
||||
2. 为以下关键链路补 smoke 验证清单:
|
||||
- 打开一本大书
|
||||
- 跨章节连续翻页
|
||||
- 恢复上次阅读位置
|
||||
- 搜索关键字并跳转
|
||||
- 长按选区并添加高亮
|
||||
- 打开目录远跳
|
||||
3. 补充日志观察点:
|
||||
- 章节首次构建耗时
|
||||
- `bookPageMap` 扩展/替换次数
|
||||
- CFI 延迟构建完成次数
|
||||
- 页面静态底图缓存命中率
|
||||
4. 把“禁止 UI 主路径同步章节加载”加成断言或日志告警。
|
||||
5. 补充用户感知最直接的观测指标:
|
||||
- 翻页路径主线程阻塞时长
|
||||
- 跨章节翻页帧稳定性
|
||||
- 预加载命中率
|
||||
|
||||
### 5.3 建议修改文件
|
||||
|
||||
- `Doc/TESTING.md`
|
||||
- `Doc/ARCHITECTURE.md`
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBBackgroundTrace.swift`
|
||||
- 必要时新增轻量调试开关文件
|
||||
|
||||
### 5.4 验收标准
|
||||
|
||||
- 有一份明确的人工回归 checklist
|
||||
- 核心链路能通过现有 Demo 手工验证
|
||||
- 关键性能/状态切换点有结构化日志
|
||||
|
||||
### 5.5 关键观测指标
|
||||
|
||||
Phase 0 结束前,建议至少具备以下指标:
|
||||
|
||||
1. `prepareOnDemandChapter` 主线程 wall clock
|
||||
- 目标:识别是否仍有 UI 主路径同步等待章节构建
|
||||
2. 跨章节翻页时的帧稳定性
|
||||
- 可使用 `os_signpost`、`CADisplayLink` 或简化采样方案
|
||||
- 目标:量化“动画有没有明显掉帧”
|
||||
3. `RDReaderPreloadController.takePreloadedView(for:)` 命中率
|
||||
- 目标:评估预加载是否真的在帮助翻页,而不是名义存在
|
||||
4. `bookPageMap` 扩展/替换频次
|
||||
- 目标:评估分页窗口切换是否过于频繁
|
||||
5. CFI 延迟构建次数与完成耗时
|
||||
- 目标:评估交互增强能力是否被延迟过度
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 0.5:优先消除主线程同步加载与跨章节翻页卡顿
|
||||
|
||||
### 6.1 目标
|
||||
|
||||
在不等待大规模结构拆分的前提下,优先解决用户感知最强的问题:
|
||||
|
||||
- 跨章节翻页时主线程阻塞
|
||||
- 章节边界预加载命中不足
|
||||
- 页面进入时重复重绘开销过高
|
||||
|
||||
这是最高优先级阶段,目标是尽快让用户在大书场景下获得可感知的流畅度提升。
|
||||
|
||||
### 6.2 主要问题链路
|
||||
|
||||
当前高风险链路集中在:
|
||||
|
||||
- `RDEPUBReaderController+DataSource.pageContentView`
|
||||
- `RDEPUBReaderController+DataSource.pageNum`
|
||||
- `RDEPUBReaderRuntime.prepareOnDemandChapter`
|
||||
- `RDEPUBReaderRuntime.extendPartialBookPageMapIfNeeded`
|
||||
- `RDEPUBChapterLoader.loadChapterSynchronouslyForMigration`
|
||||
- `RDReaderPreloadController`
|
||||
- `RDEPUBTextPageRenderView`
|
||||
|
||||
### 6.3 具体工作
|
||||
|
||||
1. 将普通翻页路径上的章节准备改为异步
|
||||
- `prepareOnDemandChapter` 不再在 UI 主路径同步等待章节构建
|
||||
- 允许返回占位页,章节就绪后刷新当前可见内容
|
||||
2. 将 `extendPartialBookPageMapIfNeeded` 改为后台批量加载
|
||||
- 不在 `pageNum` 回调中同步循环加载多个章节
|
||||
- 加载完成后回主线程合并 `bookPageMap`
|
||||
3. 前移跨章节预热
|
||||
- 用户接近章节尾页时提前 lookahead 下一章或下两章
|
||||
- 不等 `pageContentView` 被请求后才开始准备
|
||||
4. 调整预加载半径与策略
|
||||
- `RDReaderPreloadController.radius` 不再固定为章节内相邻 1 页思维
|
||||
- 预加载应感知章节边界,而不只是页号连续性
|
||||
5. 为 `RDEPUBTextPageRenderView` 引入静态内容缓存
|
||||
- 避免页面进入或选区变化时重复完整 CoreText 绘制
|
||||
- 将“静态底图”和“动态选区/交互覆盖”尽量分离
|
||||
6. 明确 `contentMode = .redraw` 的优化策略
|
||||
- 不建议简单把 `contentMode` 改成缩放模式来规避重绘
|
||||
- 建议对静态文本层使用手动位图缓存,必要时再评估 `CATiledLayer`
|
||||
- 动态选区、高亮交互层应保持独立 overlay,避免拖拽选区时重绘整页文本
|
||||
7. 为主线程阻塞建立监控
|
||||
- 重点观察跨章节翻页与恢复定位路径
|
||||
|
||||
### 6.4 建议修改文件
|
||||
|
||||
- `Sources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swift`
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderRuntime.swift`
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift`
|
||||
- `Sources/RDReaderView/ReaderView/Paging/RDReaderPreloadController.swift`
|
||||
- `Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextPageRenderView.swift`
|
||||
- `Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextContentView.swift`
|
||||
|
||||
### 6.5 阶段边界
|
||||
|
||||
本阶段只解决“同步改异步”和“预热/缓存前移”的问题,不强行做大规模类拆分。
|
||||
|
||||
也就是说:
|
||||
|
||||
- 可以调整方法签名
|
||||
- 可以新增轻量状态与回调
|
||||
- 但不以“四服务化拆分 `RDEPUBChapterLoader`”为本阶段目标
|
||||
|
||||
### 6.6 风险点
|
||||
|
||||
- 占位页策略若处理不当,可能从“卡顿”变成“短暂空白”
|
||||
- 异步章节准备如果重复触发,可能导致重复刷新和抖动
|
||||
- `bookPageMap` 合并时若定位恢复策略不稳定,可能引起页码闪跳
|
||||
|
||||
### 6.7 阶段验收
|
||||
|
||||
- 普通翻页主路径不再依赖 UI 主线程同步章节加载
|
||||
- 跨章节翻页体感明显改善
|
||||
- 主线程阻塞监控下降到可接受水平
|
||||
- 预加载命中率比整改前提高
|
||||
- 选区拖拽时不再频繁整页重绘
|
||||
|
||||
---
|
||||
|
||||
## 7. Phase 1:收缩 Context,切开环境与状态
|
||||
|
||||
### 7.1 目标
|
||||
|
||||
把当前 `RDEPUBReaderContext` 从“全能对象”收缩为组合式对象,降低隐藏依赖。
|
||||
|
||||
### 7.2 改造结果
|
||||
|
||||
整改后至少形成三个对象:
|
||||
|
||||
#### `RDEPUBReaderState`
|
||||
|
||||
负责纯运行状态:
|
||||
|
||||
- `parser`
|
||||
- `publication`
|
||||
- `readingSession`
|
||||
- `textBook`
|
||||
- `bookPageMap`
|
||||
- `pendingFullPageMap`
|
||||
- `activeBookmarks`
|
||||
- `activeHighlights`
|
||||
- `searchState`
|
||||
- `currentSelection`
|
||||
|
||||
#### `RDEPUBReaderEnvironment`
|
||||
|
||||
负责 UI 与设备环境:
|
||||
|
||||
- viewport size
|
||||
- safeAreaInsets
|
||||
- traitCollection 抽象
|
||||
- brightness
|
||||
- fallbackViewportSize
|
||||
|
||||
#### `RDEPUBReaderServices`
|
||||
|
||||
负责工厂与外部依赖:
|
||||
|
||||
- parser factory
|
||||
- paginator factory
|
||||
- text renderer factory
|
||||
- text builder factory
|
||||
- persistence
|
||||
- cache repository factory
|
||||
|
||||
### 7.3 具体步骤
|
||||
|
||||
1. 新增文件:
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderState.swift`
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderEnvironment.swift`
|
||||
- `Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderServices.swift`
|
||||
2. 让 `RDEPUBReaderController` 初始化并持有这三个对象。
|
||||
3. `RDEPUBReaderContext` 第一阶段暂时保留,但只作为过渡门面。
|
||||
4. 逐步把这些方法迁出 `Context`:
|
||||
- `currentLayoutContext()`
|
||||
- `currentTextPageSize()`
|
||||
- `currentTextRenderStyle()`
|
||||
- `currentTextLayoutConfig(pageSize:)`
|
||||
- `makeParser()`
|
||||
- `makePaginator()`
|
||||
- `makeTextBookBuilder(...)`
|
||||
5. 移除 `context.runtime` 这种反向访问。
|
||||
6. 下层对象改成显式注入自己真正需要的 state/environment/services。
|
||||
|
||||
### 7.4 优先改造对象
|
||||
|
||||
1. `RDEPUBReaderPaginationCoordinator`
|
||||
2. `RDEPUBReaderLocationCoordinator`
|
||||
3. `RDEPUBChapterLoader`
|
||||
4. `RDEPUBReaderRuntime`
|
||||
|
||||
### 7.5 风险点
|
||||
|
||||
- 迁移过程中容易出现旧 `context` 和新 `state/environment/services` 双写。
|
||||
- 必须在阶段末关闭旧入口,否则后续会继续新增对 `context` 的依赖。
|
||||
|
||||
### 7.6 阶段验收
|
||||
|
||||
- `RDEPUBReaderContext` 不再直接访问 `controller.view`
|
||||
- `RDEPUBReaderContext` 不再暴露 `runtime`
|
||||
- 后台线程不再通过 `context` 回主线程同步取分页尺寸
|
||||
|
||||
---
|
||||
|
||||
## 8. Phase 2:拆分 Runtime 与章节加载编排
|
||||
|
||||
### 8.1 目标
|
||||
|
||||
把当前大而全的 runtime 拆成稳定的领域 facade,并把章节加载逻辑从“对象内联编排”改成“服务化编排”。
|
||||
|
||||
### 8.2 拆分方向
|
||||
|
||||
建议把 `RDEPUBReaderRuntime` 拆为以下 3 类 facade:
|
||||
|
||||
#### `RDEPUBNavigationRuntime`
|
||||
|
||||
负责:
|
||||
|
||||
- 恢复阅读位置
|
||||
- 跳转到页码/目录/高亮/书签
|
||||
- 章节窗口维护
|
||||
- jump session
|
||||
|
||||
#### `RDEPUBPresentationRuntime`
|
||||
|
||||
负责:
|
||||
|
||||
- 分页
|
||||
- `bookPageMap` / `pendingFullPageMap`
|
||||
- viewport monitor
|
||||
- settings preview
|
||||
- 当前快照替换
|
||||
|
||||
#### `RDEPUBAnnotationRuntime`
|
||||
|
||||
负责:
|
||||
|
||||
- 高亮
|
||||
- 批注
|
||||
- 书签
|
||||
- 当前选区
|
||||
- 搜索结果定位联动
|
||||
|
||||
### 8.3 章节加载服务化
|
||||
|
||||
当前 `RDEPUBChapterLoader` 既做缓存命中、章节构建、磁盘写回、延迟 CFI、主线程回调。Phase 2 不建议一开始就强制拆成 4 个服务,而是建议在 Phase 0.5 完成异步化后,根据残余复杂度做渐进式收口。
|
||||
|
||||
优先建议的落地方式是:
|
||||
|
||||
- 保留 `RDEPUBChapterLoader` 作为过渡门面
|
||||
- 先抽出稳定边界最清晰的构建与缓存职责
|
||||
- 把预热、lookahead、延迟 CFI、优先级调度收口到一个协调对象,而不是立即拆成多个小服务
|
||||
|
||||
第一轮更合适的职责拆分可以是:
|
||||
|
||||
#### `RDEPUBChapterBuildService`
|
||||
|
||||
- 输入:spineIndex + render/layout context
|
||||
- 输出:`RDEPUBRuntimeChapter`
|
||||
- 不关心 UI、不关心回调、不关心磁盘缓存
|
||||
|
||||
#### `RDEPUBChapterCacheRepository`
|
||||
|
||||
- 管理内存缓存与磁盘摘要缓存
|
||||
- 提供统一读写接口
|
||||
|
||||
#### `RDEPUBChapterWarmupOrchestrator`
|
||||
|
||||
- 负责预热章节
|
||||
- 负责延迟 CFI 构建
|
||||
- 负责边界 lookahead 预取
|
||||
- 负责导航优先级、预取优先级、取消与串行化策略
|
||||
|
||||
如果后续复杂度继续上升,再考虑把 `WarmupOrchestrator` 继续拆成更细的 service;但这不应作为当前阶段的先决交付物。
|
||||
|
||||
### 8.4 具体步骤
|
||||
|
||||
1. 新增 facade 文件与必要的 service 文件。
|
||||
2. 把 `RDEPUBReaderRuntime` 中的公共 API 先转发到新 facade。
|
||||
3. 再把具体逻辑迁走。
|
||||
4. `RDEPUBChapterLoader` 先收缩为过渡门面,内部优先委派到 build/cache/warmup orchestration。
|
||||
5. `RDEPUBReaderRuntime` 最终只保留一个薄门面,负责兼容旧调用。
|
||||
|
||||
### 8.5 风险点
|
||||
|
||||
- facade 与旧 runtime 并存期较长,容易出现调用路径重复。
|
||||
- 如果服务拆分粒度过细,可能先增加调用跳转和维护成本,收益却不明显。
|
||||
- 章节加载优先级与取消策略若迁移不完整,可能引入新的页面空白或重复构建。
|
||||
|
||||
### 8.6 阶段验收
|
||||
|
||||
- `RDEPUBReaderRuntime.swift` 行数明显下降
|
||||
- 章节加载主逻辑不再集中在单文件
|
||||
- `RDEPUBReaderRuntime` 不再直接操作章节缓存细节
|
||||
|
||||
---
|
||||
|
||||
## 9. Phase 3:统一分页状态模型与导航状态机
|
||||
|
||||
### 9.1 目标
|
||||
|
||||
统一分页窗口模型,并把关键导航链路从“多个 bool/pending 值”升级为显式状态机。
|
||||
|
||||
### 9.2 要解决的问题
|
||||
|
||||
当前并存:
|
||||
|
||||
- `bookPageMap`
|
||||
- `pendingFullPageMap`
|
||||
- `chapter window snapshot`
|
||||
- `readingSession.activePages`
|
||||
|
||||
这些对象都在表达“用户当前能看到什么”,只是层次不同,导致 takeover 和刷新规则分散。
|
||||
|
||||
### 9.3 目标模型
|
||||
|
||||
建议建立统一的分页状态对象,例如:
|
||||
|
||||
```swift
|
||||
struct RDEPUBPaginationState {
|
||||
var activeWindow: RDEPUBPageWindow
|
||||
var candidateFullMap: RDEPUBBookPageMap?
|
||||
var chapterWindowSnapshot: RDEPUBChapterWindowSnapshot?
|
||||
var source: Source
|
||||
}
|
||||
```
|
||||
|
||||
配套引入状态机:
|
||||
|
||||
```swift
|
||||
enum RDEPUBNavigationState {
|
||||
case idle
|
||||
case initialLoading
|
||||
case restoringLocation
|
||||
case preparingChapter(spineIndex: Int)
|
||||
case presentingWindow
|
||||
case reconcilingFullMap
|
||||
case repaginating
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 具体步骤
|
||||
|
||||
1. 新增 `RDEPUBPaginationState.swift`
|
||||
2. 新增 `RDEPUBNavigationStateMachine.swift`
|
||||
3. 把以下逻辑统一收口:
|
||||
- `applyBookPageMap`
|
||||
- `refreshBookPageMapInPlace`
|
||||
- `applyPendingFullPageMapIfNeeded`
|
||||
- `extendPartialBookPageMapIfNeeded`
|
||||
- jump session 覆盖判断
|
||||
4. 所有页码替换、窗口扩展、完整 map takeover 都必须经过统一 evaluator。
|
||||
5. 把当前 scattered bool 收敛:
|
||||
- `isRepaginating`
|
||||
- `didStartInitialLoad`
|
||||
- `isSettingsPanelOpen`
|
||||
- `needsFullRepaginationAfterSettingsClose`
|
||||
|
||||
### 9.5 风险点
|
||||
|
||||
- 这是最容易影响“当前页恢复”和“目录跳转”的阶段。
|
||||
- 必须先保留旧日志与旧行为兜底,再切换状态机入口。
|
||||
|
||||
### 9.6 阶段验收
|
||||
|
||||
- 翻页、远跳、恢复位置、设置变更后 repagination 都走统一状态流
|
||||
- 不再出现多个对象各自判断“是否该替换当前窗口”
|
||||
- `bookPageMap` 与 snapshot 的来源关系更清晰
|
||||
|
||||
---
|
||||
|
||||
## 10. Phase 4:收敛 ReaderView 抽象与模块边界
|
||||
|
||||
### 10.1 目标
|
||||
|
||||
让 `RDReaderView` 重新成为“通用阅读分页容器”,而不是业务与兼容逻辑混合层。
|
||||
|
||||
### 10.2 具体方向
|
||||
|
||||
#### 统一 Provider 协议
|
||||
|
||||
对外保留:
|
||||
|
||||
- `RDReaderPageProvider`
|
||||
|
||||
逐步废弃:
|
||||
|
||||
- `RDReaderDataSource`
|
||||
- `RDReaderLegacyDataSourceAdapter`
|
||||
|
||||
#### 拆分 ReaderView 角色
|
||||
|
||||
建议拆成以下内部组件:
|
||||
|
||||
- `RDReaderPagingSurface`
|
||||
- `RDReaderChromeHost`
|
||||
- `RDReaderInteractionRouter`
|
||||
- `RDReaderPreloadManager`
|
||||
|
||||
#### 收紧模块依赖
|
||||
|
||||
需要形成明确约束:
|
||||
|
||||
- `EPUBCore` 不依赖 `EPUBUI`
|
||||
- `EPUBTextRendering` 不依赖 `UIViewController`
|
||||
- `ReaderView` 不依赖 `RDEPUBReaderController`
|
||||
- `TextPage` 只依赖页面模型与交互协议,不直接访问 controller/runtime
|
||||
|
||||
### 10.3 具体步骤
|
||||
|
||||
1. 先把 `RDEPUBReaderController` 改成只通过 `RDReaderPageProvider` 对接容器。
|
||||
2. 给旧 `RDReaderDataSource` 增加 deprecate 注释。
|
||||
3. 把 tool view、tap routing、单双页布局决策进一步下沉到 ReaderView 内部组件。
|
||||
4. 整理 `ReaderView` 对业务对象的隐式假设。
|
||||
|
||||
### 10.4 风险点
|
||||
|
||||
- 这是 API 层整改,最容易影响 SDK 使用方。
|
||||
- 如果已有外部方直接实现 `RDReaderDataSource`,需要提供过渡期。
|
||||
|
||||
### 10.5 阶段验收
|
||||
|
||||
- `RDEPUBReaderController` 与 `RDReaderView` 之间只通过 page provider 交互
|
||||
- `RDReaderLegacyDataSourceAdapter` 不再是主路径依赖
|
||||
- ReaderView 层可以被描述为“业务无关的分页容器”
|
||||
|
||||
---
|
||||
|
||||
## 11. 配套横向任务
|
||||
|
||||
这些任务建议穿插在各阶段中进行:
|
||||
|
||||
### 11.1 建立缓存协议层
|
||||
|
||||
建议新增:
|
||||
|
||||
- `RDEPUBPageRenderCacheKey`
|
||||
- `RDEPUBPageRenderCacheStore`
|
||||
- `RDEPUBRenderInvalidationPolicy`
|
||||
|
||||
目标:
|
||||
|
||||
- 统一字体/主题/尺寸/highlight/search 的缓存失效规则
|
||||
- 让底图缓存不再散落在 view 内部
|
||||
|
||||
### 11.2 建立定位能力层
|
||||
|
||||
建议新增:
|
||||
|
||||
- `RDEPUBTextAnchorService`
|
||||
- `RDEPUBSelectionLocationService`
|
||||
- `RDEPUBSearchAnchorService`
|
||||
|
||||
目标:
|
||||
|
||||
- 把 CFI、rangeAnchor、fragmentOffset 相关逻辑从 UI 与 text rendering 之间抽离
|
||||
|
||||
### 11.3 建立仓储接口
|
||||
|
||||
建议新增协议:
|
||||
|
||||
- `RDEPUBChapterSummaryRepository`
|
||||
- `RDEPUBPageCountRepository`
|
||||
- `RDEPUBAnnotationRepository`
|
||||
|
||||
目标:
|
||||
|
||||
- 解耦 loader 与具体缓存实现
|
||||
- 便于做测试与未来存储替换
|
||||
|
||||
---
|
||||
|
||||
## 12. 建议执行顺序
|
||||
|
||||
建议按以下顺序创建实施任务:
|
||||
|
||||
1. `phase-0-baseline-and-guardrails`
|
||||
2. `phase-0-5-remove-main-thread-sync-loading`
|
||||
3. `phase-1-context-split`
|
||||
4. `phase-2-runtime-and-chapter-orchestration-split`
|
||||
5. `phase-3-pagination-state-unification`
|
||||
6. `phase-4-reader-view-abstraction-cleanup`
|
||||
7. `horizontal-cache-and-anchor-services`
|
||||
|
||||
原因:
|
||||
|
||||
- Phase 0.5 解决最强用户痛点,且不依赖 `Context` 拆分。
|
||||
- Phase 1 是中期结构整改前置条件,但不是性能解阻塞前置条件。
|
||||
- Phase 2 如果先做,会继续依赖旧 context。
|
||||
- Phase 3 必须建立在 runtime 拆分之后,否则状态机会继续长进 runtime。
|
||||
- Phase 4 应该最后做,避免 UI 容器抽象在业务状态还不稳定时反复返工。
|
||||
|
||||
---
|
||||
|
||||
## 13. 每阶段交付物清单
|
||||
|
||||
### Phase 0
|
||||
|
||||
- 基线文档
|
||||
- smoke checklist
|
||||
- 日志观测点
|
||||
|
||||
### Phase 0.5
|
||||
|
||||
- 异步化的 `prepareOnDemandChapter`
|
||||
- 异步化的 `extendPartialBookPageMapIfNeeded`
|
||||
- 跨章节预热逻辑
|
||||
- 预加载半径与章节边界感知策略调整
|
||||
- `RDEPUBTextPageRenderView` 静态内容缓存
|
||||
- 主线程阻塞监控数据与整改前后对比
|
||||
|
||||
### Phase 1
|
||||
|
||||
- `RDEPUBReaderState`
|
||||
- `RDEPUBReaderEnvironment`
|
||||
- `RDEPUBReaderServices`
|
||||
- 过渡版 `RDEPUBReaderContext`
|
||||
|
||||
### Phase 2
|
||||
|
||||
- runtime facade 拆分
|
||||
- 渐进式的 chapter build/cache/warmup orchestration 收口
|
||||
- 过渡版 `RDEPUBChapterLoader` 门面
|
||||
- 旧 runtime 兼容门面
|
||||
|
||||
### Phase 3
|
||||
|
||||
- `RDEPUBPaginationState`
|
||||
- `RDEPUBNavigationStateMachine`
|
||||
- 统一 takeover/reconciliation 入口
|
||||
|
||||
### Phase 4
|
||||
|
||||
- `RDReaderPageProvider` 成为唯一主协议
|
||||
- ReaderView 组件化
|
||||
- 模块依赖约束文档
|
||||
|
||||
---
|
||||
|
||||
## 14. 验收口径
|
||||
|
||||
整改完成后,至少满足以下口径:
|
||||
|
||||
1. 普通翻页、跨章节翻页、目录远跳、恢复位置不再依赖 UI 主线程同步加载章节。
|
||||
2. 下层服务不再通过 `context` 反向访问 `controller` 或 `runtime`。
|
||||
3. `RDEPUBReaderRuntime` 不再是主要业务实现载体,而是兼容门面。
|
||||
4. 分页状态替换与窗口扩展有单一入口。
|
||||
5. `RDReaderView` 可以独立描述为容器层,不持有明显业务规则。
|
||||
|
||||
---
|
||||
|
||||
## 15. 不建议立即做的事
|
||||
|
||||
以下事项不建议在第一轮整改中做:
|
||||
|
||||
1. 全量改名或移动全部文件目录。
|
||||
2. 直接重写分页系统。
|
||||
3. 直接废弃现有 `readingSession`。
|
||||
4. 一次性删除所有旧 API。
|
||||
5. 在没有回归基线前同时推进多阶段大改。
|
||||
|
||||
这些操作返工风险过高,不适合当前代码体量。
|
||||
|
||||
---
|
||||
|
||||
## 16. 建议下一步
|
||||
|
||||
建议立即启动的实际工作是:
|
||||
|
||||
1. 将本路线图拆成 6 个 phase 文档或 issue。
|
||||
2. 先执行 Phase 0。
|
||||
3. 紧接着执行 Phase 0.5,优先交付用户可感知的翻页流畅度改进。
|
||||
4. Phase 1 只做 `Context` 拆分,不夹带 ReaderView 或分页状态重构。
|
||||
5. 每完成一个 phase,就更新 `Doc/ARCHITECTURE.md`。
|
||||
|
||||
如果需要继续推进,下一份建议产物是:
|
||||
|
||||
- `Doc/PhasePlan/phase-0-5-remove-main-thread-sync-loading.md`
|
||||
|
||||
它将把 Phase 0.5 再拆成更细的文件级改动清单、迁移顺序和验收步骤。
|
||||
+20
-1
@@ -1,6 +1,6 @@
|
||||
# 测试说明
|
||||
|
||||
**分析日期:** 2026-06-09(更新)
|
||||
**分析日期:** 2026-06-22(更新)
|
||||
|
||||
## 测试框架与现状
|
||||
|
||||
@@ -102,6 +102,25 @@ xcodebuild test \
|
||||
|
||||
测试报告输出到 `.artifacts/ui-tests/{timestamp}/UI-Test-Report.md`。
|
||||
|
||||
## 架构整改回归重点
|
||||
|
||||
在 2026-06-22 的阅读器架构整改后,以下场景应作为 smoke 回归最小集合:
|
||||
|
||||
- 打开大书后首次进入阅读页
|
||||
- 章节尾页连续翻页,确认跨章节动画无明显卡顿
|
||||
- 目录远跳到未预热章节,再返回当前阅读流
|
||||
- 关闭并重新打开书籍,恢复上次阅读位置
|
||||
- 长按选区、高亮、批注后再次翻页
|
||||
- 打开设置面板调整字号/间距,观察预览与最终全量 repagination
|
||||
|
||||
建议同时记录以下观测项:
|
||||
|
||||
- `prepareOnDemandChapter` 主线程 wall clock
|
||||
- `RDReaderPreloadController` 预加载命中率
|
||||
- `bookPageMap` partial extension / full replacement 次数
|
||||
- 页面静态底图缓存命中率
|
||||
- CFI 延迟构建完成次数与耗时
|
||||
|
||||
## 覆盖率(Coverage)
|
||||
|
||||
- 当前未启用代码覆盖率收集
|
||||
|
||||
Reference in New Issue
Block a user