源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
24 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(开箱即用入口) │
│ +ContentDelegates / +DataSource / +PublicAPI │
│ +RenderSupport / +RuntimeBridge / +TableOfContents │
│ RDURLReaderController(URL 阅读入口) │
│ │
│ ReaderController/(协调器) │
│ RDEPUBReaderRuntime(中央运行时协调器) │
│ RDEPUBReaderContext(上下文状态容器) │
│ RDEPUBReaderDependencies(依赖注入) │
│ RDEPUBReaderLoadCoordinator(EPUB 加载) │
│ RDEPUBReaderPaginationCoordinator(分页协调) │
│ RDEPUBReaderLocationCoordinator(位置持久化) │
│ RDEPUBReaderAnnotationCoordinator(标注管理) │
│ RDEPUBReaderSearchCoordinator(搜索) │
│ RDEPUBReaderChromeCoordinator(工具栏) │
│ RDEPUBReaderAssemblyCoordinator(UI 组装) │
│ 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) │
│ │
│ RDReaderView(UIView,统一翻页外壳) │
│ 3 种翻页模式:pageCurl / horizontalScroll / │
│ verticalScroll │
│ RDReaderViewProtocols(DataSource / Delegate / DisplayType)│
│ +CollectionView / +ContentAccess / +PageCurl / +ToolView │
│ RDReaderFlowLayout / RDReaderContentCell │
│ RDReaderPageChildViewController(pageCurl 页包装) │
│ RDReaderGestureController │
│ │
│ Paging/(翻页控制) │
│ RDReaderPagingController(转场与排队) │
│ RDReaderPreloadController(预加载与缓存) │
│ RDReaderSpreadResolver(双页配对) │
│ RDReaderTapRegionHandler(手势分区) │
└───────────────────────────┬─────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────┐
│ EPUBCore 层(EPUB 引擎) │
│ │
│ 解析与模型 │
│ RDEPUBParser(+Archive / +Package / +TOC / │
│ +ReadingProfile / +Resources) │
│ RDEPUBPublication(出版物聚合对象) │
│ RDEPUBModels(metadata / manifest / spine 模型) │
│ Models/ │
│ RDEPUBReadingLocationModels(location 模型) │
│ RDEPUBPaginationModels(分页模型) │
│ RDEPUBAnnotationModels(标注模型) │
│ RDEPUBTextAnchor / RDEPUBTextRangeAnchor(文本锚点) │
│ RDEPUBRenderRequest(渲染请求模型) │
│ │
│ 服务层 │
│ RDEPUBResourceResolver(资源 URL 统一入口) │
│ RDEPUBResourceURLSchemeHandler(ss-reader:// 协议) │
│ RDEPUBPreferences(展示参数聚合) │
│ RDEPUBPaginator(离屏分页服务) │
│ RDEPUBStyleSheetBuilder / RDEPUBJavaScriptBridge │
│ RDEPUBFixedLayoutTemplate / RDEPUBAssetRepository │
│ │
│ 会话与导航 │
│ RDEPUBReadingSession(状态机 + 会话协调) │
│ RDEPUBNavigatorState(状态枚举) │
│ RDEPUBNavigatorLayoutContext │
│ │
│ WebView 渲染 │
│ RDEPUBWebView(+Configuration / +Reflowable / │
│ +FixedLayout / +JavaScriptBridge / │
│ +Search) │
│ RDEPUBWebViewDebug(调试日志工具) │
│ │
│ 搜索 │
│ RDEPUBSearchEngine(协议)/ RDEPUBHTMLSearchEngine │
│ RDEPUBSearchModels(SearchMatch/Result/State/Presentation)│
└───────────────────────────┬─────────────────────────────┘
│
┌───────────────────────────▼─────────────────────────────┐
│ EPUBTextRendering 层(文本 EPUB 渲染) │
│ │
│ 渲染 │
│ RDEPUBTextRenderer(协议) │
│ RDEPUBDTCoreTextRenderer(DTCoreText 实现) │
│ RDPlainTextBookBuilder(纯文本书籍构建) │
│ RDEPUBTextPositionConverter(位置转换器) │
│ RDEPUBTextSearchEngine(文本搜索引擎) │
│ RDEPUBTextIndexTable / RDEPUBChapterData │
│ │
│ BuildPipeline/(构建管线) │
│ RDEPUBTextBookBuilder(分页书籍构建器) │
│ RDEPUBTextBookCache / RDEPUBTextBookModels │
│ RDEPUBTextBuildPipelineInterfaces(管线协议) │
│ RDEPUBPaginationCacheCoordinator(缓存协调) │
│ RDEPUBChapterTailNormalizer(章尾规范化) │
│ RDEPUBBuildDiagnosticsReporter(诊断报告) │
│ RDEPUBTextPerformanceSampler(性能采样) │
│ │
│ Pagination/(分页引擎) │
│ RDEPUBTextLayouter / RDEPUBTextLayoutFrame │
│ RDEPUBChapterPageCounter / RDEPUBCoreTextPageFrameFactory│
│ RDEPUBPageBreakPolicy(断页策略) │
│ RDEPUBTextPaginationInterfaces(分页协议) │
│ RDEPUBTextPaginationSupport(分页支持) │
│ │
│ Typesetter/(排版管线) │
│ RDEPUBTypesettingPipeline(排版管线编排) │
│ RDEPUBHTMLNormalizer(HTML 规范化) │
│ RDEPUBStyleSheetComposer(CSS 组合) │
│ RDEPUBFontNormalizer(字体规范化) │
│ RDEPUBAttachmentNormalizer(附件规范化) │
│ RDEPUBFragmentMarkerInjector(Fragment 标记注入) │
│ RDEPUBSemanticMarkerInjector(语义标记注入) │
│ RDEPUBRenderDiagnosticsCollector(渲染诊断) │
│ RDEPUBTextRendererSupport(渲染辅助工具) │
└──────────────────────────────────────────────────────────┘
3. 翻页容器层(RDReaderView)
3.1 三种翻页模式
| 模式 | 实现方式 | 特点 |
|---|---|---|
pageCurl |
UIPageViewController | 原生翻书效果,手势由系统提供 |
horizontalScroll |
UICollectionView + RDReaderFlowLayout | 每屏显示 2 项,水平分页滚动 |
verticalScroll |
UICollectionView + RDReaderFlowLayout | 全宽项目,垂直连续滚动 |
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 层依赖