- 更新模块文件数(EPUBCore 31+2, EPUBTextRendering 13, ReaderView 5, EPUBUI 19) - 更新 Swift 版本(5.10)、iOS 版本(15.0)、行数等基本信息 - 补充新增文件文档(TextAnchor, RenderRequest, WebViewDebug, AssetRepository, TextIndexTable, TextPerformanceSampler, ChapterData, PageInteractionController 等) - 更新 RDEPUBLocation 模型(新增 rangeAnchor 字段) - 更新 RDEPUBReaderConfiguration(13 项配置)、Delegate 签名、Persistence(8 方法) - 修正 RDURLReaderController 归属(ReaderView → EPUBUI) - 移除过时的 LegacyRDReaderController 引用 - 标记 SS→RD 命名迁移为已完成
436 lines
19 KiB
Markdown
436 lines
19 KiB
Markdown
# RDReaderView 架构文档
|
||
|
||
## 1. 项目概览
|
||
|
||
RDReaderView 是一个 iOS 阅读器组件库(CocoaPods),提供开箱即用的 EPUB 阅读能力,并保留对纯文本翻页的支持。
|
||
|
||
- **最低 iOS 版本**:15.0
|
||
- **Swift 版本**:5.10+
|
||
- **依赖**:ZIPFoundation(EPUB 解压)、DTCoreText(文本 EPUB 渲染)
|
||
- **Demo 额外依赖**:SnapKit、SSAlertSwift
|
||
|
||
---
|
||
|
||
## 2. 总体分层
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────┐
|
||
│ Demo / 宿主 App │
|
||
│ ViewController → RDURLReaderController(demo 级路由控制器)│
|
||
└───────────────────────────┬─────────────────────────────┘
|
||
│
|
||
┌───────────────────────────▼─────────────────────────────┐
|
||
│ EPUBUI 层(library 级读者 UI) │
|
||
│ RDEPUBReaderController(开箱即用入口,~1995 行) │
|
||
│ RDEPUBReaderConfiguration / Theme / Settings / Persistence│
|
||
│ TopToolView / BottomToolView / ToolView 基类 │
|
||
│ ChapterList / Highlights / Bookmarks / Settings 面板 │
|
||
│ RDEPUBTextContentView / RDEPUBWebContentView │
|
||
│ RDEPUBPageInteractionController / SelectionOverlayView │
|
||
│ RDEPUBPageLayoutSnapshot / RDURLReaderController │
|
||
└───────────────────────────┬─────────────────────────────┘
|
||
│
|
||
┌───────────────────────────▼─────────────────────────────┐
|
||
│ 翻页容器层(RDReaderView) │
|
||
│ RDReaderView(UIView,统一翻页外壳) │
|
||
│ 4 种翻页模式:pageCurl / horizontalScroll / │
|
||
│ verticalScroll / horizontalCoverScroll │
|
||
│ RDReaderFlowLayout / RDReaderContentCell │
|
||
│ RDReaderPageChildViewController(pageCurl 页包装) │
|
||
└───────────────────────────┬─────────────────────────────┘
|
||
│
|
||
┌───────────────────────────▼─────────────────────────────┐
|
||
│ EPUBCore 层(EPUB 引擎) │
|
||
│ │
|
||
│ Publication 层 │
|
||
│ RDEPUBParser(+Archive / +Package / +TOC / │
|
||
│ +ReadingProfile / +Resources) │
|
||
│ RDEPUBPublication(出版物聚合对象) │
|
||
│ RDEPUBModels(metadata / manifest / spine 模型) │
|
||
│ RDEPUBReadingModels(location / viewport / highlight) │
|
||
│ RDEPUBTextAnchor / RDEPUBTextRangeAnchor(文本锚点) │
|
||
│ RDEPUBRenderRequest(渲染请求模型) │
|
||
│ │
|
||
│ Services 层 │
|
||
│ RDEPUBResourceResolver(资源 URL 统一入口) │
|
||
│ RDEPUBPreferences(展示参数聚合) │
|
||
│ RDEPUBPaginator(离屏分页服务) │
|
||
│ │
|
||
│ Navigator 层 │
|
||
│ RDEPUBReadingSession(状态机 + 会话协调) │
|
||
│ RDEPUBNavigatorState(状态枚举) │
|
||
│ RDEPUBNavigatorLayoutContext │
|
||
│ │
|
||
│ Resource View 层 │
|
||
│ RDEPUBWebView(+Configuration / +Reflowable / │
|
||
│ +FixedLayout / +JavaScriptBridge / │
|
||
│ +Search) │
|
||
│ RDEPUBResourceURLSchemeHandler(ss-reader:// 协议) │
|
||
│ RDEPUBStyleSheetBuilder / RDEPUBJavaScriptBridge │
|
||
│ RDEPUBFixedLayoutTemplate / RDEPUBAssetRepository │
|
||
│ RDEPUBWebViewDebug(调试日志工具) │
|
||
│ │
|
||
│ Search 层 │
|
||
│ RDEPUBSearchEngine(协议)/ RDEPUBHTMLSearchEngine │
|
||
│ RDEPUBSearchModels(SearchMatch/Result/State/Presentation)│
|
||
└───────────────────────────┬─────────────────────────────┘
|
||
|
||
┌───────────────────────────▼─────────────────────────────┐
|
||
│ EPUBTextRendering 层(文本 EPUB 渲染) │
|
||
│ RDEPUBTextRenderer(协议) │
|
||
│ RDEPUBDTCoreTextRenderer(DTCoreText 实现) │
|
||
│ RDEPUBTextRendererSupport / RDEPUBTextPaginationSupport │
|
||
│ RDEPUBTextBookBuilder / RDPlainTextBookBuilder │
|
||
│ RDEPUBTextLayouter / RDEPUBTextLayoutFrame │
|
||
│ RDEPUBTextBookCache / RDEPUBChapterData │
|
||
│ RDEPUBTextIndexTable / RDEPUBTextPerformanceSampler │
|
||
│ RDEPUBTextSearchEngine │
|
||
└──────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 翻页容器层(RDReaderView)
|
||
|
||
### 3.1 四种翻页模式
|
||
|
||
| 模式 | 实现方式 | 特点 |
|
||
|------|----------|------|
|
||
| `pageCurl` | UIPageViewController | 原生翻书效果,手势由系统提供 |
|
||
| `horizontalScroll` | UICollectionView + RDReaderFlowLayout | 每屏显示 2 项,水平分页滚动 |
|
||
| `verticalScroll` | UICollectionView + RDReaderFlowLayout | 全宽项目,垂直连续滚动 |
|
||
| `horizontalCoverScroll` | UICollectionView + RDReaderFlowLayout | 覆盖滚动效果,Z 轴动画 |
|
||
|
||
### 3.2 数据源协议
|
||
|
||
```swift
|
||
public protocol RDReaderDataSource: NSObjectProtocol {
|
||
func pageCountOfReaderView(readerView: RDReaderView) -> Int
|
||
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
|
||
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
|
||
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
|
||
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
|
||
}
|
||
```
|
||
|
||
### 3.3 手势分区(scroll 模式)
|
||
|
||
屏幕水平三等分:
|
||
- **左 1/3**:上一页
|
||
- **中 1/3**:显示 / 隐藏工具栏
|
||
- **右 1/3**:下一页
|
||
|
||
### 3.4 翻页模式切换
|
||
|
||
`switchReaderDisplayType(_ type:)` 会完全销毁并重建底层 view(pageViewController 或 collectionView),然后重新加载数据。
|
||
|
||
---
|
||
|
||
## 4. EPUB 引擎层(EPUBCore)
|
||
|
||
### 4.1 Publication 层:解析与聚合
|
||
|
||
**主链路:**
|
||
|
||
```
|
||
epubURL
|
||
→ RDEPUBParser.parse(epubURL:)
|
||
→ extractArchive # ZIP 解压到沙盒临时目录
|
||
→ parseContainerXML # 定位 OPF 路径
|
||
→ parseOPF # 解析 metadata / manifest / spine
|
||
→ parseTOC # 解析 NCX 或 Nav 目录
|
||
→ RDEPUBPublication(parser:) # 聚合解析结果,挂载 resourceResolver
|
||
```
|
||
|
||
**RDEPUBPublication 暴露的能力:**
|
||
|
||
| 属性 / 方法 | 说明 |
|
||
|------------|------|
|
||
| `metadata` | 书名、作者、语言、layout 等 |
|
||
| `manifest` | id → RDEPUBManifestItem 映射 |
|
||
| `spine` | 有序 spine 列表(包含 href) |
|
||
| `tableOfContents` | 树形目录 |
|
||
| `layout` | `.reflowable` 或 `.fixed` |
|
||
| `readingProfile` | `.webInteractive` / `.webFixedLayout` / `.textReflowable` |
|
||
| `resourceResolver` | 统一资源 URL 解析入口 |
|
||
| `fixedLayoutSpreadEnabled(for:viewportSize:)` | 判断是否启用双页 spread |
|
||
| `makeFixedSpreads(preferences:viewportSize:)` | 生成 fixed spread 页模型 |
|
||
|
||
**readingProfile 判定逻辑:**
|
||
|
||
```
|
||
layout == .fixed → webFixedLayout
|
||
layout == .reflowable + 含交互脚本 → webInteractive
|
||
layout == .reflowable + 无交互脚本 → textReflowable
|
||
```
|
||
|
||
### 4.2 Services 层
|
||
|
||
#### RDEPUBResourceResolver
|
||
|
||
统一处理所有资源路径转换,是 WebView 和 Paginator 访问资源的唯一入口:
|
||
|
||
| 方法 | 说明 |
|
||
|------|------|
|
||
| `fileURL(forHref:)` | href → 本地文件 URL |
|
||
| `schemeURL(forHref:)` | href → `ss-reader://` 协议 URL |
|
||
| `href(forSpineIndex:)` | spineIndex → href |
|
||
| `normalizedHref(_:)` | 相对路径标准化(统一相对 OPF) |
|
||
|
||
#### RDEPUBPaginator
|
||
|
||
使用隐藏的 `WKWebView` 离屏加载每个 spine 资源,通过 JS 注入分页 CSS,回调每个资源的页数:
|
||
|
||
```swift
|
||
paginator.calculate(publication: publication, preferences: preferences, viewportSize: size) { pageCounts in
|
||
// pageCounts[i] = spine[i] 的页数
|
||
}
|
||
```
|
||
|
||
#### RDEPUBPreferences
|
||
|
||
聚合 WebView 和 Paginator 共用的展示参数:字号、行距、主题色、边距、布局适配模式、spread 模式。
|
||
|
||
### 4.3 Navigator 层
|
||
|
||
#### RDEPUBNavigatorState 状态机
|
||
|
||
```
|
||
initializing → loading → idle ←→ jumping
|
||
←→ moving
|
||
←→ repaginating
|
||
```
|
||
|
||
| 状态 | 含义 | 允许动作 |
|
||
|------|------|----------|
|
||
| `initializing` | 刚打开书籍 | 接收初始恢复位置 |
|
||
| `loading` | 生成首轮 page model | 缓存 pending navigation |
|
||
| `idle` | 显示稳定 | 应用 staged snapshot、处理跳转、保存位置 |
|
||
| `jumping` | 目录 / 内部链接跳转中 | 重放 pending location |
|
||
| `moving` | 用户翻页中 | 更新当前 pageNum |
|
||
| `repaginating` | 重新分页中(字号/横竖屏变化) | 缓存当前位置,等待完成 |
|
||
|
||
#### RDEPUBReadingSession
|
||
|
||
会话级协调者,持有:
|
||
|
||
- `publication`:出版物只读视图
|
||
- `activePages / activeChapters`:当前展示的页模型和章节信息
|
||
- `stagedPages / stagedChapters`:后台分页完成后暂存,等待 idle 状态时应用
|
||
- `pendingNavigationLocation / pendingNavigationPageNum`:等待当前加载完成后再执行的跳转请求
|
||
- `currentViewport / currentReadingContext`:当前可见区域的位置信息
|
||
|
||
关键操作:
|
||
|
||
```swift
|
||
session.stageSnapshot(snapshot, restoreLocation:) // 后台分页完成,暂存结果
|
||
session.consumeStagedSnapshotIfAllowed() // idle 时消费暂存(线程安全切换)
|
||
session.transition(to: .idle) // 状态跃迁
|
||
session.clearPendingNavigation() // 取消待执行跳转
|
||
```
|
||
|
||
### 4.4 Resource View 层
|
||
|
||
#### RDEPUBWebView
|
||
|
||
承载单个 spine 资源的 WKWebView,按职责拆分为 4 个扩展文件:
|
||
|
||
| 扩展 | 职责 |
|
||
|------|------|
|
||
| `+Configuration` | WKWebViewConfiguration、schemeHandler 注册、user scripts |
|
||
| `+Reflowable` | 注入分页 CSS、滚动到指定 progression、接收 JS 事件 |
|
||
| `+FixedLayout` | fixed-layout HTML wrapper 生成和加载 |
|
||
| `+JavaScriptBridge` | JS ↔ Swift 消息路由、选区、高亮、进度上报 |
|
||
| `+Search` | 搜索高亮装饰 |
|
||
|
||
#### ss-reader:// 协议
|
||
|
||
所有 spine 资源(XHTML、CSS、图片、字体)统一通过 `ss-reader://book/<relative-path>` 访问,由 `RDEPUBResourceURLSchemeHandler` 从本地解压目录读取并返回。
|
||
|
||
优点:
|
||
- 相对资源路径在 WebView 中自然解析
|
||
- fixed-layout iframe 不再白屏
|
||
- 无需 `allowingReadAccessTo` 路径权限
|
||
|
||
---
|
||
|
||
## 5. 文本 EPUB 渲染层(EPUBTextRendering)
|
||
|
||
适用于 `readingProfile == .textReflowable` 的书籍(纯文本小说类 EPUB2)。
|
||
|
||
### 5.1 渲染链路
|
||
|
||
```
|
||
章节 HTML 文件
|
||
→ RDEPUBTextRendererSupport.injectFragmentMarkers(into:) # 注入 fragment 标记
|
||
→ RDEPUBDTCoreTextRenderer.renderChapter(html:baseURL:style:)
|
||
→ DTHTMLAttributedStringBuilder # HTML → NSAttributedString
|
||
→ extractFragmentOffsets # fragment → 字符偏移量映射
|
||
→ normalizeReadingAttributes # 统一字体/行距
|
||
→ RDEPUBRenderedChapterContent
|
||
.attributedString # 渲染后的富文本
|
||
.fragmentOffsets # fragment id → 字符偏移
|
||
```
|
||
|
||
### 5.2 分页
|
||
|
||
```
|
||
RDEPUBTextBookBuilder.buildBook(publication:style:pageSize:renderer:)
|
||
→ 逐章节调用 renderer.renderChapter(...)
|
||
→ RDEPUBTextPaginationSupport.paginate(attributedString:pageSize:)
|
||
→ RDEPUBTextBook(章节 + 页模型 + fragment 索引)
|
||
```
|
||
|
||
### 5.3 内容显示
|
||
|
||
`RDEPUBTextContentView` 基于 `NSAttributedString` + `UITextView`(或 DTCoreText 自定义绘制),实现:
|
||
- 每页显示对应字符范围的内容
|
||
- 高亮重叠渲染
|
||
- 支持按 fragment 偏移量跳转
|
||
|
||
---
|
||
|
||
## 6. EPUBUI 层(开箱即用读者 UI)
|
||
|
||
`RDEPUBReaderController` 是 library 层提供的完整读者入口,调用方只需传入 EPUB 文件 URL:
|
||
|
||
```swift
|
||
let controller = RDEPUBReaderController(
|
||
epubURL: url,
|
||
configuration: .default,
|
||
persistence: RDEPUBReaderPersistence(storageKey: "my-book")
|
||
)
|
||
controller.delegate = self
|
||
present(controller, animated: true)
|
||
```
|
||
|
||
### 6.1 内置能力
|
||
|
||
| 能力 | 对应文件 |
|
||
|------|----------|
|
||
| 顶部工具栏(书名、返回、目录、高亮入口) | RDEPUBReaderTopToolView |
|
||
| 底部工具栏(进度条、页码) | RDEPUBReaderBottomToolView |
|
||
| 目录面板 | RDEPUBReaderChapterListController |
|
||
| 高亮管理 | RDEPUBReaderHighlightsViewController |
|
||
| 设置面板(字号、行距、主题、翻页模式) | RDEPUBReaderSettingsViewController |
|
||
| 阅读位置持久化 | RDEPUBReaderPersistence |
|
||
|
||
### 6.2 配置项(RDEPUBReaderConfiguration)
|
||
|
||
| 属性 | 默认值 | 说明 |
|
||
|------|--------|------|
|
||
| `fontSize` | 15 | 字号(pt) |
|
||
| `lineHeightMultiple` | 1.6 | 行距倍数 |
|
||
| `displayType` | `.pageCurl` | 翻页模式 |
|
||
| `landscapeDualPageEnabled` | `true` | 横屏双页 |
|
||
| `showsTableOfContents` | `true` | 是否显示目录入口 |
|
||
| `allowsHighlights` | `true` | 是否显示高亮入口 |
|
||
| `showsSettingsPanel` | `true` | 是否显示设置入口 |
|
||
| `reflowableContentInsets` | (40,16,40,16) | 可重排内容内边距 |
|
||
| `fixedContentInset` | .zero | 固定版式内容内边距 |
|
||
| `theme` | `.light` | 主题 |
|
||
| `fixedLayoutFit` | `.page` | 固定版式适配模式 |
|
||
| `fixedLayoutSpreadMode` | `.automatic` | fixed-layout spread 模式 |
|
||
| `textRenderingEngine` | `.dtCoreText` | 文本 EPUB 渲染引擎 |
|
||
|
||
---
|
||
|
||
## 7. 关键数据流
|
||
|
||
### 7.1 打开 EPUB 书籍
|
||
|
||
```
|
||
RDEPUBReaderController.init(epubURL:)
|
||
→ viewDidLoad
|
||
→ RDEPUBParser.parse(epubURL:)
|
||
→ RDEPUBPublication(parser:)
|
||
→ RDEPUBReadingSession(publication:)
|
||
→ readingProfile 判断渲染路径
|
||
textReflowable:
|
||
RDEPUBTextBookBuilder.buildBook(...) # 后台分页
|
||
→ session.stageSnapshot(...)
|
||
→ session.consumeStagedSnapshotIfAllowed() # idle 时应用
|
||
→ readerView.reloadData()
|
||
webInteractive / webFixedLayout:
|
||
RDEPUBPaginator.calculate(...) # 离屏 WebView 分页
|
||
→ session.stageSnapshot(...)
|
||
→ readerView.reloadData()
|
||
→ persistence.restoreLocation() # 恢复上次阅读位置
|
||
```
|
||
|
||
### 7.2 翻页
|
||
|
||
```
|
||
用户手势(tap / swipe)
|
||
→ RDReaderView 检测手势分区 / 翻页方向
|
||
→ delegate.pageNum(readerView:pageNum:)
|
||
→ RDEPUBReaderController 根据 pageNum 找 EPUBPage
|
||
→ RDEPUBReadingSession.transition(to: .moving)
|
||
→ readerView.pageContentView(pageNum:) 回调
|
||
→ 创建 RDEPUBWebContentView 或 RDEPUBTextContentView
|
||
→ loadPage(spineIndex: pageIndexInChapter: preferences:)
|
||
→ JS bridge 上报 progression → session.currentViewport 更新
|
||
```
|
||
|
||
### 7.3 定位模型(RDEPUBLocation)
|
||
|
||
EPUB 是可重排内容,字号 / 横竖屏变化会使页号失效。定位模型使用 `href + progression`:
|
||
|
||
```swift
|
||
struct RDEPUBLocation: Codable, Equatable {
|
||
var bookIdentifier: String? // 隔离不同书的进度
|
||
var href: String // OPF 相对路径(对应 spine 资源)
|
||
var progression: Double // 视口起始位置 [0, 1]
|
||
var lastProgression: Double? // 视口末尾位置 [0, 1]
|
||
var fragment: String? // 锚点
|
||
var rangeAnchor: RDEPUBTextRangeAnchor? // 文本范围锚点(用于文本 EPUB 高亮定位)
|
||
}
|
||
```
|
||
|
||
**恢复流程:**
|
||
|
||
1. 标准化 `href`(相对 OPF)
|
||
2. 找到对应 spineIndex
|
||
3. 由 `navigationProgression` 估算扁平页号
|
||
4. 若有 `fragment`,加载后在 WebView 中锚点滚动
|
||
|
||
---
|
||
|
||
## 8. 已知限制与后续待办
|
||
|
||
| 问题 | 说明 |
|
||
|------|------|
|
||
| RDEPUBReaderController 过大 | 约 1995 行,仍混有 EPUB 加载、UI 管理、数据源逻辑,待进一步拆分 |
|
||
| 部分 UI 文件仍然偏大 | RDEPUBTextContentView ~728 行、RDEPUBTextBookBuilder ~717 行,需继续拆分 |
|
||
| 横竖屏切换已接入一阶段支持 | `RDEPUBReaderController.viewWillTransition()` 统一接管正文重分页,`RDReaderView` 保留容器级双页布局刷新;仍需补齐固定回归矩阵 |
|
||
| 固定手势分区比例 | 三等分固定写死,不支持自定义 |
|
||
| 自动化测试为首批接入状态 | 已有 `ReadViewSDKDemoTests` / `ReadViewSDKDemoUITests` 与 parser / resolver / persistence / smoke 用例,分页、恢复链路和更多 UI 闭环仍需继续补齐 |
|
||
| 基线验证待跑 | 四本样书(凡人修仙传、爱忘事熊爷爷、宝山辽墓、张学良传)的分页耗时、目录命中率、末页事件等数据尚未收集 |
|
||
|
||
---
|
||
|
||
## 9. 构建与依赖
|
||
|
||
### 9.1 安装
|
||
|
||
```bash
|
||
cd RDReaderDemo
|
||
pod install
|
||
# 打开 RDReaderDemo.xcworkspace,选 RDReaderDemo scheme,构建
|
||
```
|
||
|
||
### 9.2 主要依赖
|
||
|
||
| 依赖 | 用途 | 使用层 |
|
||
|------|------|--------|
|
||
| ZIPFoundation (~> 0.9) | EPUB ZIP 解压 | RDEPUBParser+Archive |
|
||
| DTCoreText | HTML → NSAttributedString | RDEPUBDTCoreTextRenderer |
|
||
| SnapKit | Demo 布局 | Demo 层 |
|
||
| SSAlertSwift | Demo 弹窗 | Demo 层 |
|
||
|
||
### 9.3 podspec 关键配置
|
||
|
||
- `s.source_files = 'Sources/RDReaderView/**/*.{swift}'`(递归包含所有子目录)
|
||
- `s.resource_bundles`:包含 JS、CSS 等资源文件
|
||
- Library 本身无需 demo 层依赖
|