diff --git a/Doc/ARCHITECTURE.md b/Doc/ARCHITECTURE.md index 29e35d6..829ed13 100644 --- a/Doc/ARCHITECTURE.md +++ b/Doc/ARCHITECTURE.md @@ -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+ -- **依赖**:ZIPFoundation(EPUB 解压)、DTCoreText(文本 EPUB 渲染) -- **Demo 额外依赖**:SnapKit、SSAlertSwift +**技术栈:** +- 语言:Swift 5,最低 iOS 15.6 +- 依赖:DTCoreText(HTML→NSAttributedString)、ZIPFoundation(EPUB 解压)、SnapKit(Auto Layout)、SSAlertSwift(弹窗) +- 构建:CocoaPods,本地 pod 引用 --- -## 2. 总体分层 +## 2. 分层架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ ReadViewDemo (Demo App) │ +│ ViewController · LaunchAutomationPlan · UITests │ +└──────────────────────────────┬──────────────────────────────┘ + │ imports RDReaderView +┌──────────────────────────────▼──────────────────────────────┐ +│ EPUBUI 层 │ +│ RDEPUBReaderController · Coordinators · Settings · TextPage │ +│ ChapterRuntime(Loader · Store · DiskCache · PageMap) │ +└──────────────────────────────┬──────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────┐ +│ RDReaderView 层 │ +│ RDReaderView · FlowLayout · PreloadController │ +│ SpreadResolver · TapRegionHandler · PagingController │ +└──────────────────────────────┬──────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────┐ +│ EPUBTextRendering 层 │ +│ TypesetterPipeline · DTCoreTextRenderer · Pagination │ +│ BuildPipeline(TextBookBuilder · 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 // 内联 、收集图片诊断 + ├─ 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 → 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(渲染辅助工具) │ -└──────────────────────────────────────────────────────────┘ +│ Tier 1: 内存缓存 (RDEPUBChapterRuntimeStore) │ +│ ├─ chapterDataCache: [Int: RDEPUBRuntimeChapter] │ +│ ├─ pageCountCache: [CacheKey: RDEPUBRuntimePageCount] │ +│ ├─ imageCache: NSCache (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:)` 会完全销毁并重建底层 view(pageViewController 或 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/ + │ ├─ 映射: 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/` 访问,由 `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`,避免运行中参数漂移 | diff --git a/Doc/BUSINESS_LOGIC.md b/Doc/BUSINESS_LOGIC.md new file mode 100644 index 0000000..81e7840 --- /dev/null +++ b/Doc/BUSINESS_LOGIC.md @@ -0,0 +1,546 @@ +# ReadViewSDK 业务逻辑文档 + +> 最后更新:2026-06-04 + +--- + +## 1. EPUB 解析流程 + +### 1.1 文件解压 + +**入口:** `RDEPUBParser.parse(epubURL:)` + +EPUB 文件本质是 ZIP 压缩包。解压流程: + +1. 计算缓存目录:`~/Library/Caches/ssreaderview-epub/{slug}-{fileSize}-{modifiedTimestamp}/` +2. 如果缓存目录已存在且包含 `META-INF/container.xml`,跳过解压 +3. 否则使用 `ZIPFoundation` 解压到缓存目录 +4. 解压是幂等的,重复调用不会重复解压 + +### 1.2 OPF 解析 + +OPF(Open Packaging Format)是 EPUB 的核心描述文件。使用 SAX 解析(`XMLParser` + `XMLParserDelegate`)以降低内存占用。 + +**解析内容:** + +| 区域 | 提取字段 | +|------|----------| +| `` | identifier, title, author, language, version, rendition:layout, rendition:spread, readingProgression | +| `` | id, href, media-type, properties, fallback, media-overlay | +| `` | idref, linear, properties, page-spread | +| `` | NCX 文件 id | + +**Spine 构建:** 将 spine 引用映射到 manifest 条目,规范化 href 相对于 OPF 目录,解析每个条目的布局覆盖(rendition:layout-pre-paginated/reflowable)。 + +### 1.3 目录解析 + +TOC(目录)按优先级尝试三种来源: + +1. **NCX(EPUB 2):** 解析 `` 树形结构,支持嵌套 +2. **Navigation Document(EPUB 3):** 解析 `