ReadViewSDK/Doc/ARCHITECTURE.md
shen 948004eed1 docs: 补充注释、修正过时文档、清理重复内容
源码注释:
- 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法)
- 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块

文档维护:
- 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致)
- 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值)
- 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll
- 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录
- 更新 index.md 索引:新增开发计划和架构对比文档引用
2026-06-01 09:33:23 +08:00

24 KiB
Raw Blame History

RDReaderView 架构文档

1. 项目概览

RDReaderView 是一个 iOS 阅读器组件库CocoaPods提供开箱即用的 EPUB 阅读能力,并保留对纯文本翻页的支持。

  • 最低 iOS 版本15.0
  • Swift 版本5.10+
  • 依赖ZIPFoundationEPUB 解压、DTCoreText文本 EPUB 渲染)
  • Demo 额外依赖SnapKit、SSAlertSwift

2. 总体分层

┌─────────────────────────────────────────────────────────┐
│                      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渲染辅助工具              │
└──────────────────────────────────────────────────────────┘

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:) 会完全销毁并重建底层 viewpageViewController 或 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 高亮定位)
}

恢复流程:

  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 安装

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 层依赖