- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
621 lines
20 KiB
Markdown
621 lines
20 KiB
Markdown
# ReadViewSDK 业务逻辑文档
|
||
|
||
> 最后更新:2026-06-09
|
||
|
||
---
|
||
|
||
## 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`)以降低内存占用。
|
||
|
||
**解析内容:**
|
||
|
||
| 区域 | 提取字段 |
|
||
|------|----------|
|
||
| `<metadata>` | identifier, title, author, language, version, rendition:layout, rendition:spread, readingProgression |
|
||
| `<manifest>` | id, href, media-type, properties, fallback, media-overlay |
|
||
| `<spine>` | idref, linear, properties, page-spread |
|
||
| `<spine toc="...">` | NCX 文件 id |
|
||
|
||
**Spine 构建:** 将 spine 引用映射到 manifest 条目,规范化 href 相对于 OPF 目录,解析每个条目的布局覆盖(rendition:layout-pre-paginated/reflowable)。
|
||
|
||
### 1.3 目录解析
|
||
|
||
TOC(目录)按优先级尝试三种来源:
|
||
|
||
1. **NCX(EPUB 2):** 解析 `<navPoint>` 树形结构,支持嵌套
|
||
2. **Navigation Document(EPUB 3):** 解析 `<nav epub:type="toc">` 中的 `<ol>/<li>/<a>` 结构
|
||
3. **回退:** 直接使用 spine 条目的 title 列表
|
||
|
||
### 1.4 阅读配置文件判断
|
||
|
||
```swift
|
||
enum RDEPUBReadingProfile {
|
||
case webInteractive // 通用 EPUB,通过 WebView 渲染
|
||
case webFixedLayout // 固定布局 EPUB(漫画、杂志)
|
||
case textReflowable // 文本重排,通过 CoreText 渲染(本 SDK 核心路径)
|
||
}
|
||
```
|
||
|
||
判断逻辑(`RDEPUBParser+ReadingProfile.swift`):
|
||
- `rendition:layout == pre-paginated` → `.webFixedLayout`
|
||
- 纯 HTML/XHTML 内容且无复杂交互 → `.textReflowable`
|
||
- 其他 → `.webInteractive`
|
||
|
||
---
|
||
|
||
## 2. 文本渲染管线
|
||
|
||
### 2.1 HTML 预处理(Typesetter Pipeline)
|
||
|
||
`RDEPUBTextTypesetterPipeline.makeRequest(from:)` 按顺序执行 8 个阶段:
|
||
|
||
**阶段 1:HTML 规范化** (`RDEPUBHTMLNormalizer`)
|
||
- 删除 `<hr lang="zh-CN">分页符</hr>` 分页标记
|
||
- CR → LF,合并连续空行
|
||
- 规范化附件 HTML 标记:
|
||
- `div.qrbodyPic / div.bodyPic` → 合并样式到 `<img>`
|
||
- `img.qqreader-footnote` → 行内 1em×1em
|
||
- `h1.frontCover > img` → 封面图片 100% 宽度
|
||
|
||
**阶段 2:语义标记注入** (`RDEPUBSemanticMarkerInjector`)
|
||
- 遍历所有 HTML 标签,维护开标签栈
|
||
- 为有分页语义的标签注入标记:
|
||
- `${rd-sem-start:id=X;block=...;hints=...;placement=...}`
|
||
- `${rd-sem-end:id=X}`
|
||
- 空标签(img, br, hr)同时注入开始和结束标记
|
||
|
||
**阶段 3:样式表内联** (`RDEPUBRenderDiagnosticsCollector`)
|
||
- 查找 `<link rel=stylesheet href=...>`
|
||
- 读取 CSS 文件内容
|
||
- 重写 CSS 中的相对 `url()` 引用
|
||
- 内联到 HTML 的 `<head>` 中
|
||
|
||
**阶段 4:CSS 层合成** (`RDEPUBStyleSheetComposer`)
|
||
- 构建 5 层 CSS,按优先级从低到高:
|
||
1. **default** - 基础阅读器样式(隐藏 head/title/style,默认字体大小)
|
||
2. **replace** - 微信读书风格格式化(代码块、标题、引用)
|
||
3. **dark** - 暗色模式覆盖(仅暗色主题时注入)
|
||
4. **epub** - EPUB 自带样式
|
||
5. **user** - 用户设置(字号、行距、颜色,!important)
|
||
- 语言检测:检查 lang 属性和文本采样,拉丁语言使用专用 CSS
|
||
|
||
**阶段 5:字体注册** (`RDEPUBFontNormalizer`)
|
||
- 解析 `@font-face { url(...) }` 块
|
||
- 通过 `CTFontManagerRegisterFontsForURL(.process)` 注册嵌入字体
|
||
- 已注册字体路径缓存在 `registeredFontPaths` 集合中,避免重复注册
|
||
|
||
**阶段 6:Base URL 注入** (`RDEPUBHTMLNormalizer`)
|
||
- 注入 `<base href="...">` 用于相对路径解析
|
||
|
||
**阶段 7:Fragment 标记注入** (`RDEPUBFragmentMarkerInjector`)
|
||
- 查找 `<tag id="xxx">` 标签
|
||
- 在其前面注入 `${id=xxx}` 标记
|
||
|
||
**阶段 8:图片诊断收集** (`RDEPUBRenderDiagnosticsCollector`)
|
||
- 扫描 `<img src="...">` 标签
|
||
- 解析引用路径,检查文件是否存在
|
||
- 收集诊断信息用于调试
|
||
|
||
### 2.2 HTML→NSAttributedString
|
||
|
||
`RDEPUBDTCoreTextRenderer.renderChapter(request:)`:
|
||
|
||
1. 将 HTML 编码为 `Data`
|
||
2. 使用 `DTHTMLAttributedStringBuilder` 构建 `NSAttributedString`
|
||
3. 在 `willFlushCallback` 中对每个 DOM 元素调用 `RDEPUBAttachmentNormalizer.prepareHTMLElementForReaderRendering()`
|
||
4. 后处理:
|
||
- `applyPaginationSemantics()` - 将 `${rd-sem-start/end}` 标记转为 NSAttributedString 属性
|
||
- `extractFragmentOffsets()` - 提取 fragment ID→offset 映射,删除标记文本
|
||
- `normalizeReadingAttributes()` - 规范化字体、行距、颜色、附件
|
||
|
||
### 2.3 分页计算
|
||
|
||
`RDEPUBChapterPageCounter.layoutFrames()` 使用迭代循环:
|
||
|
||
```
|
||
location = 0
|
||
while location < totalLength:
|
||
1. 创建 CTFrame(通过 CTFramesetter)
|
||
2. 获取可见范围 (CTFrameGetVisibleStringRange)
|
||
3. 应用 avoidPageBreakInside 规则(最多移除 3 行尾部)
|
||
4. 应用 keepWithNext 规则(最多移除 3 行尾部)
|
||
5. 应用 widow/orphan 控制
|
||
6. 应用 pageBreakPolicy 调整
|
||
7. 记录页面范围
|
||
8. location = 调整后的范围末尾
|
||
```
|
||
|
||
**分页规则优先级:**
|
||
|
||
| 规则 | 说明 | 最大调整行数 |
|
||
|------|------|-------------|
|
||
| avoidPageBreakInside | 块内不分页(标题、图片等) | 3 行 |
|
||
| keepWithNext | 标题与正文不分离 | 3 行 |
|
||
| widow control | 段落最后一行不留到下一页 | 1 行 |
|
||
| orphan control | 段落第一行不单独在上一页 | 1 行 |
|
||
| pageRelate | 微信读书跨页关联 | 1 行 |
|
||
| attachment boundary | 块级图片前后分页 | 0(精确切分) |
|
||
|
||
### 2.4 尾页规范化
|
||
|
||
`RDEPUBChapterTailNormalizer.normalize()` 三遍处理:
|
||
|
||
1. **删除空白中间帧:** 无可见字符且无附件的帧
|
||
2. **删除空白尾部帧:** 从末尾开始删除同类空白帧
|
||
3. **合并短尾帧:** 如果最后一帧 ≤2 个可见字符且前一帧 ≥8 倍长,合并
|
||
|
||
---
|
||
|
||
## 3. 后台元数据解析(大书优化)
|
||
|
||
### 3.1 整体流程
|
||
|
||
对于《凡人修仙传》这样的大书(~1000 章),后台解析是核心性能路径。
|
||
|
||
```
|
||
打开书籍
|
||
│
|
||
├─ 尝试磁盘缓存恢复(restoreBookPageMapIfPossible)
|
||
│ └─ 成功 → 直接显示完整页码
|
||
│
|
||
├─ 快速打开(3-5 章窗口)
|
||
│ ├─ 同步加载首个可渲染章节
|
||
│ ├─ 加载窗口内相邻章节
|
||
│ └─ 应用局部 BookPageMap → 用户可立即阅读
|
||
│
|
||
└─ 后台解析(RDEPUBMetadataParseWorker.start())
|
||
├─ 预计算所有章节 contentHash(串行,~1-3s)
|
||
├─ 恢复已有磁盘缓存
|
||
├─ 等待用户操作冷却 0.8s
|
||
├─ 并发渲染未缓存章节(OperationQueue,N=CPU核心数)
|
||
├─ 每 32 章增量刷新 UI
|
||
└─ 最终完整刷新
|
||
```
|
||
|
||
### 3.2 预计算 contentHash 优化
|
||
|
||
**问题:** 每个章节在渲染后需要构建缓存键,原始实现会重新读取 HTML 并计算 SHA-256。
|
||
|
||
**优化:** 在后台解析开始时,串行预计算所有章节的 contentHash:
|
||
|
||
```swift
|
||
var contentHashBySpineIndex: [Int: String] = [:]
|
||
for spineIndex in allBuildableIndices {
|
||
let html = parser.htmlString(forRelativePath: href)
|
||
contentHashBySpineIndex[spineIndex] = html?.rd_sha256Hex ?? ""
|
||
}
|
||
```
|
||
|
||
后续所有 `chapterCacheKey` 调用都使用预计算值,避免重复 I/O 和 SHA-256 计算。
|
||
|
||
### 3.3 冻结 renderSignature
|
||
|
||
**问题:** 如果用户在后台解析进行中更改字号/行距,`context.currentRenderSignature()` 会返回新值,导致部分章节用旧签名、部分用新签名写入磁盘缓存,造成缓存不一致。
|
||
|
||
**解决:** 在 `RDEPUBMetadataParseWorker` 初始化时冻结签名:
|
||
|
||
```swift
|
||
let renderSignature = context.currentRenderSignature()
|
||
// 后续所有 chapterCacheKey 调用使用此固定值
|
||
```
|
||
|
||
token 机制确保设置变更会触发新的解析任务(新 token),旧任务自动废弃。
|
||
|
||
### 3.4 锁区瘦身
|
||
|
||
**原始实现:** `resultLock` 内调用 `buildPageMap()`,该方法遍历全量 catalog 和 summaries。
|
||
|
||
**优化:** 锁内只做写入和计数,快照数据后锁外构建 pageMap:
|
||
|
||
```swift
|
||
var snapshot: [Int: RDEPUBChapterSummary]?
|
||
resultLock.lock()
|
||
summariesBySpineIndex[spineIndex] = renderResult
|
||
totalResolvedCount += 1
|
||
if shouldRefresh {
|
||
snapshot = summariesBySpineIndex // 快照
|
||
}
|
||
resultLock.unlock()
|
||
|
||
if let snapshot {
|
||
let partialMap = buildPageMap(from: catalog, summaries: snapshot) // 锁外
|
||
}
|
||
```
|
||
|
||
### 3.5 可配置刷新间隔
|
||
|
||
```swift
|
||
static var pageMapRefreshInterval: Int = 32 // 默认 32 章
|
||
```
|
||
|
||
可实测调优:32 / 48 / 64。值越大,UI 刷新频率越低,后台解析吞吐越高。
|
||
|
||
---
|
||
|
||
## 4. 章节按需加载
|
||
|
||
### 4.1 加载优先级
|
||
|
||
`RDEPUBChapterLoader.loadChapter(spineIndex:store:)` 按以下优先级:
|
||
|
||
1. **Tier 1 内存缓存** - `store.chapterData(for: spineIndex)` → 直接返回
|
||
2. **Tier 1 页数缓存** - `store.pageCount(for: cacheKey)` → 跳过分页计算
|
||
3. **Tier 2 磁盘缓存** - `summaryDiskCache.read(for: cacheKey)` → 恢复页范围
|
||
4. **全量构建** - 渲染 + 分页 + 写缓存
|
||
|
||
### 4.2 窗口驱逐策略
|
||
|
||
`RDEPUBChapterRuntimeStore.setCurrentChapter(spineIndex:totalSpineCount:windowRadius:)`:
|
||
|
||
- 保留范围:`[spineIndex - windowRadius, spineIndex + windowRadius]`
|
||
- 窗口半径由 `configuration.chapterWindowRadius` 控制
|
||
- 超出窗口的章节从内存缓存中驱逐
|
||
- 内存警告时驱逐除当前章节外的所有缓存
|
||
|
||
### 4.3 BookPageMap 增量刷新
|
||
|
||
`RDEPUBBookPageMap` 是轻量页码映射(~100KB/1000 章),不持有 `NSAttributedString`。
|
||
|
||
Builder 模式支持增量构建:
|
||
|
||
```swift
|
||
var builder = RDEPUBBookPageMap.Builder()
|
||
for item in catalog {
|
||
if let summary = summaries[item.spineIndex] {
|
||
builder.add(spineIndex:href:title:pageCount:fragmentOffsets:)
|
||
}
|
||
}
|
||
return builder.build()
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 翻页容器逻辑
|
||
|
||
### 5.1 三种翻页模式
|
||
|
||
| 模式 | 底层实现 | 特点 |
|
||
|------|----------|------|
|
||
| pageCurl | UIPageViewController | 翻页动画,系统手势 |
|
||
| horizontalScroll | UICollectionView + 自定义 Layout | 水平滑动,分页锁定 |
|
||
| verticalScroll | UICollectionView + 自定义 Layout | 垂直滚动,连续滚动 |
|
||
|
||
### 5.2 双页展开(Landscape Dual Page)
|
||
|
||
**触发条件:** `landscapeDualPageEnabled && isLandscape && !verticalScroll`
|
||
|
||
**页面配对逻辑(RDEpubReaderSpreadResolver):**
|
||
|
||
- 有封面页:封面独占一屏,后续页面两两配对
|
||
- 配对规则:`(coverIndex, nil)`, `(1, 2)`, `(3, 4)`, ...
|
||
- 无封面页:标准偶奇配对
|
||
- 配对规则:`(0, 1)`, `(2, 3)`, `(4, 5)`, ...
|
||
|
||
**封面感知布局(RDEpubReaderFlowLayout):**
|
||
|
||
```
|
||
封面页: [====全屏宽度====]
|
||
配对页: [==半宽==][==半宽==]
|
||
```
|
||
|
||
### 5.3 页面预加载
|
||
|
||
`RDEpubReaderPreloadController` 管理两个缓存:
|
||
- `preloadedPageViews` - 预渲染的页面视图
|
||
- `pageCurlCachedViews` - 当前显示的页面视图
|
||
|
||
**预加载策略:**
|
||
1. 每次页面变化后调用 `prime(around:preferredForward:)`
|
||
2. 计算预测目标:前后各 `radius` 个展开页 + 预测方向额外 1 个展开页
|
||
3. 将目标页面预渲染到隐藏的 `preloadHostView`
|
||
4. 缓存签名(displayType + isLandscape + pagesPerScreen + boundsSize)变化时清除缓存
|
||
|
||
### 5.4 点击区域处理
|
||
|
||
屏幕三等分:
|
||
|
||
```
|
||
[ 左 1/3 ][ 中 1/3 ][ 右 1/3 ]
|
||
上一页 切换工具栏 下一页
|
||
```
|
||
|
||
RTL 模式下左右互换。工具栏显示时,左右区域变为 `.center`(隐藏工具栏)。
|
||
|
||
---
|
||
|
||
## 6. 标注系统
|
||
|
||
### 6.1 数据模型
|
||
|
||
```swift
|
||
// 统一标注类型
|
||
struct RDEPUBAnnotation {
|
||
let id: String
|
||
let bookIdentifier: String?
|
||
let kind: RDEPUBAnnotationKind // .bookmark, .highlight, .underline
|
||
let location: RDEPUBLocation
|
||
let text: String?
|
||
let rangeInfo: String? // CoreText 选区范围信息
|
||
let chapterTitle: String?
|
||
let createdAt: Date
|
||
// computed: bookmark, highlight
|
||
}
|
||
|
||
// 书签
|
||
struct RDEPUBBookmark {
|
||
let id: String
|
||
let bookIdentifier: String?
|
||
let location: RDEPUBLocation
|
||
let chapterTitle: String?
|
||
let note: String?
|
||
let createdAt: Date
|
||
}
|
||
|
||
// 高亮
|
||
struct RDEPUBHighlight {
|
||
let id: String
|
||
let bookIdentifier: String?
|
||
let location: RDEPUBLocation
|
||
let text: String
|
||
let style: RDEPUBHighlightStyle // .highlight, .underline
|
||
let color: String
|
||
let note: String?
|
||
let rangeInfo: String?
|
||
let createdAt: Date
|
||
}
|
||
```
|
||
|
||
### 6.2 选区处理流程
|
||
|
||
SDK 支持两条选区处理路径,分别对应 WebView 渲染和原生文本渲染:
|
||
|
||
**路径 A:WebView 渲染(webInteractive / webFixedLayout)**
|
||
|
||
```
|
||
用户长按 → WKWebView 选区变化
|
||
│
|
||
▼
|
||
JS Bridge: ssReaderSelectionChanged
|
||
│
|
||
▼
|
||
RDEPUBReaderAnnotationCoordinator
|
||
├─ 解析选区位置 (RDEPUBLocation)
|
||
├─ 提取选中文本
|
||
├─ 创建 RDEPUBSelection
|
||
└─ 显示操作菜单(拷贝/高亮/批注)
|
||
```
|
||
|
||
**路径 B:原生文本渲染(textReflowable)**
|
||
|
||
```
|
||
用户长按 → RDEPUBTextContentView.handleLongPress(_:)
|
||
│
|
||
├─ layoutIfNeeded() // 确保布局完成
|
||
│
|
||
▼
|
||
RDEPUBTextSelectionController.handleLongPress(_:)
|
||
├─ RDEPUBPageInteractionController.characterIndexForViewPoint()
|
||
│ └─ CoreText CTLineGetStringIndexForPosition 字符命中
|
||
├─ 计算选区范围 (NSRange)
|
||
├─ isSelecting = true
|
||
│
|
||
▼
|
||
用户拖拽 → handlePan(_:)
|
||
├─ 更新选区范围
|
||
├─ RDEPUBSelectionOverlayView 绘制选区高亮
|
||
└─ .ended/.cancelled/.failed → isSelecting = false(保留选区)
|
||
│
|
||
▼
|
||
RDEPUBTextContentView delegate → RDEPUBReaderAnnotationCoordinator
|
||
├─ 创建 RDEPUBSelection(含 rangeInfo)
|
||
└─ 显示操作菜单(拷贝/高亮/批注)
|
||
```
|
||
|
||
### 6.3 高亮渲染
|
||
|
||
文本模式下,高亮通过 NSAttributedString 属性注入:
|
||
|
||
```swift
|
||
// 注入高亮属性
|
||
attributedString.addAttribute(
|
||
kRDEPUBHighlightAttributeName, // "com.rdreader.highlight"
|
||
value: highlight,
|
||
range: nsRange
|
||
)
|
||
|
||
// 注入下划线属性
|
||
attributedString.addAttribute(
|
||
kRDEPUBUnderlineAttributeName, // "com.rdreader.underline"
|
||
value: highlight,
|
||
range: nsRange
|
||
)
|
||
```
|
||
|
||
CoreText 渲染时识别这些自定义属性并绘制高亮背景/下划线。
|
||
|
||
---
|
||
|
||
## 7. 搜索系统
|
||
|
||
### 7.1 全文搜索
|
||
|
||
SDK 提供两个搜索引擎,分别服务于不同的渲染路径:
|
||
|
||
**文本渲染路径:`RDEPUBTextSearchEngine`**
|
||
|
||
`RDEPUBTextSearchEngine.search(keyword:)`:
|
||
|
||
1. 遍历 `RDEPUBTextBook.chapters` 中的所有章节
|
||
2. 对每个章节的 `attributedContent`(已渲染的 NSAttributedString)执行搜索
|
||
3. 使用 `NSString.range(of:options:.caseInsensitive)` 进行大小写不敏感搜索
|
||
4. 为每个匹配生成:
|
||
- `progression`:0.0-1.0 的阅读进度
|
||
- `previewText`:匹配位置前后各 12 字符
|
||
- `rangeAnchor`:精确的文本锚点(RDEPUBTextRangeAnchor)
|
||
|
||
**纯文本文件搜索:`RDEPUBTextSearchEngine.searchWithoutPublication(textBook:keyword:)`**
|
||
|
||
静态方法,用于 `.txt` 文件(有 TextBook 但无 Publication)的搜索:
|
||
|
||
1. 遍历 TextBook 章节的 attributedContent
|
||
2. 跳过 publication 依赖的 href 规范化
|
||
3. 不生成 rangeAnchor(纯文本无 CoreText 锚点)
|
||
|
||
**WebView 渲染路径:`RDEPUBHTMLSearchEngine`**
|
||
|
||
1. 遍历所有 linear spine 条目(html/xhtml 类型)
|
||
2. 读取 HTML,转为纯文本(正则去除标签)
|
||
3. 执行大小写不敏感搜索
|
||
4. 通过 JS Bridge 在 WebView DOM 中绘制搜索高亮
|
||
|
||
### 7.2 搜索结果导航
|
||
|
||
搜索结果通过 `RDEPUBSearchState` 管理:
|
||
|
||
```swift
|
||
struct RDEPUBSearchState {
|
||
let keyword: String
|
||
let matches: [RDEPUBSearchMatch]
|
||
var currentMatchIndex: Int?
|
||
}
|
||
```
|
||
|
||
**搜索结果导航流程:**
|
||
|
||
`RDEPUBReaderSearchCoordinator` 协调搜索结果的前进/后退导航:
|
||
|
||
1. `goToNextMatch()` / `goToPreviousMatch()` 更新 `currentMatchIndex`
|
||
2. 根据匹配位置计算目标页码,触发页面跳转
|
||
3. 通知 `RDEPUBReaderController` 更新搜索计数显示
|
||
|
||
**搜索结果渲染:**
|
||
|
||
- **WebView 路径:** 通过 JS Bridge 接收搜索高亮数据,在 DOM 中绘制高亮矩形
|
||
- **原生文本路径:** `RDEPUBTextContentView` 根据 `rangeAnchor` 在 CoreText 绘制层高亮匹配文本
|
||
|
||
**搜索栏生命周期:**
|
||
|
||
搜索栏采用延迟安装模式:
|
||
|
||
1. `showSearchBar()` 设置 `isSearchBarVisible = true`,仅在工具栏可见时立即安装视图
|
||
2. 若工具栏隐藏,延迟到 `handleToolViewVisibilityChanged(isVisible: true)` 时安装
|
||
3. `installSearchBarView()` 负责实际的视图层级添加、约束、动画和焦点管理
|
||
|
||
---
|
||
|
||
## 8. 设置与主题
|
||
|
||
### 8.1 配置变更流程
|
||
|
||
```
|
||
用户修改设置
|
||
│
|
||
▼
|
||
RDEPUBReaderConfiguration 更新
|
||
│
|
||
├─ 字号/字体/行距变更 → 触发重新分页
|
||
│ └─ renderSignature 变化 → 磁盘缓存失效 → 后台重新解析
|
||
│
|
||
├─ 主题变更 → 刷新可见内容
|
||
│ └─ CSS 层重建(dark/user 层)
|
||
│
|
||
├─ 显示模式变更 → 切换翻页容器
|
||
│ └─ switchReaderDisplayType()
|
||
│
|
||
└─ 列数变更 → 触发重新分页
|
||
└─ layoutConfig.cacheSignature 变化
|
||
```
|
||
|
||
### 8.2 主题系统
|
||
|
||
6 种内置主题,通过 `RDEPUBReaderThemeSelector` 选择:
|
||
|
||
| 主题 | 背景色 | 文字色 |
|
||
|------|--------|--------|
|
||
| 默认白 | #FFFFFF | #333333 |
|
||
| 护眼绿 | #CCE8CF | #333333 |
|
||
| 牛皮纸 | #E8D8B8 | #333333 |
|
||
| 深灰 | #333333 | #CCCCCC |
|
||
| 纯黑 | #000000 | #B4B4B6 |
|
||
| 暗蓝 | #1A2332 | #8A9BB5 |
|
||
|
||
暗色主题(深灰/纯黑/暗蓝)注入 `wxread-dark.css` 覆盖层。
|
||
|
||
### 8.3 持久化策略
|
||
|
||
设置通过 `RDEPUBReaderPersistence` 协议持久化:
|
||
|
||
```swift
|
||
protocol RDEPUBReaderPersistence {
|
||
func saveLocation(_ location: RDEPUBLocation, for bookIdentifier: String)
|
||
func loadLocation(for bookIdentifier: String) -> RDEPUBLocation?
|
||
func saveBookmarks(_ bookmarks: [RDEPUBBookmark], for bookIdentifier: String)
|
||
func loadBookmarks(for bookIdentifier: String) -> [RDEPUBBookmark]
|
||
func saveHighlights(_ highlights: [RDEPUBHighlight], for bookIdentifier: String)
|
||
func loadHighlights(for bookIdentifier: String) -> [RDEPUBHighlight]
|
||
}
|
||
```
|
||
|
||
默认实现使用 `UserDefaults`,键格式:
|
||
- 位置:`ssreader.epub.location.{bookID}`
|
||
- 书签:`ssreader.epub.bookmarks.{bookID}`
|
||
- 高亮:`ssreader.epub.highlights.{bookID}`
|
||
- 设置:`ssreader.epub.settings`
|
||
|
||
---
|
||
|
||
## 9. 纯文本 (.txt) 支持
|
||
|
||
`RDEpubPlainTextBookBuilder` 复用 EPUB 渲染管线处理 .txt 文件:
|
||
|
||
1. **解码:** 依次尝试 UTF-8 → GB18030 → GBK
|
||
2. **分章:** 正则匹配 `^(第[零一二三四五六七八九十百千万\d]+[章节回卷].*)$`
|
||
3. **包装 HTML:** 每行包裹 `<p>` 标签
|
||
4. **渲染+分页:** 复用 `RDEPUBTextRenderer` 和 `RDEPUBCoreTextPageFrameFactory`
|
||
|
||
---
|
||
|
||
## 10. 性能关键路径
|
||
|
||
### 10.1 首次打开(冷启动)
|
||
|
||
| 阶段 | 耗时占比 | 优化方向 |
|
||
|------|----------|----------|
|
||
| ZIP 解压 | ~5% | 缓存解压目录 |
|
||
| OPF 解析 | ~1% | SAX 流式解析 |
|
||
| 首章渲染 | ~15% | 快速打开路径 |
|
||
| 后台全量解析 | ~70% | 并发、缓存、预计算 |
|
||
| 磁盘 I/O | ~9% | 异步写入、批量读取 |
|
||
|
||
### 10.2 二次打开(热缓存)
|
||
|
||
| 阶段 | 耗时占比 | 说明 |
|
||
|------|----------|------|
|
||
| 磁盘摘要恢复 | ~30% | readAll 批量读取 |
|
||
| BookPageMap 构建 | ~5% | Builder 模式 |
|
||
| UI 应用 | ~65% | 刷新 collection view |
|
||
|
||
### 10.3 关键优化项
|
||
|
||
| 优化 | 收益 | 文件 |
|
||
|------|------|------|
|
||
| 预计算 contentHash | 消除每章重复 HTML 读取 + SHA-256 | RDEPUBReaderPaginationCoordinator |
|
||
| 冻结 renderSignature | 避免参数漂移导致缓存不一致 | RDEPUBReaderContext |
|
||
| 锁区瘦身 | 降低并发锁竞争 | RDEPUBReaderPaginationCoordinator |
|
||
| 可配置并发数 | 适配不同设备 | RDEPUBReaderConfiguration |
|
||
| 可配置刷新间隔 | 平衡 UI 响应和吞吐 | RDEPUBReaderPaginationCoordinator |
|