# 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 数据源协议 ```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/` 访问,由 `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 层依赖