ReadViewSDK/Doc/当前阅读器问题修复开发清单.md
shenlei ed390da147 feat: unify reader chrome state management
- Add RDEPUBReaderUIState struct for centralized UI state model
- Refactor RDEPUBReaderChromeCoordinator with makeUIState() and applyUIState()
- Remove direct UI updates from RDEPUBReaderAnnotationCoordinator
- Consolidate updateBookmarkChrome() into updateReaderChrome()
- Update RDEPUBReaderContext, Runtime, and LocationCoordinator
- Add reader problem fix development checklist documentation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-12 17:51:08 +08:00

22 KiB
Raw Blame History

当前阅读器问题修复开发清单

最后更新2026-06-12 依据说明:本清单仅基于当前仓库代码实现整理,不以现有说明文档是否准确为前提。


1. 文档目标

本文将“当前阅读器还存在的问题”展开为可执行的逐任务开发清单,目标是把工作从“方向建议”落到“具体改哪里、怎么改、如何验收”。

本文聚焦以下四类问题:

  1. 核心交互链路不稳定:选区、高亮、书签、工具栏状态分散。
  2. 默认安全策略偏弱:外链、解压、日志、持久化默认行为需要收口。
  3. 资源与分页稳定性不足:大资源内存峰值、离屏分页 WebView 边界控制不足。
  4. SDK 契约与回归手段不足:默认 no-op 容易误用,自动化观测点不够结构化。

2. 总体实施顺序

建议按以下顺序执行,减少返工:

  1. bookmark-chrome-state-unification
  2. selection-state-refactor
  3. external-link-policy-hardening
  4. epub-archive-path-validation
  5. webview-debug-sanitization
  6. persistence-defaults-hardening
  7. resource-scheme-streaming-and-limits
  8. pagination-webview-hardening
  9. cache-hardening
  10. sdk-contract-and-test-hardening

原因:

  • 任务 2 是已确认的重复 UI 状态写入问题,收益最高、风险相对低,应优先处理。
  • 任务 1 更偏“状态模型收口优化”,问题真实存在,但严重度低于任务 2。
  • 第 3-6 项收口默认安全行为,改动局部、收益高。
  • 第 7-9 项涉及资源与分页底层,副作用更大,放在前面稳定后更容易验证。
  • 最后一项负责把前面改动固化为稳定契约和测试基线。

3. 任务清单

任务 1selection-state-refactor

3.1 目标

在不破坏当前清晰分层的前提下,收口选区相关状态的冗余表达,降低 view 层与 controller 层各持有一份选区状态所带来的维护成本和时序问题。

3.2 主要问题代码

  • Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextSelectionController.swift
  • Sources/RDReaderView/EPUBUI/TextPage/RDEPUBTextContentView.swift
  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift
  • Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift

3.3 修改方案

当前代码并不是“混乱散写”,而是相对清晰的分层流:

  • gesture
  • view
  • coordinator
  • context/controller

其中真正值得优化的点主要有两个:

  1. view 层和 controller 层各自持有一份 currentSelection
  2. menuSelection 是为 UIMenuController 时序保底引入的 workaround属于合理存在但可以尝试被更明确的状态模型替代

新增一个统一的选区状态模型仍然是可行方案,例如:

enum RDEPUBSelectionState: Equatable {
    case idle
    case selecting(anchor: Int)
    case selected(RDEPUBSelection)
    case committingAction(RDEPUBSelection, action: RDEPUBAnnotationMenuAction)
}

建议新增文件:

  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBSelectionState.swift

建议在 RDEPUBReaderAnnotationCoordinator 内新增或抽出一个单一入口:

  • applySelectionState(_ state: RDEPUBSelectionState)
  • currentSelectionState

