15 KiB
EPUB 阅读模块维护与排查指南
1. 文档目的
本文档说明 EPUB 阅读模块的关键文件职责、主要数据流、常见问题排查和后续扩展建议,供维护时快速建立上下文。
完整架构设计请参考 ARCHITECTURE.md。 编码规范请参考 CODING_STYLE.md。
2. 关键文件速查
2.1 EPUBCore 层
| 文件 | 职责 |
|---|---|
RDEPUBParser.swift + 扩展 |
EPUB 解压、container.xml、OPF、manifest、spine、TOC/NCX 解析 |
RDEPUBPublication.swift |
解析结果的唯一聚合入口,外部不直接访问 parser 字段 |
RDEPUBModels.swift |
metadata / manifest / spine / TOC 基础模型 |
RDEPUBReadingModels.swift |
RDEPUBLocation、RDEPUBViewport、RDEPUBHighlight、选区模型 |
RDEPUBResourceResolver.swift |
href → fileURL / schemeURL 的统一入口,normalizedHref 标准化路径 |
RDEPUBPaginator.swift |
离屏 WKWebView 分页,计算每个 spine 资源被视口切成多少页 |
RDEPUBReadingSession.swift |
状态机 + 会话协调,管理 staged/active/pending 页模型 |
RDEPUBNavigatorState.swift |
状态枚举(initializing/loading/idle/jumping/moving/repaginating) |
RDEPUBWebView.swift + 扩展 |
承载 spine 资源的 WKWebView,分页 CSS 注入、JS bridge、fixed wrapper |
RDEPUBResourceURLSchemeHandler.swift |
ss-reader:// 协议,从本地解压目录提供所有 EPUB 资源 |
RDEPUBStyleSheetBuilder.swift |
生成注入 WebView 的 CSS |
RDEPUBJavaScriptBridge.swift |
JS ↔ Swift 消息定义和编解码 |
RDEPUBSearchEngine.swift |
搜索协议定义 + WebView 全文搜索引擎 |
RDEPUBSearchModels.swift |
搜索模型(SearchMatch/Result/State/Presentation) |
2.2 EPUBTextRendering 层
| 文件 | 职责 |
|---|---|
RDEPUBTextRenderer.swift |
渲染引擎协议 + 错误类型 + 样式 / 内容模型 |
RDEPUBDTCoreTextRenderer.swift |
DTCoreText 实现,HTML → NSAttributedString |
RDEPUBTextRendererSupport.swift |
fragment 标记注入、fragment offset 提取、属性标准化 |
RDEPUBTextPaginationSupport.swift |
NSAttributedString 切页算法 |
RDEPUBTextBookBuilder.swift |
驱动逐章节渲染和分页,生成 RDEPUBTextBook |
2.3 EPUBUI 层
| 文件 | 职责 |
|---|---|
RDEPUBReaderController.swift |
开箱即用读者控制器,整合 session / readerView / UI |
RDEPUBReaderConfiguration.swift |
外部配置项(字号、主题、翻页模式等) |
RDEPUBReaderPersistence.swift |
阅读位置和高亮的本地持久化 |
RDEPUBReaderSettings.swift |
运行时设置状态 |
RDEPUBWebContentView.swift |
WKWebView 内容视图,供 RDReaderView 渲染 WebView 类 spine |
RDEPUBTextContentView.swift |
文本内容视图,供 RDReaderView 渲染文本类 spine |
3. 资源服务协议说明
加载 spine 资源统一使用 ss-reader:// 协议,不直接使用 file://:
// 正确方式
let schemeURL = publication.resourceResolver.schemeURL(forHref: spineItem.href)
webView.load(URLRequest(url: schemeURL))
// 禁止方式(relative 资源、图片、CSS 会失效)
webView.loadHTMLString(htmlString, baseURL: nil)
这样 HTML 内部的相对 CSS、图片、字体和 iframe 资源都会通过同一个 schemeHandler 解析,避免 loadHTMLString 或 allowingReadAccessTo 导致的资源失效问题。
3.1 DTCoreText 实现逻辑(textReflowable 路径)
当 readingProfile == .textReflowable 时,渲染链路不再走 WebView 列分页,而是走 EPUBTextRendering/ 的 DTCoreText 文本渲染链。
端到端流程:
RDEPUBTextBookBuilder按 spine 顺序读取章节 HTML。RDEPUBTextRendererSupport.injectFragmentMarkers(into:)注入 fragment 标记,保证目录锚点可映射到文本偏移。RDEPUBDTCoreTextRenderer.renderChapter(html:baseURL:style:)执行章节渲染。- 渲染成功时,内部通过
DTHTMLAttributedStringBuilder将 HTML 转为NSAttributedString。 RDEPUBTextRendererSupport.extractFragmentOffsets(from:)提取fragment -> 字符偏移映射。RDEPUBTextRendererSupport.normalizeReadingAttributes(in:style:)统一字体、行距、颜色等阅读属性。RDEPUBTextPaginationSupport将富文本按可视区域切页,构建页模型。RDEPUBTextBookBuilder汇总章节与页映射,交给RDEPUBTextContentView呈现。
关键实现点:
- 条件编译:
#if canImport(DTCoreText)- 可用时走 DTCoreText 路径。
- 不可用时走 fallback(纯
NSAttributedString降级),确保工程可编译可运行。
dtOptions(baseURL:style:)会注入:- 默认字体族 / 字体名 / 字号
- 行高倍数(由字体行高与行距折算)
NSBaseURLDocumentOption(保证相对资源解析)- 默认文本颜色
- fragment 偏移在“属性标准化前后”都要保持稳定,避免目录跳转漂移。
- 文本分页以字符范围为准,不以 WebView 像素滚动位置为准。
失败与降级策略:
- HTML 转 UTF-8 失败:抛出
htmlEncodingFailed。 - DTCoreText builder 失败:进入 fallback 渲染路径。
- fallback 仍会执行 fragment 提取与阅读属性标准化,保证目录跳转和阅读样式能力不丢失。
与定位恢复的关系:
- 仍使用
RDEPUBLocation(href + progression)作为跨会话恢复模型。 - textReflowable 模式下,
fragmentOffsets用于将目录锚点映射到分页后的字符区间,再换算到页索引。 - 字号/行距变化触发重分页后,优先按
href + progression回落恢复,再用 fragment 做细化定位。
推荐日志点(DTCoreText 专用):
RDEPUBDTCoreTextRenderer.renderChapter(...):记录章节 href、输入 HTML 长度、渲染耗时。makeAttributedString(from:baseURL:style:):记录 DTCoreText 是否成功返回 attributed string。extractFragmentOffsets(from:):记录 fragment 数量与关键锚点是否命中。RDEPUBTextPaginationSupport:记录总页数、每页字符范围边界。
4. 常见问题排查
4.1 EPUB 打不开或解析失败
优先检查:
RDEPUBParser.parse(epubURL:)是否抛出异常,打印具体错误RDEPUBParser+Archive.swift— 解压是否成功META-INF/container.xml是否存在- OPF 中 manifest / spine 是否完整
常见原因:
- 文件不是标准 EPUB ZIP 结构
container.xml找不到 OPF 路径- OPF manifest 或 spine 格式异常
- 文件路径包含特殊字符导致解压目录路径错误
排查顺序:
- 确认 EPUB 已成功复制到沙盒
- 确认解压目录存在(
extractionRootURL) - 确认
opfDirectoryURL不为 nil - 确认
spine.count > 0
4.2 EPUB 显示空白页
优先检查:
RDEPUBWebContentView.loadPage(...)传入的 spineIndex 是否在范围内RDEPUBWebView+Reflowable— 分页 CSS 注入是否成功WKNavigationDelegate.didFinish是否正常触发RDEPUBResourceURLSchemeHandler是否正确返回资源数据
常见原因:
- scheme URL 拼装错误,资源请求 404
- 注入的 CSS 破坏了原始布局(尤其是
column-width样式冲突) - fixed-layout 书籍走了 reflowable 渲染路径(readingProfile 判定错误)
排查顺序:
- 确认
schemeURL(forHref:)返回的 URL 对应文件真实存在 - 用
RDEPUBWebViewDebug打开 WebView inspector 查看网络请求 - 确认 readingProfile 判定正确(webInteractive / webFixedLayout / textReflowable)
4.3 修改字号后位置恢复不准
原因: 字号变化触发重新分页(repaginating),页号失效。
正确做法:
当前实现依赖 href + progression 恢复位置,不依赖纯页号。排查重点:
RDEPUBLocation.href是否已通过normalizedHref标准化progression是否在字号变化后重新从 WebView 读取RDEPUBReadingSession.navigatorState是否在重分页后回到.idle并消费了 staged snapshot
4.4 目录点击后跳错位置
优先检查:
RDEPUBParser+TOC.swift— TOC href 是否正确标准化为相对 OPF 路径RDEPUBResourceResolver.normalizedHref(_:)— 跳转时是否使用了标准化 hreffragment是否正确传入 WebView 锚点滚动
常见原因:
- TOC 链接是相对 Nav 或 NCX 文件目录,未转为相对 OPF
href#fragment只命中了资源,未命中锚点- 同一资源中多个 TOC 条目被折叠成同一页
排查步骤:
- 打印原始 TOC href 和标准化后 href 进行比较
- 确认
fragment字段被保留在 RDEPUBLocation 中 - 确认 WebView 收到
fragment后执行了scrollToFragment(fragment:)JS 调用
4.5 正文内部链接不工作
优先检查:
RDEPUBWebView+JavaScriptBridge中内部链接拦截逻辑RDEPUBReaderController.navigateToLocation(_:)— 跨资源跳转路径
常见原因:
- 链接是空
#fragment,但当前 DOM 中找不到对应元素 - 跨资源链接 href 未被 normalizedHref 处理
- 对应 spine 资源没有分页结果(pageCounts[spineIndex] == 0)
4.6 图片多时位置跳动
原因: 图片异步加载导致 DOM 高度变化,进度报告时机早于实际布局稳定。
排查:
- 检查 JS
ResizeObserver是否在图片加载后触发了进度重同步 - 确认
didUpdateLocation不在 pending navigation 未消费前覆盖目标位置 - 确认
scrollToLocation()JS 调用时序在didFinish之后
4.7 高亮未显示或恢复失败
优先检查:
RDEPUBHighlight是否成功写入 persistenceRDEPUBReaderController.activeHighlights是否正确过滤出当前 spine 的高亮RDEPUBWebView+JavaScriptBridge中setHighlights()JS 调用是否触发- JS 中
rangeFromInfo()是否能找到对应 DOM 节点(嵌套节点恢复容易失败)
排查顺序:
- 确认有效选区(
currentSelection不为 nil) - 确认
addHighlight()调用后 persistence 中数据已写入 - 确认当前页传入的 highlights 数组不为空
- 确认 JS 收到了 highlights 数据并尝试了 rangeFromInfo 恢复
4.8 fixed-layout 书籍显示异常
优先检查:
RDEPUBPublication.readingProfile是否判定为.webFixedLayoutRDEPUBFixedLayoutTemplate— wrapper HTML 是否正确生成- viewport meta 尺寸是否与书籍 OPF 中声明一致
- spread 模式是否正确(单页 vs 双页)
5. 推荐日志点
遇到复杂问题时,建议先在以下位置加日志:
RDEPUBParser.parse(epubURL:)— 打印解压路径、OPF 路径、spine 数量RDEPUBReadingSession.transition(to:)— 打印状态跃迁RDEPUBPaginator— 打印每个 spine 资源的分页结果RDEPUBReaderController.restoreLocation(_:)— 打印恢复位置的 href / progressionRDEPUBWebView+JavaScriptBridge— 打印所有 JS 消息收发RDEPUBResourceURLSchemeHandler— 打印资源请求 URL 和响应状态
6. 标准排查顺序
当 EPUB 功能出问题时,建议按以下顺序排查:
- 解析是否成功 — parser.spine.count > 0?metadata 正确?
- readingProfile 是否正确 — 用了对的渲染路径?
- TOC href 是否标准化 — 与 spine href 格式一致?
- 分页结果是否合理 — 每个 spine 资源的 pageCount > 0?
- 资源是否能正常加载 — ss-reader:// 请求是否 200?
- JS bridge 是否正常 — progression 上报是否到达 Swift 侧?
- 状态机是否流转正确 — session.navigatorState 是否符合预期?
- 控制器是否正确映射页号 — EPUBPage 列表与 RDReaderView 页号对应是否正确?
7. 后续扩展建议
- 拆分 demo 层 RDReaderController:约 1900+ 行,EPUB 加载、UI、数据源职责仍混在一起,建议继续向 library 层迁移
- 基线验证:用四本样书跑一轮分页耗时、目录命中率、末页事件、设置恢复稳定性数据
- 横竖屏支持回归矩阵:控制器级
viewWillTransition()与 viewport 去重已接入,下一步重点是补齐 pageCurl / scroll、三条正文路径和 iPad 视口变化场景的固定回归 - Pod 分层:评估是否将 EPUBCore / EPUBTextRendering / EPUBUI 拆为独立 subspec
- RDEPUBParser 继续收敛:OPF metadata 细节处理仍偏重,可考虑按 OPFMetadataParser 单独拆出
8. 阅读器能力现状与下一阶段规划
8.1 当前已具备的核心能力
当前 SDK 已具备一套可用的 EPUB 阅读器主链路,包含:
- EPUB 打开与解析:支持 ZIP 解压、OPF / spine / TOC 解析,以及三类 readingProfile 分流
- 正文渲染与分页:支持 WebView 路径和 DTCoreText 文本路径,覆盖固定版式、交互式可重排和纯文本可重排 EPUB
- 完整阅读交互:支持目录跳转、翻页、阅读位置恢复、主题切换、字号和行高调整、亮度调整
- 标注能力:支持高亮、划线、批注、标注列表、编辑、删除、跳转与持久化恢复
- 书签能力:支持当前位置书签添加/取消、书签列表、删除、跳转与持久化恢复
- 自动化测试基础能力:已接入
ReadViewSDKDemoTests/ReadViewSDKDemoUITests、样书夹具定位工具,以及 parser / resolver / persistence 首批 XCTest 和阅读器启动 smoke 用例 - 搜索能力:支持全文搜索、匹配项跳转、当前匹配高亮
8.2 高优先级待补能力
从“可用 MVP”走向“稳定可交付阅读器”,当前最值得优先完成的是:
- 样书基线验证 需要基于四本样书补齐分页耗时、目录命中率、末页事件、设置恢复稳定性等基线数据
8.3 面向完整商业阅读器的下一阶段能力
若目标是继续向完整阅读产品演进,建议后续补齐以下能力:
- 阅读进度展示 包括全书进度、章节进度、页码信息、剩余页数或剩余章节等
- 更完整的标注管理 包括按章节分组、排序、筛选、批量删除、标注导出、复制批注内容等
- 更成熟的搜索体验 包括搜索结果列表页、章节聚合、上下文预览、搜索历史等
- 更多阅读增强能力 包括字体切换、自定义主题、翻页手势自定义、TTS 朗读、无障碍支持等
8.4 工程侧持续建设建议
除了阅读器功能本身,还建议并行推进以下工程项:
- 控制器继续拆分
RDEPUBReaderController与 demo 层控制器都偏大,继续叠加功能会增加维护成本 - Parser 与模块职责继续收敛
RDEPUBParser仍承担较多 OPF metadata 解析细节,后续可继续按职责拆分 - 回归验收矩阵固化 建议把样书、渲染路径、主题、字号、翻页模式、标注恢复等整理为固定回归清单
8.5 推荐下一阶段实施顺序
如果以“最小投入换最大稳定性提升”为目标,建议按下面顺序推进:
- 样书基线验证