- 更新模块文件数(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 命名迁移为已完成
19 KiB
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 数据源协议
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,回调每个资源的页数:
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:当前可见区域的位置信息
关键操作:
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:
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:
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 高亮定位)
}
恢复流程:
- 标准化
href(相对 OPF) - 找到对应 spineIndex
- 由
navigationProgression估算扁平页号 - 若有
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 安装
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 层依赖