具体修改:

  1. RDEPUBTextSelectionController

    • 移除“自己就是最终状态源”的职责。
    • 保留字符范围计算与 RDEPUBSelection? 构建。
    • isSelecting 可保留为手势内部临时状态,但不再作为 UI 的最终依据。
  2. RDEPUBTextContentView

    • 不再直接通过 currentSelection != nil 作为所有 UI 行为的唯一判断条件。
    • 长按、拖拽、结束只上报“开始选择 / 更新选择 / 结束选择 / 取消选择”事件。
    • menuSelection 不建议直接删除。
    • 第一版应先保留 menuSelection,并把它明确标注为菜单时序缓冲状态。
    • 待状态模型稳定后,再评估是否能安全移除。
  3. RDEPUBReaderAnnotationCoordinator.updateCurrentSelection(_:)

    • 改造成 applySelectionState(_:)
    • .selected 时统一:
      • 更新 controller.currentSelection
      • 决定是否展示工具栏
      • 更新底部高亮按钮可用性
      • 通知 delegate
    • .idle 时统一清理:
      • 清空 controller.currentSelection
      • 关闭菜单
      • 刷新按钮状态
  4. RDEPUBReaderController

    • currentSelection 继续保留为公开只读语义,但底层来源改为 selectionState 推导,而不是多处直接赋值。

3.4 实施步骤

  1. 新增 RDEPUBSelectionState.swift
  2. RDEPUBReaderAnnotationCoordinator 中接管选区状态
  3. 重写 RDEPUBTextContentView 中选区相关回调
  4. 先收口零散的 currentSelection != nil 控制逻辑
  5. 保留 menuSelection,待验证不再需要后再移除
  6. 统一 delegate 通知路径

3.5 风险点

  • 选区菜单弹出时机可能变化。
  • menuSelection 是现有时序补丁,若贸然删除,菜单动作可能拿不到正确 selection。
  • Web 路径和 Native Text 路径都要走统一状态流,避免只修一条。

3.6 验收标准

  • 长按后必然进入“选区中”状态。
  • 松手后有有效文本时必然进入“已选中”状态。
  • 复制/高亮/批注后必然回到空闲状态。
  • selection=1 的 Demo 状态与按钮启用状态始终一致。

3.7 优先级修正

本任务应视为“结构优化 + 降低后续维护成本”,不是当前代码库里最严重的缺陷。建议优先级下调到与任务 2 同级,排在任务 2 之后实施。


任务 2bookmark-chrome-state-unification

3.8 目标

统一顶部书签按钮、底部书签按钮、高亮按钮、添加高亮按钮的可用状态计算,避免不同协调器各自改一部分 UI。

3.9 主要问题代码

  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderChromeCoordinator.swift
  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift
  • Sources/RDReaderView/EPUBUI/RDEPUBReaderTopToolView.swift
  • Sources/RDReaderView/EPUBUI/RDEPUBReaderBottomToolView.swift

3.10 修改方案

新增统一 UI 状态模型:

struct RDEPUBReaderUIState {
    let canToggleBookmark: Bool
    let hasBookmarkAtCurrentLocation: Bool
    let canShowBookmarks: Bool
    let canAddHighlight: Bool
    let canShowHighlights: Bool
}

建议新增文件:

  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderUIState.swift

具体修改:

  1. RDEPUBReaderChromeCoordinator

    • 新增 makeUIState()applyUIState(_:)
    • updateReaderChrome() 内不再直接散写多个 setXxxEnabled
  2. RDEPUBReaderAnnotationCoordinator

    • updateBookmarkChrome() 不再直接写 view改为触发 context.updateReaderChrome()
  3. RDEPUBReaderTopToolView / RDEPUBReaderBottomToolView

    • 保持 dumb view只接收状态不参与规则判断

3.11 实施步骤

  1. 新增 RDEPUBReaderUIState.swift
  2. 改造 updateReaderChrome()
  3. 不要简单删除 AnnotationCoordinator 内的按钮状态刷新
  4. 区分两类触发时机:
    • ChromeCoordinator 负责整页/整轮 chrome 同步
    • AnnotationCoordinator 负责用户操作后的即时反馈触发
  5. 将二者统一收口到同一个状态计算入口,例如 context.updateReaderChrome()
  6. 调整书签新增/删除/跳转后的刷新入口为统一 updateReaderChrome()

