feat: add in-reader search and restructure documentation

- Add RDEPUBReaderSearchBarView with animated show/hide, keyword
  navigation, and match counting integrated into the reader controller
- Restructure docs: replace scattered design docs with consolidated
  BUSINESS_LOGIC.md and UML_CLASS_DIAGRAMS.md; update ARCHITECTURE.md
- Add SearchTests and FanrenParseTimeTest; enhance LargeBookOnDemandTests
- Add scripts/run_ui_regression.sh and summarize_ui_results.py for
  automated UI test execution and reporting

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-06-05 17:34:50 +08:00
co-authored by Claude Opus 4.7
parent d20196ee34
commit 0e7c0577e3
37 changed files with 4941 additions and 7621 deletions
+389 -456
View File
@@ -1,507 +1,440 @@
# RDReaderView 架构文档
# ReadViewSDK 系统架构文档
> 最后更新:2026-06-04
---
## 1. 项目概览
RDReaderView 是一个 iOS 阅读器组件库(CocoaPods),提供开箱即用的 EPUB 阅读能力,并保留对纯文本翻页的支持
ReadViewSDK 是一个 iOS EPUB 阅读器 SDK,支持文本重排(Reflowable)和固定布局(Fixed Layout)两种 EPUB 格式,同时兼容纯文本 (.txt) 文件。SDK 提供完整的 EPUB 解析、文本渲染、分页计算、阅读 UI 和标注管理能力
- **最低 iOS 版本**15.0
- **Swift 版本**5.10+
- **依赖**ZIPFoundationEPUB 解压)、DTCoreText(文本 EPUB 渲染
- **Demo 额外依赖**SnapKit、SSAlertSwift
**技术栈:**
- 语言:Swift 5,最低 iOS 15.6
- 依赖:DTCoreTextHTML→NSAttributedString)、ZIPFoundationEPUB 解压)、SnapKitAuto Layout)、SSAlertSwift(弹窗
- 构建:CocoaPods,本地 pod 引用
---
## 2. 总体分层
## 2. 分层架构
```
┌─────────────────────────────────────────────────────────────┐
│ ReadViewDemo (Demo App) │
│ ViewController · LaunchAutomationPlan · UITests │
└──────────────────────────────┬──────────────────────────────┘
│ imports RDReaderView
┌──────────────────────────────▼──────────────────────────────┐
│ EPUBUI 层 │
│ RDEPUBReaderController · Coordinators · Settings · TextPage │
│ ChapterRuntimeLoader · Store · DiskCache · PageMap
└──────────────────────────────┬──────────────────────────────┘
┌──────────────────────────────▼──────────────────────────────┐
│ RDReaderView 层 │
│ RDReaderView · FlowLayout · PreloadController │
│ SpreadResolver · TapRegionHandler · PagingController │
└──────────────────────────────┬──────────────────────────────┘
┌──────────────────────────────▼──────────────────────────────┐
│ EPUBTextRendering 层 │
│ TypesetterPipeline · DTCoreTextRenderer · Pagination │
│ BuildPipelineTextBookBuilder · Cache · Sampler
└──────────────────────────────┬──────────────────────────────┘
┌──────────────────────────────▼──────────────────────────────┐
│ EPUBCore 层 │
│ RDEPUBParser · Publication · ResourceResolver │
│ ReadingSession · WebView · JavaScriptBridge · Paginator │
└─────────────────────────────────────────────────────────────┘
```
### 各层职责
| 层 | 职责 | 核心类 |
|---|---|---|
| **EPUBCore** | EPUB 文件解析、资源管理、WebView 渲染、JS 桥接 | `RDEPUBParser`, `RDEPUBPublication`, `RDEPUBWebView` |
| **EPUBTextRendering** | HTML→NSAttributedString 转换、排版、分页、全书构建 | `RDEPUBTextBookBuilder`, `RDEPUBCoreTextPageFrameFactory` |
| **RDReaderView** | 通用翻页容器、手势识别、页面预加载、双页布局 | `RDReaderView`, `RDReaderFlowLayout`, `RDReaderPreloadController` |
| **EPUBUI** | 阅读器控制器、协调器模式、设置面板、按需加载、磁盘缓存 | `RDEPUBReaderController`, `RDEPUBReaderPaginationCoordinator` |
---
## 3. 核心数据流
### 3.1 打开书籍流程
```
用户点击书籍
RDEPUBReaderController.openBook(epubURL:)
├─ RDEPUBParser.parse(epubURL:) // 解压 ZIP → 解析 OPF → 构建 spine/TOC
│ └─ extractArchiveIfNeeded() // ZIPFoundation 解压到 Caches
│ └─ parseContainerRootFile() // SAX 解析 container.xml
│ └─ parseOPF() // SAX 解析 OPF (metadata/manifest/spine)
│ └─ parseTOC() // NCX 或 Navigation Document
├─ RDEPUBPublication(parser:) // 创建门面对象
├─ 判断 readingProfile:
│ ├─ .textReflowable → 文本排版路径(本 SDK 核心路径)
│ ├─ .webInteractive → WebView 渲染路径
│ └─ .webFixedLayout → 固定布局路径
└─ paginateTextPublication() // 进入分页流程
```
### 3.2 文本重排分页流程(大书优化路径)
```
paginateTextPublication()
├─ restoreBookPageMapIfPossible() // 尝试从磁盘恢复完整 BookPageMap
│ └─ summaryDiskCache.readAll(keys:) // 批量读取磁盘摘要
│ └─ 如果完整 → 直接 applyBookPageMap() → 完成
├─ 快速打开路径:
│ ├─ loadFirstRenderableRuntimeChapter() // 加载第一个可渲染章节
│ ├─ loadInitialRuntimeChapters() // 加载窗口内相邻章节
│ ├─ makePartialPageMap() // 构建局部 BookPageMap
│ └─ applyBookPageMap() // 应用到 UI,用户可立即阅读
└─ paginateMetadataOnly() // 后台元数据解析
├─ 预计算 contentHash(串行) // 读取所有章节 HTML + SHA-256
├─ readAll(keys:) 恢复已有缓存
├─ waitForReadingInteractionToSettle() // 等待用户操作冷却 0.8s
├─ OperationQueue (并发 N):
│ ├─ buildChapter() // 渲染单章 (HTML→NSAttrStr→分页)
│ ├─ chapterCacheKey() // 构建缓存键(复用预计算 hash)
│ ├─ RDEPUBChapterSummary 写盘 // 异步写入磁盘摘要
│ └─ 每 32 章刷新 BookPageMap // 增量更新 UI
└─ 最终 applyBookPageMap() // 完整页码映射
```
### 3.3 单章渲染管线
```
Raw HTML (从 EPUB 解压目录读取)
▼ [RDEPUBTextTypesetterPipeline]
├─ RDEPUBHTMLNormalizer // 去 CR、合并空行、规范化附件标记
├─ RDEPUBSemanticMarkerInjector // 注入分页语义标记 ${rd-sem-start/end}
├─ RDEPUBRenderDiagnosticsCollector // 内联 <link stylesheet>、收集图片诊断
├─ RDEPUBStyleSheetComposer // 构建 5 层 CSS (default/replace/dark/epub/user)
├─ RDEPUBFontNormalizer // 注册 @font-face 嵌入字体
├─ RDEPUBFragmentMarkerInjector // 注入 fragment 锚点标记 ${id=xxx}
▼ [RDEPUBDTCoreTextRenderer]
├─ DTHTMLAttributedStringBuilder // HTML → NSAttributedString
├─ applyPaginationSemantics() // 将语义标记转为NSAttributedString属性
├─ extractFragmentOffsets() // 提取 fragment ID→offset 映射
├─ normalizeReadingAttributes() // 规范化字体、行距、颜色、附件
▼ [RDEPUBChapterPageCounter]
├─ CTFramesetter 创建帧
├─ RDEPUBPageBreakPolicy 应用语义边界规则
│ ├─ avoidPageBreakInside // 避免块内分页
│ ├─ keepWithNext // 标题与正文不分离
│ ├─ widow/orphan 控制 // 孤行/寡行控制
│ └─ pageRelate // 微信读书跨页关联
├─ RDEPUBChapterTailNormalizer // 删除/合并空白尾页
▼ [RDEPUBTextBookBuilder]
└─ 组装 RDEPUBTextPage[] → RDEPUBTextChapter → RDEPUBTextBook
```
---
## 4. 缓存架构(三级缓存)
```
┌─────────────────────────────────────────────────────────┐
Demo / 宿主 App
ViewController → RDURLReaderControllerdemo 级路由控制器)
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ EPUBUI 层(library 级读者 UI
│ 主控制器 │
RDEPUBReaderController(开箱即用入口)
+ContentDelegates / +DataSource / +PublicAPI
+RenderSupport / +RuntimeBridge / +TableOfContents
RDURLReaderControllerURL 阅读入口)
ReaderController/(协调器)
│ RDEPUBReaderRuntime(中央运行时协调器) │
RDEPUBReaderContext(上下文状态容器)
│ RDEPUBReaderDependencies(依赖注入) │
RDEPUBReaderLoadCoordinatorEPUB 加载)
RDEPUBReaderPaginationCoordinator(分页协调)
RDEPUBReaderLocationCoordinator(位置持久化)
RDEPUBReaderAnnotationCoordinator(标注管理)
RDEPUBReaderSearchCoordinator(搜索)
│ RDEPUBReaderChromeCoordinator(工具栏) │
│ RDEPUBReaderAssemblyCoordinatorUI 组装) │
│ RDEPUBReaderViewportMonitor(视口变化监听) │
│ │
│ Settings/(配置与主题) │
│ RDEPUBReaderConfiguration / RDEPUBReaderSettings │
│ RDEPUBReaderSettingsViewController / RDEPUBReaderTheme │
│ │
│ TextPage/(文本页面交互) │
│ RDEPUBTextContentView / RDEPUBTextPageRenderView │
│ RDEPUBSelectableTextView / RDEPUBTextSelectionController│
│ RDEPUBSelectionOverlayView / RDEPUBTextAnnotationOverlay│
│ RDEPUBPageInteractionController / RDEPUBPageLayoutSnapshot│
│ RDEPUBTextPageDecorationView │
│ │
│ 工具栏与面板 │
│ RDEPUBReaderTopToolView / RDEPUBReaderBottomToolView │
│ RDEPUBReaderToolView(基类) │
│ RDEPUBReaderChapterListController(目录面板) │
│ RDEPUBReaderHighlightsViewController(高亮管理) │
│ RDEPUBReaderPersistence(位置持久化) │
│ RDEPUBReaderDelegate / RDEPUBReaderTableOfContentsItem│
│ RDEPUBWebContentView / RDEPUBWebDecorationOverlayView │
│ RDEPUBViewportTypes / UIColor+RDEPUBHex │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ 翻页容器层(RDReaderView
│ │
│ RDReaderViewUIView,统一翻页外壳) │
│ 3 种翻页模式:pageCurl / horizontalScroll / │
│ verticalScroll │
│ RDReaderViewProtocolsDataSource / Delegate / DisplayType)│
│ +CollectionView / +ContentAccess / +PageCurl / +ToolView │
│ RDReaderFlowLayout / RDReaderContentCell │
│ RDReaderPageChildViewControllerpageCurl 页包装) │
│ RDReaderGestureController │
│ │
│ Paging/(翻页控制) │
│ RDReaderPagingController(转场与排队) │
│ RDReaderPreloadController(预加载与缓存) │
│ RDReaderSpreadResolver(双页配对) │
│ RDReaderTapRegionHandler(手势分区) │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ EPUBCore 层(EPUB 引擎) │
│ │
│ 解析与模型 │
│ RDEPUBParser+Archive / +Package / +TOC / │
│ +ReadingProfile / +Resources
│ RDEPUBPublication(出版物聚合对象) │
│ RDEPUBModelsmetadata / manifest / spine 模型) │
│ Models/ │
│ RDEPUBReadingLocationModelslocation 模型) │
│ RDEPUBPaginationModels(分页模型) │
│ RDEPUBAnnotationModels(标注模型) │
│ RDEPUBTextAnchor / RDEPUBTextRangeAnchor(文本锚点) │
│ RDEPUBRenderRequest(渲染请求模型) │
│ │
│ 服务层 │
│ RDEPUBResourceResolver(资源 URL 统一入口) │
│ RDEPUBResourceURLSchemeHandlerss-reader:// 协议) │
│ RDEPUBPreferences(展示参数聚合) │
│ RDEPUBPaginator(离屏分页服务) │
│ RDEPUBStyleSheetBuilder / RDEPUBJavaScriptBridge │
│ RDEPUBFixedLayoutTemplate / RDEPUBAssetRepository │
│ │
│ 会话与导航 │
│ RDEPUBReadingSession(状态机 + 会话协调) │
│ RDEPUBNavigatorState(状态枚举) │
│ RDEPUBNavigatorLayoutContext │
│ │
│ WebView 渲染 │
│ RDEPUBWebView+Configuration / +Reflowable / │
│ +FixedLayout / +JavaScriptBridge / │
│ +Search
│ RDEPUBWebViewDebug(调试日志工具) │
│ │
│ 搜索 │
│ RDEPUBSearchEngine(协议)/ RDEPUBHTMLSearchEngine │
│ RDEPUBSearchModelsSearchMatch/Result/State/Presentation)│
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ EPUBTextRendering 层(文本 EPUB 渲染) │
│ │
│ 渲染 │
│ RDEPUBTextRenderer(协议) │
│ RDEPUBDTCoreTextRendererDTCoreText 实现) │
│ RDPlainTextBookBuilder(纯文本书籍构建) │
│ RDEPUBTextPositionConverter(位置转换器) │
│ RDEPUBTextSearchEngine(文本搜索引擎) │
│ RDEPUBTextIndexTable / RDEPUBChapterData │
│ │
│ BuildPipeline/(构建管线) │
│ RDEPUBTextBookBuilder(分页书籍构建器) │
│ RDEPUBTextBookCache / RDEPUBTextBookModels │
│ RDEPUBTextBuildPipelineInterfaces(管线协议) │
│ RDEPUBPaginationCacheCoordinator(缓存协调) │
│ RDEPUBChapterTailNormalizer(章尾规范化) │
│ RDEPUBBuildDiagnosticsReporter(诊断报告) │
│ RDEPUBTextPerformanceSampler(性能采样) │
│ │
│ Pagination/(分页引擎) │
│ RDEPUBTextLayouter / RDEPUBTextLayoutFrame │
│ RDEPUBChapterPageCounter / RDEPUBCoreTextPageFrameFactory│
│ RDEPUBPageBreakPolicy(断页策略) │
│ RDEPUBTextPaginationInterfaces(分页协议) │
│ RDEPUBTextPaginationSupport(分页支持) │
│ │
│ Typesetter/(排版管线) │
│ RDEPUBTypesettingPipeline(排版管线编排) │
│ RDEPUBHTMLNormalizerHTML 规范化) │
│ RDEPUBStyleSheetComposerCSS 组合) │
│ RDEPUBFontNormalizer(字体规范化) │
│ RDEPUBAttachmentNormalizer(附件规范化) │
│ RDEPUBFragmentMarkerInjectorFragment 标记注入) │
│ RDEPUBSemanticMarkerInjector(语义标记注入) │
│ RDEPUBRenderDiagnosticsCollector(渲染诊断) │
│ RDEPUBTextRendererSupport(渲染辅助工具) │
└──────────────────────────────────────────────────────────┘
Tier 1: 内存缓存 (RDEPUBChapterRuntimeStore)
├─ chapterDataCache: [Int: RDEPUBRuntimeChapter]
│ ├─ pageCountCache: [CacheKey: RDEPUBRuntimePageCount] │
├─ imageCache: NSCache<NSString, UIImage> (50 上限)
│ └─ 窗口驱逐:只保留当前章节 ± windowRadius 的章节 │
└──────────────────────────┬──────────────────────────────┘
miss
┌──────────────────────────▼──────────────────────────────┐
Tier 2: 磁盘摘要缓存 (RDEPUBChapterSummaryDiskCache)
├─ 路径: ~/Caches/RDEPUBChapterSummaryCache/{bookID}/
├─ 文件名: SHA256(bookID_spineIdx_renderSig_contentHash)
├─ 格式: JSON (RDEPUBChapterSummary)
├─ 写入: 异步(serial DispatchQueue
└─ 读取: 同步(readAll 批量读取)
└──────────────────────────┬──────────────────────────────┘
miss
┌──────────────────────────▼──────────────────────────────┐
Tier 3: 全书分页缓存 (RDEPUBTextBookCache)
├─ 路径: ~/Caches/RDEPUBTextBookCache/
├─ 格式: NSSecureCoding archive
├─ 内容: 每章的 pageRanges + breakReasons + semanticHints
└─ 失效: schemaVersion 变更时全部失效
└─────────────────────────────────────────────────────────┘
```
---
## 3. 翻页容器层(RDReaderView
### 3.1 三种翻页模式
| 模式 | 实现方式 | 特点 |
|------|----------|------|
| `pageCurl` | UIPageViewController | 原生翻书效果,手势由系统提供 |
| `horizontalScroll` | UICollectionView + RDReaderFlowLayout | 每屏显示 2 项,水平分页滚动 |
| `verticalScroll` | UICollectionView + RDReaderFlowLayout | 全宽项目,垂直连续滚动 |
### 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?
struct RDEPUBChapterCacheKey: Hashable {
let bookID: String //
let spineIndex: Int //
let renderSignature: String // ////
let chapterContentHash: String // HTML SHA-256
}
```
### 3.3 手势分区(scroll 模式)
屏幕水平三等分:
- **左 1/3**:上一页
- **中 1/3**:显示 / 隐藏工具栏
- **右 1/3**:下一页
### 3.4 翻页模式切换
`switchReaderDisplayType(_ type:)` 会完全销毁并重建底层 viewpageViewController 或 collectionView),然后重新加载数据。
缓存键的四元组设计确保:
- 换字体/字号 → renderSignature 变化 → 缓存失效
- EPUB 内容更新 → contentHash 变化 → 缓存失效
- 不同书籍 → bookID 不同 → 互不干扰
---
## 4. EPUB 引擎层(EPUBCore
### 4.1 Publication 层:解析与聚合
**主链路:**
## 5. 章节按需加载架构
```
epubURL
→ RDEPUBParser.parse(epubURL:)
→ extractArchive # ZIP 解压到沙盒临时目录
parseContainerXML # 定位 OPF 路径
→ parseOPF # 解析 metadata / manifest / spine
→ parseTOC # 解析 NCX 或 Nav 目录
→ RDEPUBPublication(parser:) # 聚合解析结果,挂载 resourceResolver
RDEPUBReaderController
├─ RDEPUBReaderContext // 共享状态中心
├─ parser, publication, readingSession
│ ├─ configuration, persistence
│ └─ 便捷方法 (renderStyle, layoutConfig, cacheKey)
├─ RDEPUBReaderRuntime // 运行时协调器集合
│ ├─ chapterLoader // 章节加载器
│ ├─ chapterRuntimeStore // 内存缓存
│ ├─ summaryDiskCache // 磁盘摘要缓存
│ └─ viewportMonitor // 视口变化监控
├─ RDEPUBReaderPaginationCoordinator // 分页协调器
│ ├─ paginatePublication() // 入口
│ ├─ paginateMetadataOnly() // 后台元数据解析
│ └─ restoreBookPageMapIfPossible() // 缓存恢复
├─ RDEPUBChapterLoader // 章节加载器
│ ├─ loadChapter() // 异步加载(Tier1→Tier2→全量构建)
│ └─ loadChapterSynchronouslyForMigration() // 同步加载(快速打开用)
└─ RDEPUBBookPageMap // 轻量页码映射
├─ ~100KB/1000章,不持有 NSAttributedString
└─ 支持增量刷新 (Builder pattern)
```
**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 判定逻辑:**
## 6. WebView 渲染架构(Web 路径)
```
layout == .fixed → webFixedLayout
layout == .reflowable + 含交互脚本 → webInteractive
layout == .reflowable + 无交互脚本 → textReflowable
RDEPUBWebView (UIView)
├─ WKWebView (RDEPUBAnnotationWebView)
│ ├─ 自定义选择菜单(拷贝/高亮/批注)
│ └─ 禁用系统手势(滚动手势由原生控制)
├─ RDEPUBResourceURLSchemeHandler
│ ├─ 协议: ss-reader://book/<path>
│ ├─ 映射: URL → 本地 EPUB 解压文件
│ └─ 容错: 缺失资源返回空响应(不 404)
├─ JavaScript Bridge (6 种消息)
│ ├─ ssReaderProgressionChanged // 阅读进度更新
│ ├─ ssReaderSelectionChanged // 文本选区变化
│ ├─ ssReaderInternalLink // EPUB 内部链接
│ ├─ ssReaderExternalLink // 外部链接
│ ├─ ssReaderJSError // JS 运行时错误
│ └─ ssReaderFixedLayoutReady // 固定布局加载完成
└─ 注入脚本
├─ epub-bridge.js (502 行) // 核心桥接,分页/滚动/选区/高亮
├─ cssInjector.js (45 行) // CSS 注入管理
├─ WeReadApi.js (67 行) // 微信读书 API 兼容层
├─ rangy-core.js / rangy-serializer.js // DOM Range 序列化
└─ CSS: default/replace/dark/latin // 4 套样式表
```
### 4.2 Services 层
---
#### RDEPUBResourceResolver
## 7. 翻页容器架构(RDReaderView
统一处理所有资源路径转换,是 WebView 和 Paginator 访问资源的唯一入口:
```
RDReaderView (UIView)
├─ 三种翻页模式:
│ ├─ .pageCurl → UIPageViewController (翻页动画)
│ ├─ .horizontalScroll → UICollectionView (水平滑动)
│ └─ .verticalScroll → UICollectionView (垂直滚动)
├─ 组合对象:
│ ├─ RDReaderPagingController // 翻页状态管理、请求队列
│ ├─ RDReaderPreloadController // 页面预加载、缓存管理
│ ├─ RDReaderSpreadResolver // 双页展开计算
│ └─ RDReaderTapRegionHandler // 点击区域分类(左/中/右)
├─ 数据源协议:
│ ├─ RDReaderPageProvider (新) // 格式无关,优先级高
│ └─ RDReaderDataSource (旧) // 遗留兼容,通过 Adapter 适配
└─ 手势流:
点击 → TapRegionHandler → 分类(左/中/右)
├─ 左 → goPreviousPage()
├─ 右 → goNextPage()
└─ 中 → tapCenter() (切换工具栏)
```
| 方法 | 说明 |
|------|------|
| `fileURL(forHref:)` | href → 本地文件 URL |
| `schemeURL(forHref:)` | href → `ss-reader://` 协议 URL |
| `href(forSpineIndex:)` | spineIndex → href |
| `normalizedHref(_:)` | 相对路径标准化(统一相对 OPF) |
---
#### RDEPUBPaginator
## 8. 配置与设置
使用隐藏的 `WKWebView` 离屏加载每个 spine 资源,通过 JS 注入分页 CSS,回调每个资源的页数:
### RDEPUBReaderConfiguration
```swift
paginator.calculate(publication: publication, preferences: preferences, viewportSize: size) { pageCounts in
// pageCounts[i] = spine[i]
struct RDEPUBReaderConfiguration {
var fontSize: CGFloat // 16
var fontChoice: FontChoice // //
var lineHeightMultiple: CGFloat // 1.5
var numberOfColumns: Int // 1 2
var columnGap: CGFloat // 20
var theme: ReaderTheme // 6
var displayType: RDReaderView.DisplayType // pageCurl/horizontal/vertical
var onDemandChapterWindowSize: Int // 3 3 15
var metadataParsingConcurrency: Int // CPU
var chapterWindowRadius: Int //
}
```
#### 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` 路径权限
| 数据 | 存储方式 | Key 前缀 |
|------|----------|----------|
| 阅读位置 | UserDefaults | `ssreader.epub.location.{bookID}` |
| 书签 | UserDefaults | `ssreader.epub.bookmarks.{bookID}` |
| 高亮标注 | UserDefaults | `ssreader.epub.highlights.{bookID}` |
| 全局设置 | UserDefaults | `ssreader.epub.settings` |
| 章节摘要 | 磁盘文件 | `~/Caches/RDEPUBChapterSummaryCache/` |
| 全书分页 | 磁盘文件 | `~/Caches/RDEPUBTextBookCache/` |
---
## 5. 文本 EPUB 渲染层(EPUBTextRendering
适用于 `readingProfile == .textReflowable` 的书籍(纯文本小说类 EPUB2)。
### 5.1 渲染链路
## 9. 目录结构
```
章节 HTML 文件
→ RDEPUBTextRendererSupport.injectFragmentMarkers(into:) # 注入 fragment 标记
→ RDEPUBDTCoreTextRenderer.renderChapter(html:baseURL:style:)
→ DTHTMLAttributedStringBuilder # HTML → NSAttributedString
→ extractFragmentOffsets # fragment → 字符偏移量映射
→ normalizeReadingAttributes # 统一字体/行距
→ RDEPUBRenderedChapterContent
.attributedString # 渲染后的富文本
.fragmentOffsets # fragment id → 字符偏移
Sources/RDReaderView/
├── EPUBCore/ # EPUB 解析与 WebView 渲染
│ ├── Models/ # 数据模型
│ │ ├── RDEPUBAnnotationModels.swift
│ │ ├── RDEPUBPaginationModels.swift
│ │ └── RDEPUBReadingLocationModels.swift
│ ├── Resources/ # JS/CSS/HTML 静态资源
│ │ ├── epub-bridge.js
│ │ ├── cssInjector.js
│ │ ├── WeReadApi.js
│ │ ├── rangy-core.js / rangy-serializer.js
│ │ ├── wxread-default.css / wxread-dark.css / wxread-replace.css
│ │ └── epub-fixed-layout.html
│ ├── RDEPUBParser.swift # 核心解析器
│ ├── RDEPUBParser+Archive.swift # ZIP 解压
│ ├── RDEPUBParser+Package.swift # OPF 解析
│ ├── RDEPUBParser+TOC.swift # 目录解析
│ ├── RDEPUBParser+Resources.swift # 资源访问
│ ├── RDEPUBPublication.swift # 出版物门面
│ ├── RDEPUBResourceResolver.swift # URL 解析
│ ├── RDEPUBReadingSession.swift # 阅读会话状态机
│ ├── RDEPUBWebView.swift # WebView 主类
│ ├── RDEPUBWebView+*.swift # WebView 扩展(配置/重排/固定/搜索/JS桥接)
│ ├── RDEPUBPaginator.swift # 离屏分页计算器
│ ├── RDEPUBSearchEngine.swift # 全文搜索引擎
│ └── ...
├── EPUBTextRendering/ # 文本渲染引擎
│ ├── Typesetter/ # 排版管线
│ │ ├── RDEPUBTypesettingPipeline.swift
│ │ ├── RDEPUBHTMLNormalizer.swift
│ │ ├── RDEPUBSemanticMarkerInjector.swift
│ │ ├── RDEPUBFragmentMarkerInjector.swift
│ │ ├── RDEPUBFontNormalizer.swift
│ │ ├── RDEPUBAttachmentNormalizer.swift
│ │ ├── RDEPUBStyleSheetComposer.swift
│ │ ├── RDEPUBRenderDiagnosticsCollector.swift
│ │ └── RDEPUBTextRendererSupport.swift
│ ├── Pagination/ # 分页引擎
│ │ ├── RDEPUBChapterPageCounter.swift
│ │ ├── RDEPUBCoreTextPageFrameFactory.swift
│ │ ├── RDEPUBPageBreakPolicy.swift
│ │ ├── RDEPUBTextLayoutFrame.swift
│ │ └── ...
│ ├── BuildPipeline/ # 全书构建
│ │ ├── RDEPUBTextBookBuilder.swift
│ │ ├── RDEPUBTextBookModels.swift
│ │ ├── RDEPUBTextBookCache.swift
│ │ ├── RDEPUBChapterTailNormalizer.swift
│ │ └── ...
│ ├── RDEPUBTextRenderer.swift # 渲染器协议与类型定义
│ ├── RDEPUBDTCoreTextRenderer.swift # DTCoreText 渲染器实现
│ ├── RDEPUBChapterData.swift # 章节查询门面
│ ├── RDEPUBTextIndexTable.swift # 全书索引表
│ └── RDPlainTextBookBuilder.swift # 纯文本 (.txt) 构建器
├── ReaderView/ # 通用翻页容器
│ ├── Paging/ # 翻页子系统
│ │ ├── RDReaderPagingController.swift
│ │ ├── RDReaderPreloadController.swift
│ │ ├── RDReaderSpreadResolver.swift
│ │ └── RDReaderTapRegionHandler.swift
│ ├── RDReaderView.swift # 主容器视图
│ ├── RDReaderView+*.swift # 扩展(CollectionView/PageCurl/ToolView/ContentAccess
│ ├── RDReaderFlowLayout.swift # 自定义 CollectionView 布局
│ ├── RDReaderContentCell.swift # 滚动模式 Cell
│ └── RDReaderPageChildViewController.swift // 翻页模式子 VC
└── EPUBUI/ # 阅读器 UI
├── ReaderController/ # 控制器与协调器
│ ├── ChapterRuntime/ # 章节运行时
│ │ ├── RDEPUBChapterLoader.swift
│ │ ├── RDEPUBChapterRuntimeStore.swift
│ │ ├── RDEPUBChapterSummaryDiskCache.swift
│ │ ├── RDEPUBChapterCacheKey.swift
│ │ ├── RDEPUBBookPageMap.swift
│ │ ├── RDEPUBChapterSummary.swift
│ │ └── ...
│ ├── RDEPUBReaderContext.swift // 共享状态中心
│ ├── RDEPUBReaderRuntime.swift // 运行时协调器
│ ├── RDEPUBReaderPaginationCoordinator.swift // 分页协调器
│ ├── RDEPUBReaderAnnotationCoordinator.swift // 标注协调器
│ ├── RDEPUBReaderSearchCoordinator.swift // 搜索协调器
│ ├── RDEPUBReaderNavigationCoordinator.swift // 导航协调器
│ └── ...
├── Settings/ # 设置面板
│ ├── RDEPUBReaderSettingsViewController.swift
│ ├── RDEPUBReaderThemeSelector.swift
│ └── ...
├── TextPage/ # 文本页面渲染
│ ├── RDEPUBTextContentView.swift
│ ├── RDEPUBTextPageViewController.swift
│ └── ...
├── RDEPUBReaderController.swift # 主控制器
├── RDEPUBReaderConfiguration.swift # 配置模型
└── ...
```
### 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)
## 10. 关键设计模式
`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 层依赖
| **Coordinator** | 分页/标注/搜索/导航各一个协调器,解耦 Controller |
| **Facade** | `RDEPUBPublication` 封装 `RDEPUBParser``RDEPUBChapterData` 封装章节查询 |
| **Builder** | `RDEPUBBookPageMap.Builder` 增量构建页码映射 |
| **Strategy** | `RDEPUBTextRenderer` 协议,可替换渲染器实现 |
| **Pipeline** | `RDEPUBTextTypesetterPipeline` 8 阶段排版管线 |
| **State Machine** | `RDEPUBNavigatorState` 管理阅读器状态转换 |
| **Adapter** | `RDReaderLegacyDataSourceAdapter` 适配旧数据源协议 |
| **三级缓存** | 内存 → 磁盘摘要 → 全书分页,逐级降级 |
| **Token 取消** | `paginationToken` 确保过期异步任务不干扰新任务 |
| **Frozen Parameters** | 后台任务冻结 `renderSignature`,避免运行中参数漂移 |