源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
618 lines
25 KiB
Markdown
618 lines
25 KiB
Markdown
# 阅读器功能开发计划
|
||
|
||
> 本文档整合了渲染质量三方对比(ReadViewSDK vs WXRead)与功能开发计划,作为阅读器能力演进的单一真值。
|
||
> 基于读书 v10.0.3 逆向分析,与当前 ReadViewSDK 代码核查结果整合。
|
||
|
||
## 背景
|
||
|
||
渲染内核的架构对齐解决的是"代码可维护性"问题。本文档梳理从"能用的阅读器"到"能上架的商业阅读器"还需要补齐哪些能力,每项能力标注三方状态和具体实施计划。
|
||
|
||
### 三方对比总览(渲染质量)
|
||
|
||
| 缺失项 | 严重度 | ReadViewSDK | WXRead | 差距说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| **竖排文字** | 高 | ❌ 未实现 | ❌ 未实现 | 双方均无 `writing-mode` 支持 |
|
||
| **Ruby 注音** | 高 | ❌ 未实现 | ❌ 未实现 | 双方均无 `<ruby>`/`<rt>` 处理 |
|
||
| **数学公式** | 中 | ❌ 未实现 | ⚠️ 仅字体回退 | 非原生渲染范畴,需 WebView 回退 |
|
||
| **复杂图文分页质量** | 高 | ✅ 已实现 | ✅ 已实现 | 双方均已实现 |
|
||
| **字体选择器** | 中 | ✅ 已实现 | ⚠️ 管线完备,UI 未见 | 已支持四档字体选择 |
|
||
| **连字/断字** | 中 | ⚠️ 仅配置标记 | ⚠️ 仅属性声明 | 双方均未实际实现 |
|
||
| **多语言排版回退** | 中 | ⚠️ 语言检测有,简繁转换无 | ✅ 已实现 | WXRead 额外有简繁转换 |
|
||
|
||
### ReadViewSDK 领先 WXRead 的项
|
||
|
||
| 项 | 说明 |
|
||
| --- | --- |
|
||
| **`keepWithNext`** | 已实现 `trimmedRangeForKeepWithNext`,WXRead 反编译代码中未找到对应实现 |
|
||
|
||
### 不需要对标 WXRead 的项
|
||
|
||
以下项 WXRead 自身也未实现,不属于必须补齐的能力:竖排文字、Ruby 注音、数学公式、Hyphenation 断字。
|
||
|
||
---
|
||
|
||
## 一、渲染质量
|
||
|
||
### 1. 字体选择器
|
||
|
||
**当前状态**:✅ 基础能力已完成。
|
||
|
||
已支持系统、宋体、圆体、等宽四档字体选择;设置项可持久化;切换字体会触发重新分页;分页缓存签名已包含字体信息,避免不同字体复用旧缓存。
|
||
|
||
**已落地文件清单**:
|
||
|
||
| 文件 | 改动 | 状态 |
|
||
| --- | --- | --- |
|
||
| `RDEPUBReaderConfiguration.swift` | 新增 `RDEPUBReaderFontChoice`,配置增加 `fontChoice` | 已完成 |
|
||
| `RDEPUBReaderSettings.swift` | 新增 `fontChoice` 持久化,更新 `applying(to:)` 和 `capture(configuration:brightness:)` | 已完成 |
|
||
| `RDEPUBReaderContext.swift` | `currentTextRenderStyle()` 改用 `configuration.fontChoice.font(ofSize:)` | 已完成 |
|
||
| `RDURLReaderController.swift` | URL 阅读器同步使用 `fontChoice` 生成文字样式 | 已完成 |
|
||
| `RDEPUBReaderController+RuntimeBridge.swift` | `requiresRepagination(from:to:)` 增加 `fontChoice` 变更检查 | 已完成 |
|
||
| `RDEPUBPaginationCacheCoordinator.swift` | 分页缓存签名增加字体名 | 已完成 |
|
||
| `RDEPUBReaderSettingsViewController.swift` | 新增 `onFontChoiceChange` 回调、字体分段控件和 accessibility identifier | 已完成 |
|
||
| `RDEPUBReaderChromeCoordinator.swift` | 设置面板接线字体变更回调 | 已完成 |
|
||
| `SettingsPanelTests.swift` | UI 自动化覆盖字体选择 | 已完成 |
|
||
|
||
**当前验收结果**:
|
||
- ✅ 切换字体后触发重排。
|
||
- ✅ 字体选择可随阅读器设置持久化。
|
||
- ✅ 分页缓存按字体隔离。
|
||
- ✅ UI 自动化已覆盖设置面板字体选择。
|
||
|
||
**后续增强计划**:
|
||
1. 引入更多内置字体包(建议:思源宋体、思源黑体、方正书宋、方正兰亭黑、Lora、OpenDyslexic)。
|
||
2. 使用 `CTFontManagerRegisterFontsForURL` 注册 bundle 字体。
|
||
3. 字体列表从枚举升级为资源驱动模型,每项包含 `displayName`、`fontName`、`previewText`、可用性状态。
|
||
4. 字体选择 UI 从分段控件升级为横向预览卡片,展示真实字体效果。
|
||
5. 增加字体资源加载失败兜底,失败时回退系统字体并保留用户设置。
|
||
|
||
---
|
||
|
||
### 2. 暗色模式图片处理
|
||
|
||
**当前状态**:✅ 基础能力已完成。
|
||
|
||
暗色主题下会对正文图片做显示副本调暗,降低白底图片在深色背景上的刺眼程度。处理只作用于展示层,不修改分页原始内容;封面和小图会跳过,避免误处理封面、图标和装饰图。
|
||
|
||
**已落地文件清单**:
|
||
|
||
| 文件 | 改动 | 状态 |
|
||
| --- | --- | --- |
|
||
| `RDEPUBReaderConfiguration.swift` | 新增 `darkImageAdjustmentEnabled` 和 `darkImageBlendRatio` | 已完成 |
|
||
| `RDEPUBReaderController+RuntimeBridge.swift` | 主题和暗色图片配置变更触发可见页刷新 | 已完成 |
|
||
| `RDEPUBTextContentView.swift` | DTCoreText 展示内容增加暗色图片显示副本处理 | 已完成 |
|
||
| `RDEPUBTextContentView.swift` | 增加暗色图片缓存,避免重复生成 | 已完成 |
|
||
| `SettingsPanelTests.swift` | UI 自动化覆盖暗色主题切换 | 已完成 |
|
||
|
||
**当前实现策略**:
|
||
- 仅当背景亮度低于阈值、配置开启、混合比例大于 0 时启用。
|
||
- 仅处理正文 inline image attachment。
|
||
- 跳过封面和小于 80x80 的图片。
|
||
- 使用 `NSCache` 缓存处理后的图片。
|
||
- 使用主题背景色按比例覆盖原图,透明区域保持透明。
|
||
|
||
**当前验收结果**:
|
||
- ✅ 暗色模式下图片不再完全以原始亮色块直出。
|
||
- ✅ 图片内容仍保持可辨认,不做反色。
|
||
- ✅ 处理结果有缓存。
|
||
- ✅ UI 自动化已覆盖暗色主题入口。
|
||
|
||
**后续增强计划**:
|
||
1. 增加真实 EPUB 图片样本的截图回归测试。
|
||
2. 按图片平均亮度自适应 `darkImageBlendRatio`。
|
||
3. 增加用户侧开关,允许关闭暗色图片处理。
|
||
4. 如果后续恢复 WebView 路径,再补充 CSS filter 策略。
|
||
|
||
---
|
||
|
||
### 3. 简繁转换
|
||
|
||
**当前状态**:零实现。语言元数据已提取(`publication.metadata.language`),但仅用于拉丁/CJK CSS 分轨。
|
||
|
||
**WXRead 实现参考**:
|
||
- `WREpubTypesetter` 检测 `book.language` 含 `Hant`/`TW`/`HK` 时调用 `_WRConvertHansToHantIfNeeded()`
|
||
- 使用 `CFStringTransform` 两步转换:Hans → Latin → Hant
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:转换工具**
|
||
1. 新增 `RDEPUBTextChineseConverter.swift`,提供:
|
||
```swift
|
||
enum RDEPUBChineseScript { case hans, hant }
|
||
|
||
func convert(_ text: String, to script: RDEPUBChineseScript) -> String
|
||
```
|
||
2. 实现使用 `CFStringTransform`:
|
||
```swift
|
||
let mutable = NSMutableString(string: text) as CFMutableString
|
||
CFStringTransform(mutable, nil, kCFStringTransformToLatin, false) // Hans → Pinyin
|
||
CFStringTransform(mutable, nil, kCFStringTransformStripDiacritics, false) // Pinyin → stripped
|
||
// 然后通过字典映射到繁体
|
||
```
|
||
或直接使用 Apple 的 `kCFStringTransformHansToHant`(如果可用)。
|
||
|
||
**Step 2:判断是否需要转换**
|
||
3. 在 `RDEPUBTextBookBuilder.build()` 或 `RDEPUBTextRendererSupport.makeChapterRenderRequest()` 中:
|
||
- 读取 `publication.metadata.language`
|
||
- 如果语言为 `zh-Hant`/`zh-TW`/`zh-HK` 且用户设置为简体,或语言为 `zh-Hans`/`zh-CN` 且用户设置为繁体,触发转换
|
||
4. 新增 `RDEPUBReaderConfiguration.chineseScript: RDEPUBChineseScript?`(nil = 跟随书籍)
|
||
|
||
**Step 3:应用转换**
|
||
5. 在 `normalizeReadingAttributes(in:style:)` 之后、返回 `RDEPUBTextChapterRenderRequest` 之前,对 attributed string 的文本内容执行转换
|
||
6. 需要保持 attributed string 的属性(字体、颜色、附件)不变,只替换字符
|
||
|
||
**验收标准**:
|
||
- 简体 EPUB 在繁体模式下显示繁体
|
||
- 繁体 EPUB 在简体模式下显示简体
|
||
- 转换不影响分页结果(转换后字符数可能变化,需验证)
|
||
- 高亮、搜索功能在转换后仍然正常
|
||
|
||
---
|
||
|
||
### 4. 孤行/寡行控制
|
||
|
||
**当前状态**:`RDEPUBTextLayoutConfig` 声明了 `avoidOrphans`/`avoidWidows`(默认 true),但 `RDEPUBTextLayouter` 完全不读取这两个标记。
|
||
|
||
**实现原理**:
|
||
- **寡行(widow)**:段落最后一行单独出现在页底 → 应将该行拉到下一页
|
||
- **孤行(orphan)**:段落第一行单独出现在页首 → 应将前一页最后一行拉过来
|
||
- 通常只处理寡行(保留至少 2 行在页底),孤行处理会引发连锁重排
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:CoreText 路径**
|
||
1. 在 `RDEPUBTextLayouter.layoutFramesUsingCoreText()` 的分页循环中(~行 63-137),在 `adjustedRange` 之后、追加 frame 之前,增加寡行检查:
|
||
```swift
|
||
if config.avoidWidows {
|
||
finalRange = trimmedRangeForAvoidWidows(
|
||
from: ctFrame,
|
||
proposed: finalRange,
|
||
lineRanges: lineRanges
|
||
)
|
||
}
|
||
```
|
||
2. `trimmedRangeForAvoidWidows` 实现:
|
||
- 从 proposed range 的最后一行向前检查
|
||
- 如果最后一行是一个段落的最后一行,且该段落在此页只有 1 行 → 回退到上一个段落边界
|
||
- 最多移除 2 行(避免过度收缩)
|
||
|
||
**Step 2:DTCoreText 路径**
|
||
3. 在 `layoutFramesUsingDTCoreText()` 中增加相同逻辑
|
||
|
||
**Step 3:段落边界检测**
|
||
4. 利用已有的 `paragraphRange(containing:)` 方法(`NSString.paragraphRange`)获取段落范围
|
||
5. 对比当前页最后一行的 range 和段落的 range,判断是否为段落唯一一行
|
||
|
||
**验收标准**:
|
||
- 分页页数可能轻微增加(1-3 页),但排版质量提升
|
||
- 已知的寡行问题页在修复后不再出现段落最后一行孤悬页底
|
||
- `avoidOrphans`/`avoidWidows = false` 时行为不变
|
||
|
||
---
|
||
|
||
### 5. Hyphenation 断字
|
||
|
||
**当前状态**:`hyphenation: Bool = true` 配置标记已声明,但 `NSParagraphStyle.hyphenationFactor` 从未设置。CoreText 路径更不支持断字。
|
||
|
||
**技术约束**:
|
||
- `NSParagraphStyle.hyphenationFactor` 对 `UITextView`(降级路径)有效
|
||
- CoreText 直绘路径需要设置 `kCTParagraphStyleSpecifierHyphenationFactor`
|
||
- 中文场景断字影响较小(中文无空格分词),主要改善西文排版
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:UITextView 降级路径**
|
||
1. 在 `RDEPUBTextRendererSupport.paragraphStyle(lineSpacing:)`(行 929-934)中增加:
|
||
```swift
|
||
style.hyphenationFactor = 1.0
|
||
```
|
||
2. 在 `normalizeReadingAttributes()`(行 137-143)中,对已有的 `NSMutableParagraphStyle` 增加:
|
||
```swift
|
||
paragraphStyle.hyphenationFactor = 1.0
|
||
```
|
||
|
||
**Step 2:CoreText 直绘路径**
|
||
3. 在 `RDEPUBTextLayouter` 的 frame 构造中,对 `CTParagraphStyle` 增加 hyphenation factor:
|
||
```swift
|
||
var hyphenationFactor: Float = 1.0
|
||
let settings = [CTParagraphStyleSetting(
|
||
spec: .hyphenationFactor,
|
||
valueSize: MemoryLayout<Float>.size,
|
||
value: &hyphenationFactor
|
||
)]
|
||
let paragraphStyle = CTParagraphStyleCreate(settings, settings.count)
|
||
```
|
||
4. 将此 paragraph style 设置到 attributed string 的段落属性上
|
||
|
||
**Step 3:条件控制**
|
||
5. 读取 `config.hyphenation` 标记,为 false 时跳过上述设置
|
||
|
||
**验收标准**:
|
||
- 西文长单词在行末正确断字(显示连字符)
|
||
- 中文排版不受影响
|
||
- `hyphenation = false` 时回到当前行为
|
||
|
||
---
|
||
|
||
## 二、工程成熟度
|
||
|
||
### 6. 自动化测试
|
||
|
||
**当前状态**:✅ UI 自动化测试基础设施已完成,单元测试和分页回归基准待补。
|
||
|
||
已在 Demo 工程中接入 `ReadViewDemoUITests`,覆盖打开书籍、阅读器基础交互、顶部/底部工具栏、设置面板、字体选择和暗色主题切换。
|
||
|
||
**已落地内容**:
|
||
|
||
| 项 | 状态 |
|
||
| --- | --- |
|
||
| UI Test target `ReadViewDemoUITests` | ✅ 已完成 |
|
||
| Demo 自动打开测试 EPUB 的入口 | ✅ 已完成 |
|
||
| Accessibility identifier 体系 | ✅ 已完成 |
|
||
| 阅读器基础打开测试 | ✅ 已完成 |
|
||
| 顶部/底部工具栏测试 | ✅ 已完成 |
|
||
| 设置面板测试 | ✅ 已完成 |
|
||
| 字体选择 UI 测试 | ✅ 已完成 |
|
||
| 暗色主题切换 UI 测试 | ✅ 已完成 |
|
||
|
||
**当前验证结果**:
|
||
|
||
```text
|
||
xcodebuild build: BUILD SUCCEEDED
|
||
SettingsPanelTests: TEST SUCCEEDED
|
||
ReadViewDemoUITests 全量 8 个 UI 测试: TEST SUCCEEDED
|
||
```
|
||
|
||
**下一步实施步骤**:
|
||
|
||
**Step 1:单元测试基础设施**
|
||
1. 在 `ReadViewDemo.xcodeproj` 中新增 Unit Test target `ReadViewSDKTests`
|
||
2. 创建 `Tests/` 目录,配置 `import RDReaderView` 或通过 Demo target 暴露内部测试入口
|
||
3. 新增 `.xctestplan` 配置,把 UI 测试和单元测试拆分为不同测试组
|
||
|
||
**Step 2:分页回归测试(最高优先级)**
|
||
4. 新增 `RDEPUBPaginationRegressionTests.swift`,固定 6 类样本章节:
|
||
- 纯正文章节
|
||
- 图 + 图注 + 正文章节
|
||
- 脚注小图标章节
|
||
- 多段标题章节
|
||
- 大图跨页章节
|
||
- 长段落连续章节
|
||
5. 每个样本:构建 `RDEPUBTextBook`,断言总页数不变、已知页的 `contentRange` 不变
|
||
6. 使用 golden file 对比 `RDEPUBTextChapterPaginationDiagnostic` 输出
|
||
|
||
**Step 3:位置映射测试**
|
||
7. 新增 `RDEPUBLocationMappingTests.swift`:
|
||
- `location → pageNumber` 正向映射
|
||
- `pageNumber → location` 逆向映射
|
||
- 往返一致性断言
|
||
|
||
**Step 4:渲染管线测试**
|
||
8. 新增 `RDEPUBTypesetterTests.swift`:
|
||
- `normalizeHTML` 输入输出对比
|
||
- fragment marker 注入/提取一致性
|
||
- 语义标记注入后 `rdPage*` 属性计数不变
|
||
|
||
**Step 5:CI(可选)**
|
||
9. 配置 GitHub Actions 或 Jenkins,在 PR 时自动运行测试
|
||
|
||
**验收标准**:
|
||
- UI 自动化全量测试稳定通过
|
||
- 6 个分页基准样本的页数和 contentRange 稳定
|
||
- 位置映射往返测试通过
|
||
- 新 PR 触发测试自动运行
|
||
|
||
---
|
||
|
||
### 7. 性能基线
|
||
|
||
**当前状态**:`RDEPUBTextPerformanceSampler` 用 `CFAbsoluteTimeGetCurrent()` 采样,结果仅 `print()` 输出。无 Instruments 可视化、无内存监控、无阈值告警。
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:os_signpost 集成**
|
||
1. 在 `RDEPUBTextPerformanceSampler` 中引入 `os.signpost`:
|
||
```swift
|
||
import os.signpost
|
||
let log = OSLog(subsystem: "com.rdreader", category: .pointsOfInterest)
|
||
```
|
||
2. 在 build/render/paginate 各阶段插入 `os_signpost(.begin, ...)` / `os_signpost(.end, ...)`
|
||
3. 这样 Instruments 的 Points of Interest 能直接可视化各阶段耗时
|
||
|
||
**Step 2:内存监控**
|
||
4. 新增 `RDEPUBMemoryMonitor`:
|
||
```swift
|
||
func currentMemoryFootprint() -> UInt64 // task_info.resident_size
|
||
```
|
||
5. 在 `RDEPUBTextBookBuilder.build()` 的每章循环中记录内存峰值
|
||
6. 在 `RDEPUBTextPerformanceSample` 中增加 `peakMemoryBytes: UInt64`
|
||
|
||
**Step 3:阈值告警**
|
||
7. 定义基准值:首屏 < 2s、单章渲染 < 500ms、全书构建 < 30s(按书的大小可调)
|
||
8. 超出阈值时通过 `os_log(.error, ...)` 输出警告
|
||
9. 在 Demo 中展示性能摘要面板
|
||
|
||
**Step 4:持久化**
|
||
10. 性能样本可选持久化到文件,供回归对比
|
||
|
||
**验收标准**:
|
||
- Instruments 能看到各阶段的 signpost 区间
|
||
- 内存峰值在大书(79MB 样本)构建过程中有记录
|
||
- 超出阈值时有日志输出
|
||
|
||
---
|
||
|
||
### 8. 崩溃防护
|
||
|
||
**当前状态**:`RDReaderView` 有 pageCurl 崩溃检测和异步恢复,但无全局异常捕获。
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:NSException 捕获**
|
||
1. 新增 `RDEPUBCrashGuard` 工具类:
|
||
```swift
|
||
static func performSafely(_ block: () throws -> Void) rethrows
|
||
static func performWithObjCExceptionHandling(_ block: () -> Void) -> Bool
|
||
```
|
||
2. 在关键入口包裹 `@try/@catch`:
|
||
- `RDEPUBTextBookBuilder.build()` 的每章循环体
|
||
- `RDEPUBTextLayouter.layoutFrames()` 的分页循环
|
||
- `RDEPUBTextRendererSupport.makeChapterRenderRequest()` 的 HTML 处理
|
||
|
||
**Step 2:分页/渲染单章隔离**
|
||
3. 单章渲染/分页失败时,降级到空白页或上一次缓存结果,不中断全书构建
|
||
4. 在 `RDEPUBTextChapterPaginationDiagnostic` 中记录错误状态
|
||
|
||
**Step 3:全局信号处理(可选)**
|
||
5. 注册 `NSSetUncaughtExceptionHandler` 记录 ObjC 异常
|
||
6. 注册 signal handler(SIGABRT, SIGSEGV)记录崩溃现场
|
||
7. 下次启动时上报(如果后续有上报系统)
|
||
|
||
**验收标准**:
|
||
- 单章渲染异常不导致整个 build 中断
|
||
- pageCurl 崩溃场景已有保护,验证不退化
|
||
- 崩溃日志可追踪到具体章节和阶段
|
||
|
||
---
|
||
|
||
### 9. 内存管理
|
||
|
||
**当前状态**:无显式内存预算。无 `didReceiveMemoryWarning` 处理。`RDEPUBTextBookCache` 磁盘缓存无大小限制。
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:内存警告响应**
|
||
1. 在 `RDEPUBReaderController` 中监听 `UIApplication.didReceiveMemoryWarningNotification`
|
||
2. 收到警告时:
|
||
- 清空 `RDReaderPreloadController` 的预加载缓存
|
||
- 通知 `RDEPUBTextBookCache` 的内存缓存(如果有)清空
|
||
- 释放非当前可见章节的渲染结果
|
||
|
||
**Step 2:章节级内存释放**
|
||
3. 在 `RDEPUBTextBookBuilder` 构建完成后,不再持有已构建章节的 `NSAttributedString`
|
||
4. 当前只持有 `RDEPUBTextBook`(包含 `[RDEPUBTextPage]` 的 NSRange),内存占用已较低
|
||
5. 如果后续引入 attributed string 缓存,需增加 LRU 淘汰策略
|
||
|
||
**Step 3:图片内存控制**
|
||
6. `RDReaderPreloadController` 预加载的 page view 数量与内存挂钩
|
||
7. 当内存压力时减少 `preloadRadius`(从 1 降到 0)
|
||
|
||
**验收标准**:
|
||
- 在大书(79MB)构建过程中收到内存警告时,app 不被系统杀掉
|
||
- 预加载缓存在内存压力下自动收缩
|
||
- 当前页内容不丢失
|
||
|
||
---
|
||
|
||
### 10. 增量构建
|
||
|
||
**当前状态**:全书一次性分页。`RDEPUBTextBookBuilder.build()` 遍历全部 spine item,输出完整 `RDEPUBTextBook`。
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:章节级构建接口**
|
||
1. 在 `RDEPUBTextBookBuilder` 上新增:
|
||
```swift
|
||
func buildChapter(
|
||
at spineIndex: Int,
|
||
publication: RDEPUBPublication,
|
||
parser: RDEPUBParser,
|
||
pageSize: CGSize,
|
||
style: RDEPUBTextRenderStyle
|
||
) -> RDEPUBTextChapter?
|
||
```
|
||
2. 内部逻辑从 `build()` 的循环体中提取,单章可独立构建
|
||
|
||
**Step 2:按需加载**
|
||
3. `RDEPUBReaderLoadCoordinator` 在打开书时只构建当前章节和相邻 ±1 章
|
||
4. 用户翻到新章节时,后台异步构建该章节
|
||
5. 使用 `DispatchQueue` 串行队列保证线程安全
|
||
|
||
**Step 3:与缓存联动**
|
||
6. `RDEPUBPaginationCacheCoordinator` 支持单章缓存命中检查
|
||
7. 缓存命中时跳过构建,直接加载
|
||
|
||
**验收标准**:
|
||
- 大书首次打开时间缩短(只构建 3 章而非全书)
|
||
- 翻到未构建章节时无明显卡顿(后台预构建)
|
||
- 全书构建 API 保持不变(兼容现有调用方)
|
||
|
||
---
|
||
|
||
### 11. 缓存管理
|
||
|
||
**当前状态**:`RDEPUBTextBookCache` 磁盘缓存无大小限制、无淘汰策略、无选择性失效。无内存缓存。
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:磁盘缓存容量控制**
|
||
1. 新增 `maxDiskCacheSize: UInt64`(默认 200MB)
|
||
2. 在 `save(_:key:)` 写入后检查总大小
|
||
3. 超限时按修改时间删除最旧的缓存文件,直到低于阈值
|
||
|
||
**Step 2:选择性失效**
|
||
4. 新增 `invalidate(key:)` 方法,删除指定缓存文件
|
||
5. 当用户修改 `fontChoice` 或 `fontSize` 时,只失效当前书的缓存(而非 `invalidateAll`)
|
||
|
||
**Step 3:内存缓存(NSCache)**
|
||
6. 在 `RDEPUBTextBookCache` 上层增加 `NSCache<NSString, PaginationCacheArchive>`
|
||
7. `load(key:)` 先查内存缓存,miss 后查磁盘并回填
|
||
8. 内存警告时清空 NSCache
|
||
|
||
**Step 4:缓存统计持久化**
|
||
9. `RDEPUBTextBookCache` 新增 `cacheStats` 属性,记录总命中/缺失次数
|
||
10. 可用于调试和性能分析
|
||
|
||
**验收标准**:
|
||
- 磁盘缓存不超过设定上限
|
||
- 修改字号/字体后,旧缓存被正确失效
|
||
- 连续打开同一本书时,第二次命中内存缓存(< 1ms)
|
||
|
||
---
|
||
|
||
## 三、可访问性
|
||
|
||
### 12. VoiceOver
|
||
|
||
**当前状态**:9 个 `accessibilityIdentifier`(仅用于 UI 测试),无 `accessibilityLabel`、`accessibilityHint`、`accessibilityTraits`。阅读内容区域无任何无障碍支持。
|
||
|
||
**实施步骤**:
|
||
|
||
**Step 1:工具栏无障碍(最低门槛)**
|
||
1. `RDEPUBReaderTopToolView`:
|
||
- `backButton.accessibilityLabel = "返回"`
|
||
- `bookmarkButton.accessibilityLabel = "书签"` + `accessibilityValue` 反映当前状态(已添加/未添加)
|
||
- `titleLabel.accessibilityLabel` = 书籍标题
|
||
2. `RDEPUBReaderBottomToolView`:
|
||
- 各按钮补充 `accessibilityLabel`("目录"、"书签列表"、"高亮列表"、"添加高亮"、"设置")
|
||
- 进度 slider 补充 `accessibilityValue`("第 X 页,共 Y 页")
|
||
|
||
**Step 2:设置面板**
|
||
3. `RDEPUBReaderSettingsViewController` 所有控件补充 label 和 traits
|
||
|
||
**Step 3:阅读内容区域(中等难度)**
|
||
4. `RDEPUBTextContentView` 设置 `isAccessibilityElement = true`
|
||
5. `accessibilityLabel` = 当前页纯文本内容
|
||
6. 翻页时发出 `UIAccessibility.pageScrolledNotification`
|
||
|
||
**Step 4:目录和高亮列表**
|
||
7. `RDEPUBReaderChapterListController` 列表项设置 `accessibilityLabel`(章节标题 + 页码)
|
||
8. `RDEPUBReaderHighlightsViewController` 列表项设置 `accessibilityLabel`(高亮文本 + 章节)
|
||
|
||
**验收标准**:
|
||
- VoiceOver 用户能完整导航工具栏
|
||
- 翻页时 VoiceOver 朗读新页内容
|
||
- 目录和高亮列表可被 VoiceOver 逐项朗读
|
||
|
||
---
|
||
|
||
### 13. Dynamic Type
|
||
|
||
**当前状态**:字号由 `RDEPUBReaderConfiguration.fontSize` 控制,不响应系统 `UIContentSizeCategory` 变化。
|
||
|
||
**实施步骤**:
|
||
|
||
1. 在 `RDEPUBReaderSettingsViewController` 中监听 `UIContentSizeCategory.didChangeNotification`
|
||
2. 当系统字体大小变化时,根据新的 `UIContentSizeCategory` 计算等效字号
|
||
3. 或者:在 `RDEPUBReaderConfiguration` 中增加 `useSystemFontSize: Bool`,启用时忽略 `fontSize`,使用系统推荐值
|
||
4. 字号映射表(参考 Apple 的 `preferredFont(forTextStyle:)` 返回值)
|
||
|
||
**验收标准**:
|
||
- 系统字体调大后,阅读器字号自动跟随
|
||
- 用户手动调节字号后,覆盖系统设置
|
||
|
||
---
|
||
|
||
### 14. 高对比度
|
||
|
||
**当前状态**:6 个固定主题预设,不跟随系统 `UIAccessibility.isDarkerSystemColorsEnabled`。
|
||
|
||
**实施步骤**:
|
||
|
||
1. `RDEPUBReaderTheme` 增加 `highContrastVariant: RDEPUBReaderTheme?` 属性
|
||
2. 监听 `UIAccessibility.darkerSystemColorsStatusDidChangeNotification`
|
||
3. 高对比度启用时,自动切换到高对比度主题变体(更大色彩对比度)
|
||
4. 或提供独立的"高对比度"主题预设
|
||
|
||
**验收标准**:
|
||
- 系统高对比度开启后,阅读器自动切换到高对比度主题
|
||
- 关闭后恢复原主题
|
||
|
||
---
|
||
|
||
### 15. DRM
|
||
|
||
**当前状态**:需求文档明确声明 DRM 不在 SDK 范围内。如果需要支持,建议通过以下方式:
|
||
|
||
**建议方案**:
|
||
|
||
1. 不在 SDK 内实现 DRM,而是通过 `RDEPUBParser` 的输入端控制:
|
||
- `RDEPUBParser` 接受 `Data` 或 `URL`,调用方可以先解密再传入
|
||
- 或新增 `RDEPUBDRMProvider` 协议:
|
||
```swift
|
||
protocol RDEPUBDRMProvider {
|
||
func decryptedData(for resourceURL: URL) -> Data?
|
||
func isProtected(_ resourceURL: URL) -> Bool
|
||
}
|
||
```
|
||
2. SDK 内的 `ss-reader://` URL scheme handler 在读取资源时调用 `DRMProvider`
|
||
3. 商业 DRM(如 Adobe ADEPT、Readium LCP)由调用方集成,SDK 提供接入点
|
||
|
||
**验收标准**:
|
||
- SDK 提供清晰的 DRM 接入协议
|
||
- 不引入 DRM 依赖,保持 SDK 轻量
|
||
- 调用方能通过协议接入自己的 DRM 方案
|
||
|
||
---
|
||
|
||
## 四、功能完整度(暂未展开)
|
||
|
||
以下功能需求已识别但暂未进入详细开发计划:
|
||
|
||
| 缺失项 | 严重度 | 说明 |
|
||
| --- | --- | --- |
|
||
| **书架/书库管理** | 高 | SDK 只能打开单本书,无书架 UI、阅读历史、分类管理 |
|
||
| **批注导出/分享** | 高 | 高亮/笔记只有本地存储,无导出、分享、复制到剪贴板 |
|
||
| **阅读统计** | 中 | 无阅读时长追踪、阅读速度、连续阅读天数 |
|
||
| **TTS 朗读** | 中 | 微信读书核心功能之一,当前无任何语音相关代码 |
|
||
| **全局搜索** | 中 | 当前搜索只在单本书内,无跨书搜索 |
|
||
| **离线/云端同步** | 高 | 无 iCloud/自建同步,阅读进度和笔记只在本地 |
|
||
| **夜间模式定时切换** | 低 | 有暗色主题但不能跟随系统或定时切换 |
|
||
|
||
---
|
||
|
||
## 五、按优先级排序的建议路线
|
||
|
||
### P0 — 影响商业发布
|
||
|
||
1. **分页回归基准** — UI 自动化测试已启动,下一步需要把分页结果、首屏时间、截图差异纳入回归基准
|
||
2. **字体选择器增强** — 基础字体选择器已实现,后续需补:更多内置字体包、字体预览、字体资源加载失败兜底
|
||
3. **书架/书库管理** — 商业阅读器的入口
|
||
4. **暗色模式图片处理增强** — 基础处理已实现,后续可补:按图片亮度自适应混合比例
|
||
|
||
### P1 — 影响用户留存
|
||
|
||
5. **批注导出/分享** — 深度阅读用户的核心需求
|
||
6. **阅读统计/时长追踪** — 用户粘性和产品数据的基础
|
||
7. **性能基线与大书优化** — 大书卡顿是用户流失的主要原因
|
||
8. **简繁转换** — 面向港澳台用户需要
|
||
|
||
### P2 — 提升竞争力
|
||
|
||
9. **TTS 朗读** — 通勤场景、无障碍场景刚需
|
||
10. **云端同步** — 多设备用户的基本需求
|
||
11. **VoiceOver 完善** — 合规和品牌形象
|
||
12. **全局搜索** — 藏书量大时的效率工具
|
||
|
||
---
|
||
|
||
## 六、总结
|
||
|
||
经过三方对比,渲染质量层面的真实差距比最初评估要小:
|
||
|
||
- **图文分页质量**:双方基本对齐,我们甚至在 `keepWithNext` 上领先
|
||
- **真正的差距**:字体包数量、分页回归基准、简繁转换
|
||
- **WXRead 也没做的**:竖排、ruby、公式、hyphenation——这些不是必须对齐的
|
||
|
||
如果目标是"能上架的商业阅读器",当前已补齐字体选择器、暗色模式图片处理和基础 UI 自动化测试。下一步最值得投入的是分页回归基准、更多字体资源和书架/书库管理。
|