3.12 风险点

  • 如果简单删除 AnnotationCoordinator 内的调用,可能丢失“用户操作后立即反馈”的刷新时机。
  • 如果某些按钮当前依赖副作用显示,需要顺便梳理显示时机。

3.13 验收标准

  • 添加第一个书签后,底部书签列表按钮立即可用。
  • 删除最后一个书签后,底部书签列表按钮立即禁用。
  • 当前位置已加书签时,顶部书签按钮始终是选中态。

3.14 目标

把“外链点击后直接打开系统应用”改成受控策略,避免 SDK 默认行为过于激进。

3.15 主要问题代码

  • Sources/RDReaderView/EPUBCore/RDEPUBWebView+JavaScriptBridge.swift
  • Sources/RDReaderView/EPUBUI/RDEPUBReaderController+ContentDelegates.swift
  • Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift

3.16 修改方案

RDEPUBReaderConfiguration 新增:

  • allowedExternalURLSchemes: Set<String>
  • requiresExternalLinkConfirmation: Bool
  • 可选:allowedExternalURLHosts: Set<String>?

默认值建议:

  • scheme 仅允许 https
  • 需要确认框

具体修改:

  1. RDEPUBWebView+JavaScriptBridge.swift

    • 继续识别外链,但不再表示“允许直接打开”。
    • 只负责把外链事件上抛。
  2. RDEPUBReaderController+ContentDelegates.swift

    • 新增私有方法:
      • shouldAllowExternalURL(_:)
      • presentExternalLinkConfirmation(for:)
      • openExternalURLIfAllowed(_:)
    • didActivateExternalLink 改为:
      • 先 delegate 回调
      • 再走配置校验
      • 再弹确认框
      • 最后调用 UIApplication.shared.open
  3. 可选扩展 RDEPUBReaderDelegate

    • 增加可选拦截钩子:
      • epubReader(_:shouldOpenExternalURL:) -> Bool

3.17 实施步骤

  1. 改配置模型与默认值
  2. 改控制器外链处理逻辑
  3. 增加确认对话框
  4. 增加测试用 mock URL 覆盖 httpsmailto、未知 scheme`

3.18 风险点

  • 某些 Demo 书可能依赖 mailtotel,要明确它们现在默认不再直接打开。

3.19 验收标准

  • https 外链在确认后打开。
  • 默认配置下 mailtotel 不直接打开。
  • 未知 scheme 一律拒绝。

任务 4epub-archive-path-validation

3.20 目标

为 EPUB 解压路径增加显式安全校验,防止恶意压缩包通过相对路径写出解压根目录。

3.21 主要问题代码

  • Sources/RDReaderView/EPUBCore/RDEPUBParser+Archive.swift

3.22 修改方案

新增私有方法:

  • validatedExtractionDestination(for:extractionRoot:) -> URL?

校验规则:

  1. 拒绝绝对路径。
  2. 拒绝包含 .. 的路径段。
  3. standardizedFileURL.path 必须位于 extractionRoot.standardizedFileURL.path 下。

具体修改:

  1. extractArchiveIfNeeded(epubURL:)

    • 在循环内先调用 validatedExtractionDestination
    • 无效 entry 可:
      • 直接抛错终止解析,或
      • 记日志并跳过
    • 建议优先抛错,行为更清晰
  2. 增加自定义错误:

    • 若当前 RDEPUBParserError 里没有合适 case可新增 invalidArchiveEntryPath(String)

3.23 实施步骤

  1. 增加路径校验方法
  2. 替换原始 destinationURL 生成逻辑
  3. 增加针对恶意 entry path 的单元测试

3.24 风险点

  • 少数历史 EPUB 可能带奇怪路径分隔符,需要兼容性验证。

3.25 验收标准

  • 正常 EPUB 仍可正常解压。
  • ../ 的恶意 entry 会被拒绝。

任务 5webview-debug-sanitization

3.26 目标

减少默认调试日志泄露阅读内容的风险,并关闭不必要的 inspectable 默认值。

3.27 主要问题代码

  • Sources/RDReaderView/EPUBCore/RDEPUBWebViewDebug.swift
  • Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift
  • Sources/RDReaderView/EPUBCore/RDEPUBWebView+Configuration.swift

3.28 修改方案

  1. RDEPUBWebViewDebug

    • logMessage 改为只打印:
      • message name
      • 字段名列表
      • 文本长度
      • URL 摘要
    • 不再打印完整 message body
  2. 新增更细粒度配置

    • RDEPUBWebViewDebugEnabled
    • RDEPUBVerboseWebViewDebugEnabled
  3. RDEPUBPaginator 与正常阅读 WebView

    • isInspectable 改为受配置控制
    • 默认关闭
  4. RDEPUBReaderConfiguration

    • 增加:
      • allowsInspectableWebViews
      • enablesVerboseWebViewLogging

3.29 实施步骤

  1. 改日志输出格式
  2. 改 inspectable 的默认逻辑
  3. 为 verbose 模式保留调试后门

3.30 风险点

  • 调试某些 JS 问题时信息会变少,因此要保留显式 verbose 开关。

3.31 验收标准

  • 默认 DEBUG 包不输出完整选中文本。
  • 默认分页 WebView 不可 inspect。
  • 打开 verbose 开关后仍能深度调试。

任务 6persistence-defaults-hardening

3.32 目标

降低默认 UserDefaults 持久化的误用风险和数据膨胀风险,同时不破坏当前 API 可用性。

3.33 主要问题代码

  • Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift
  • Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderAnnotationCoordinator.swift
  • Sources/RDReaderView/EPUBUI/RDEPUBReaderController.swift

3.34 修改方案

短期目标:

  1. 明确 RDEPUBUserDefaultsPersistence 是轻量实现。
  2. 高亮持久化优先依赖:
    • location
    • rangeInfo
    • style
    • note
  3. text 字段作为冗余缓存,而不是唯一恢复依据。

中期可选目标:

新增文件持久化实现:

  • Sources/RDReaderView/EPUBUI/RDEPUBFilePersistence.swift

3.35 具体修改

  1. RDEPUBReaderPersistence.swift

    • 为默认实现增加 DEBUG 警告,提示 bookmarks/settings 默认实现是 no-op
  2. RDEPUBUserDefaultsPersistence

    • 增加大小保护,例如当高亮集合序列化后超阈值时打印告警
    • 可选增加版本字段,便于后续迁移
  3. RDEPUBReaderController

    • 初始化默认 persistence 时,在注释与命名上明确这是默认轻量实现

3.36 实施步骤

  1. 增加 warning/assert 机制
  2. 优化高亮持久化字段策略
  3. 视时间增加 RDEPUBFilePersistence

3.37 风险点

  • 如果外部代码依赖 text 永远存在,需要做好兼容。

3.38 验收标准

  • 未实现书签持久化时DEBUG 下能看到明确提示。
  • 高亮即使 text 丢失,也可基于范围信息恢复定位。

任务 7resource-scheme-streaming-and-limits

3.39 目标

降低 WKURLSchemeHandler 对大资源的内存冲击,避免每次都整块 Data(contentsOf:) 读入。

3.40 主要问题代码

  • Sources/RDReaderView/EPUBCore/RDEPUBResourceURLSchemeHandler.swift

3.41 修改方案

分两步做:

第一步,先加大小阈值和告警:

  • 读取文件属性获取大小
  • 超过阈值时走分块发送

第二步,再补流式发送:

  • 使用 FileHandleInputStream
  • 按固定 chunk size 反复 didReceive(data)

建议新增私有方法:

  • resourceMetadata(for:)
  • respondWithStreaming(fileURL:requestURL:taskID:urlSchemeTask:)
  • respondWithInMemoryData(...)

3.42 实施步骤

  1. 先加文件大小检测
  2. 小文件保留内存直读
  3. 大文件切换到分块发送
  4. 增加取消任务时的中断判断

3.43 风险点

  • WKURLSchemeHandler 分块发送要注意 stop 之后不继续回调。

3.44 验收标准

  • 大图片/字体加载时内存峰值下降。
  • 取消任务不会继续发送数据。

任务 8pagination-webview-hardening

3.45 目标

加强离屏分页 WebView 的边界控制,减少外部资源波动和调试副作用。

3.46 主要问题代码

  • Sources/RDReaderView/EPUBCore/RDEPUBPaginator.swift

3.47 修改方案

  1. 为分页 WebView 增加严格导航限制

    • 仅允许 file://
    • 仅允许 ss-reader://
    • 拒绝 http/https
  2. isInspectable 受配置控制

  3. 增加测量稳定性日志

    • 记录三轮测量结果
    • 当三轮差异过大时告警

建议新增私有方法:

  • shouldAllowPaginatorNavigation(url:)
  • recordMeasurement(pass:value:)

3.48 实施步骤

  1. 扩展 WKNavigationDelegate
  2. 加入导航 allowlist
  3. 记录多轮测量差异

3.49 风险点

  • 某些 EPUB 样式若依赖外链资源,分页结果可能与当前行为不同,但这本身就是更安全的策略。

3.50 验收标准

  • 分页 WebView 不发起外部导航。
  • 三轮测量波动可观测。

任务 9cache-hardening

3.51 目标

提升章节摘要磁盘缓存的健壮性、可观测性和可清理能力。

3.52 主要问题代码

  • Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterSummaryDiskCache.swift

3.53 修改方案

  1. 原子写入

    • 先写 .tmp
    • 再 replace 到正式文件
  2. 读失败分类

    • 文件不存在
    • 读取失败
    • 解码失败
  3. 精细化清理

    • removeAll()
    • removeAll(forBookID:)
    • removeAll(forRenderSignature:)
  4. 增加统计接口

    • 缓存文件数
    • 总大小

3.54 实施步骤

  1. 改写入策略
  2. 增加读失败日志
  3. 增加清理接口
  4. 补测试覆盖损坏文件、半写文件

3.55 风险点

  • 清理策略接口若公开,需要评估是否作为 public API。

3.56 验收标准

  • 缓存文件损坏不会影响整本书重新打开。
  • 能按需清理缓存,而不是只能全删。

任务 10sdk-contract-and-test-hardening

3.57 目标

把前面所有修复固化为更清晰的 SDK 契约和更稳定的测试观测点。

3.58 主要问题代码

  • Sources/RDReaderView/EPUBUI/RDEPUBReaderPersistence.swift
  • Sources/RDReaderView/EPUBUI/RDURLReaderController.swift
  • ReadViewDemo/ReadViewDemoUITests/

3.59 修改方案

  1. 强化 persistence 契约

    • 继续保留默认实现,但在 DEBUG 下对 no-op 行为给出明确信号
  2. Demo 状态能力修正

    • 当前仓库并非完全没有结构化状态。
    • ReadViewDemo/ReadViewDemoUITests/Helpers/DemoReaderState.swift 已经提供了状态解析器和 waitForDemoReaderState(...) 结构化等待能力。
    • 真正需要推进的不是再造一个 DemoReaderSnapshot,而是把仍然大量存在的 waitForReaderState(containing:) 迁移到结构化断言。

建议方向:

  • 继续保留隐藏 label 输出
  • 保持 key=value 格式
  • 统一测试侧只通过 DemoReaderState 解析和断言
  • 逐步淘汰 containing: 子串匹配,避免 highlights=1 命中 highlights=10 之类误判
  1. 补测试
    • 单元测试:
      • RDEPUBParserArchiveSafetyTests
      • RDEPUBExternalLinkPolicyTests
      • RDEPUBChapterSummaryDiskCacheTests
    • UI 测试:
      • 选区状态流
      • 书签启用/禁用
      • 外链确认框

3.60 实施步骤

  1. 改 Demo 状态串生成逻辑
  2. 优先将 waitForReaderState(containing:) 迁移为 waitForDemoReaderState(...)
  3. 调整 UI 测试断言
  4. 增加单元测试 target 或现有测试目录内的新文件

3.61 风险点

  • 现有 UI 测试如果强依赖旧状态串,需要同步迁移。

3.62 验收标准

  • UI 测试失败时能明确知道是选区状态、书签状态还是导航状态失配。
  • persistence 的默认 no-op 行为在开发期不再“静默”。

4. 补充清理项

以下问题已在代码中可见,但未纳入前 10 个主任务,建议作为补充任务或穿插清理项处理。

4.1 pageBreakBefore/pageBreakAfter 相关死代码审计

涉及代码:

  • Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRenderer.swift
  • Sources/RDReaderView/EPUBTextRendering/Typesetter/RDEPUBSemanticMarkerInjector.swift
  • Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBPageBreakPolicy.swift
  • Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift

建议动作:

  • 审计 pageBreakBefore/pageBreakAfter 从注入到消费的完整链路
  • 判断是否有“已定义、已注入、但实际无有效触发”的死路径
  • 若确认无效,删除或补测试锁定其真实行为

4.2 PAGINATION-DEBUG 输出清理

涉及代码:

  • Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBCoreTextPageFrameFactory.swift
  • Sources/RDReaderView/EPUBTextRendering/Pagination/RDEPUBChapterPageCounter.swift

建议动作:

  • 将裸 print 改为受控调试开关
  • 默认关闭
  • 避免分页细节和正文片段直接进入控制台

4.3 ChapterRuntime 旧代码与遗留路径清理

建议动作:

  • 梳理 ChapterRuntime 下是否存在已不再被主流程使用的辅助类型或过渡实现
  • 对“仍被引用但语义已经过时”的代码先补注释标记
  • 对“完全无调用”的代码建立删除候选清单

5. 推荐里程碑切分

里程碑 A核心交互稳定

包含任务:

  • selection-state-refactor
  • bookmark-chrome-state-unification

完成标准:

  • 选区、高亮、书签相关 UI 测试恢复稳定

里程碑 B默认安全行为收口

包含任务:

  • external-link-policy-hardening
  • epub-archive-path-validation
  • webview-debug-sanitization
  • persistence-defaults-hardening

完成标准:

  • 默认配置下不再直接打开高风险外链
  • 解压路径具备显式校验
  • 默认调试日志不泄露完整阅读内容

里程碑 C底层稳定性优化

包含任务:

  • resource-scheme-streaming-and-limits
  • pagination-webview-hardening
  • cache-hardening

完成标准:

  • 大资源加载和分页更稳定
  • 缓存更健壮、可清理

里程碑 D契约与测试固化

包含任务:

  • sdk-contract-and-test-hardening

完成标准:

  • SDK 默认行为更清晰
  • 回归测试对关键状态具备稳定观测能力

6. 建议的开发节奏

若按 2 周一个小迭代,可参考:

  1. 第 1 周:bookmark-chrome-state-unification + selection-state-refactor
  2. 第 2 周:external-link-policy-hardening + epub-archive-path-validation + webview-debug-sanitization
  3. 第 3 周:persistence-defaults-hardening + resource-scheme-streaming-and-limits + pagination-webview-hardening
  4. 第 4 周:cache-hardening + sdk-contract-and-test-hardening 与回归收口

如果只想先做最值的部分,建议最小交付集合为:

  1. bookmark-chrome-state-unification
  2. selection-state-refactor
  3. external-link-policy-hardening
  4. epub-archive-path-validation

这四项完成后,阅读器的“主观可用性”和“默认安全性”会先提升一个台阶。