ReadViewSDK/Doc/WXRead/微信读书EPUB阅读器实现架构.md

561 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 读书 EPUB 阅读器实现架构分析
> 分析日期: 2026-05-18
> 基于读书 v10.0.3
---
## 核心结论:双渲染引擎架构
读书使用了 **两套渲染引擎**,根据内容类型选择不同的渲染路径:
```
┌─────────────────────────────────────────────────┐
│ WRReaderViewController │
│ (阅读器主控制器, 管理翻页和状态) │
├─────────────────────────────────────────────────┤
│ WRPageViewController │
│ (基于 UIPageViewController) │
│ 支持 UIPageCurl(仿真翻页) + Scroll(滑动) │
├──────────────────────┬──────────────────────────┤
│ 路径A: 原生渲染 │ 路径B: WebView 渲染 │
│ (EPUB/书籍正文) │ (公众号/文集文章) │
│ │ │
│ WREpubTypesetter │ WKWebView + JS Bridge │
│ DTCoreText │ weread-highlighter.js │
│ CoreText 排版 │ rangy-*.js │
│ NSAttributedString │ MediaPlatform.js/css │
│ WRCoreTextLayouter │ Readability.js │
│ WRPageView (draw) │ WRMPPageView │
└──────────────────────┴──────────────────────────┘
```
---
## 路径AEPUB 正文原生渲染 (核心路径)
这是 EPUB 阅读的**主要渲染方式**,完全用原生 CoreText 实现,不走 WebView。
### 1. EPUB 下载与解密流程
```
服务器 ZIP 包 (加密)
WRBookNetwork.loadTarForEpubBookId:chapter:isPreload:
WRBookNetwork.handleUnzipWithBookId:zipPath:encryptKey:plainBookDirectory:
│ - 使用 encryptKey 解密
│ - 解压到 plainBookDirectory
│ - 处理解压错误 handleUnzipErrorWithPath:
WRBookNetwork.processEncryptedBookFileAtPath:encryptKey:book:chapterUid:isFromReview:
WREncryptedFileManager.decryptContentsOfFile:forBookId:isFileLost:
│ - keyForBookId: 获取每本书的密钥
│ - 解密 EPUB 章节文件
WREncryptedFileManager.encryptFileForBookId:originalEncryptKey:atPath:toPath:
│ - 本地二次加密存储 (DRM 保护)
本地缓存: epubImage 目录 + 解密后的 XHTML 文件
```
**密钥管理**
- `WRPreloadBookManager.saveEncryptKey:forPath:bookId:` - 预加载章节密钥
- `WRPreloadBookManager.encryptKeyForPath:bookId:` - 读取密钥
- `WREncryptedFileManager.keyForBookId:` - 每本书独立密钥
### 2. EPUB 解析与排版流程
```
XHTML 章节文件
WRBookNetwork.fileContentWithChapter:book:shouldRemoveHtmlTags:filterTranslateContent:
│ - 读取 XHTML 内容
│ - 可选去除 HTML 标签
│ - 过滤翻译内容
WREpubTypesetter.attributeStringWithFilePath:priority:insertArticleToolAttachment:
insertBookChapterToolAttachment:insertRecommendView:book:chapter:
pageFlippingStyle:renderErrorReason:isStyleFileNotFound:options:
│ ┌─────────────────────────────────────────┐
│ │ DTHTMLAttributedStringBuilder │
│ │ (HTML -> NSAttributedString 转换器) │
│ │ │
│ │ 1. 解析 XHTML DOM 树 │
│ │ 2. 读取 EPUB 内嵌 CSS (replace.css) │
│ │ 3. 合并默认样式 (default.css) │
│ │ 4. 应用用户主题样式 (dark.css) │
│ │ 5. 转换为 NSAttributedString │
│ │ - 保留字体、颜色、行高、对齐等属性 │
│ │ - 处理图片 (NSTextAttachment) │
│ │ - 处理超链接 │
│ └─────────────────────────────────────────┘
NSAttributedString (富文本)
WRCoreTextLayouter (CoreText 排版引擎)
│ ┌─────────────────────────────────────────┐
│ │ DTCoreText 框架 (自定义修改版) │
│ │ │
│ │ DTCoreTextLayouter │
│ │ └─ CTTypesetter │
│ │ └─ CTFramesetter │
│ │ └─ DTCoreTextLayoutFrame │
│ │ └─ CTFrame (每页) │
│ │ └─ DTCoreTextLayoutLine │
│ │ └─ CTLine (每行) │
│ │ └─ DTCoreTextGlyphRun │
│ │ └─ CTLine (字形) │
│ │ │
│ │ 特殊处理: │
│ │ - wr-vertical-center-style (图片居中) │
│ │ - weread-page-relate (分页控制) │
│ │ - avoidPageBreakInside (避免断页) │
│ │ - 繁简转换 (convertHansToHant) │
│ └─────────────────────────────────────────┘
WRChapterData (章节数据模型)
│ - 包含排版后的 NSAttributedString
│ - 管理划线/高亮/书评等标注
│ - addHighlightInRange:key:itemId:color:
│ - addUnderLineToAttributedString:range:itemId:style:color:
│ - addReviewUnderlineInRange:itemId:type:
│ - generateOutlineContents (生成目录)
│ - freeTrialChapterCutOffStringLocaion (免费试读截断)
分页计算: WRChapterPageCount
│ - rangeValueWithPageInfo: 计算每页的 NSRange
│ - rangeOfPage: 获取指定页的文本范围
WRPageView (页面视图, UIView 子类)
│ - 继承 UIView
│ - drawRect: 中调用 CoreText 绘制
│ - WRCoreTextLayoutFrame.drawInContext:image:size:inRect:position:
│ - 直接用 CGContext 绘制文字和图片
│ - 不使用 UILabel/UITextView
屏幕显示 (像素级精确渲染)
```
### 3. 翻页机制
```
WRPageViewController
│ 基于 UIPageViewController 封装
│ 初始化: initWithDelegate:withPageType:pageFlippingStyle:
│ pageFlippingStyle 支持:
│ ┌────────────────────────────────────┐
│ │ UIPageCurl - 仿真翻页 (纸张卷曲) │
│ │ Scroll - 左右滑动翻页 │
│ └────────────────────────────────────┘
│ 核心方法:
│ - pageViewController:viewControllerBeforeViewController: (上一页)
│ - pageViewController:viewControllerAfterViewController: (下一页)
│ - pageViewController:spineLocationForInterfaceOrientation: (书脊位置)
│ - weread_setViewControllers:withCurlOfType:fromLocation:direction:
│ animated:notifyDelegate:completion: (自定义设置方法)
│ 故障修复:
│ - patchNavigationDirectionFault (导航方向修复)
│ - patchNoViewControllerManagingPageViewFault (页面管理修复)
│ - patchUIPageCurlFault (翻页动画修复)
│ - detectNavigationDirectionCrashWithPageViewController: (崩溃检测)
WRPageView (每个页面的渲染视图)
│ - 通过 WRChapterData 获取排版结果
│ - 通过 rangeOfPage: 获取当前页的文本范围
│ - 使用 CoreText 直接绘制到 CGContext
```
### 4. 文本选择与标注
```
用户触摸/长按
WRPageView 手势识别
文本位置计算 (CoreText hit test)
│ - CTLineGetStringIndexForPosition (坐标->字符索引)
│ - CTLineGetOffsetForStringIndex (字符索引->坐标)
选择范围确定
弹出操作菜单 (UIMenuController)
│ - 划线/高亮
│ - 写想法/书评
│ - 复制
│ - 查询/翻译
│ - 分享
WRChapterData 添加标注
│ - addHighlightInRange:key:itemId:color:
│ - addUnderLineToAttributedString:range:itemId:style:color:
│ - addReviewUnderlineInRange:itemId:type:
保存到服务器
│ - WRBookNetwork.addReview:shareToWechat:...
│ - 同步书签: loadBookmarkListWithBookId:syncKey:callback:
重新排版当前页 (recomposeCurrentPageViewWithSource:)
```
---
## 路径BWebView 渲染 (公众号/文集文章)
用于渲染**微信公众号文章、文集、书评**等富媒体内容。
```
HTML 内容 (来自服务器)
WRMPReadingManager.composeMPReviewHTMLString:withReview:
│ - 组装 HTML 模板
│ - 注入 CSS (MediaPlatform.css, MPExtra.css)
│ - 注入 JS (MediaPlatform.js, mpForArticle.js)
WKWebView 加载
│ 注入脚本:
│ <script src="WeReadApi.js"></script>
│ <script src="rich_display.js"></script>
weread-highlighter.js 初始化
│ 1. rangy.init() - 初始化 Rangy 选择库
│ 2. 创建 Highlighter (TextRange 模式)
│ 3. 注册 ClassApplier:
│ - "highlight" (高亮)
│ - "review" / "friend-review" (书评)
│ - "reference" (引用)
│ - "tts" (语音朗读标记)
│ 4. 监听 selectionchange 事件
│ 5. 通过 wereadBridge.execMPReaderMethod 通知原生
JS Bridge 双向通信
│ 原生 -> JS:
│ - evaluateJavaScript: 调用 JS 方法
│ - WKUserScript 注入脚本
│ JS -> 原生:
│ - window.webkit.messageHandlers.XXX.postMessage()
│ - wereadBridge.execMPReaderMethod('MPReader', data)
WRMPReadingViewModel (ViewModel 层)
│ - addHighlightWithStart:withEnd:withContent:callback:
│ - addReviewWithRange:content:reference:secretMode:withCallback:
│ - genJSInfosWithHighlights:refrencedHighlight:
│ - genJSInfosWithReviews:refrencedReview:
│ - readReviewsWithLoadCount:maxObj:
│ - setupTTSAudioList
WRMPPageView (WebView 包装视图)
```
---
## 关键源码路径 (从二进制中提取)
```
WeRead/Src/Modules/EpubParser/
├── WREpubTypesetter.m # EPUB 排版器
├── WREpubPositionConverter.m # 位置转换器
└── Utils/DTCoreTextFunctions.m # CoreText 工具函数
WeRead/Src/Modules/TypeSetter/
├── WRCoreTextLayouter.m # CoreText 排版器
├── WRCoreTextLayoutFrame.m # 排版帧 (管理页面)
└── DTCoreTextGlyphRun.m # 字形渲染
WeRead/Src/Modules/Reading/
├── Controller/
│ ├── WRReaderViewController.m # 阅读器主控制器
│ └── WRPageViewController.m # 翻页控制器
├── Model/
│ ├── WRChapterData.m # 章节数据模型
│ ├── WRChapterDownloadManger.m # 章节下载管理
│ ├── WRReaderViewModel.m # 阅读器 ViewModel
│ └── WRMPReadingManager.m # 公众号阅读管理
└── View/
└── WRPageView.m # 页面渲染视图
WeRead/Src/Modules/MediaPlatform/
├── Controller/
│ ├── WRMPListViewController.m
│ └── WRMPSubscribeViewController.m
└── Model/
├── WRMPCoverManager.m
├── WRMPCoverPainter.m
├── WRMPStore.m
└── WRMPViewModel.m
```
---
## EPUB CSS 样式系统
```
加载优先级 (从低到高):
1. default.css - 基础 HTML 标签样式 (Safari 默认)
2. replace.css - 读书默认替换样式
├── 标题样式 (h1-h6, 使用 Source Han Serif CN 字体)
├── 代码块 (pre, 使用 Menlo 字体)
├── 图片 (.bodyPic, wr-vertical-center-style: 2)
├── 引用 (.conQuot)
├── 翻译 (.wr-translation)
├── 章节工具 (.book-chapter-tool, .chapter-tool)
└── 分页控制 (.weread-page-relate)
3. dark.css - 暗黑主题样式
4. EPUB 内嵌 CSS - 书籍自带样式
5. 用户设置 - 字号、行高、主题覆盖
```
**自定义 CSS 属性** (读书私有):
- `wr-vertical-center-style: 1|2` - 图片垂直居中方式
- `weread-page-relate: true` - 控制分页时的内容关联
---
## 核心类职责表
| 类名 | 职责 | 渲染路径 |
|---|---|---|
| `WRReaderViewController` | 阅读器主控制器,管理阅读状态、进度保存、章节跳转 | 共用 |
| `WRPageViewController` | 翻页控制器,基于 UIPageViewController 封装 | 共用 |
| `WRPageView` | 页面渲染视图CoreText 直接绘制 | 路径A |
| `WRMPPageView` | 公众号页面视图WKWebView 包装 | 路径B |
| `WREpubTypesetter` | EPUB 排版器HTML->NSAttributedString | 路径A |
| `WRCoreTextLayouter` | CoreText 排版引擎,管理 CTFrame/CTLine | 路径A |
| `WRCoreTextLayoutFrame` | 排版帧,管理单页的绘制 | 路径A |
| `WRChapterData` | 章节数据模型,存储排版结果和标注 | 路径A |
| `WRChapterPageCount` | 分页计算,管理每页的 NSRange | 路径A |
| `WREpubPositionConverter` | EPUB 位置转换器 (文件位置<->字符位置) | 路径A |
| `WRChapterDownloadManger` | 章节下载管理器 | 共用 |
| `WREncryptedFileManager` | 加密文件管理 (DRM) | 共用 |
| `WRBookNetwork` | 书籍网络请求 (下载/解密/解压) | 共用 |
| `WRMPReadingManager` | 公众号阅读管理器 | 路径B |
| `WRMPReadingViewModel` | 公众号阅读 ViewModel (JS Bridge 交互) | 路径B |
| `WRReaderViewModel` | 阅读器 ViewModel | 共用 |
| `DTCoreTextLayouter` | DTCoreText 排版器 (第三方库修改版) | 路径A |
| `DTHTMLAttributedStringBuilder` | HTML->NSAttributedString 构建器 | 路径A |
| `WRReaderPencilNoteManager` | Apple Pencil 手写笔记管理 | 共用 |
| `WRReaderTranslationManager` | 翻译管理 (繁简转换/中英翻译) | 共用 |
| `WRReaderCht2sManager` | 繁体转简体管理 | 共用 |
---
## JavaScript 文件职责
| 文件 | 职责 |
|---|---|
| `weread-highlighter.js` | 核心高亮引擎,初始化 Rangy管理选择和高亮 |
| `rangy-core.js` | Rangy 核心库,跨浏览器 Range/Selection 封装 |
| `rangy-highlighter.js` | Rangy 高亮模块,管理高亮的创建/删除/序列化 |
| `rangy-classapplier.js` | Rangy ClassApplier 模块CSS 类应用器 |
| `rangy-textrange.js` | Rangy TextRange 模块,文本范围操作 |
| `Readability.js` | Arc90 Readability 库,提取文章正文 |
| `MediaPlatform.js` | 公众号平台 JS原生-JS 桥接 |
| `mpForArticle.js` | 文章相关 JS 逻辑 |
| `MPExtra.css` | 公众号额外样式 |
| `MediaPlatform.css` | 公众号基础样式 |
| `WeReadApi.js` | 读书 JS API (供 WebView 调用原生功能) |
| `rich_display.js` | 富文本显示逻辑 |
| `cssInjector.js` | CSS 注入器 |
| `highlight.min.js` | 代码语法高亮 (highlight.js) |
---
## DRM 与安全机制
```
┌─────────────────────────────────────────────┐
│ DRM 保护链 │
├─────────────────────────────────────────────┤
│ │
│ 1. 传输层: HTTPS + 加密 ZIP │
│ - 服务器下发加密的 .zip 文件 │
│ - 文件名格式: {bookId}_DECRYPT.zip │
│ │
│ 2. 解密层: 逐章解密 │
│ - WREncryptedFileManager │
│ - keyForBookId: (每本书独立密钥) │
│ - decryptContentsOfFile:forBookId: │
│ │
│ 3. 存储层: 本地二次加密 │
│ - encryptFileForBookId:atPath:toPath: │
│ - 解密后立即重新加密存储 │
│ - 防止直接拷贝文件读取 │
│ │
│ 4. 密钥管理: │
│ - WRPreloadBookManager 管理预加载密钥 │
│ - 密钥与设备绑定 │
│ - 通过 Keychain 安全存储 │
│ │
│ 5. 免费试读控制: │
│ - freeTrialChapterCutOffStringLocaion │
│ - 服务端控制试读范围 │
│ - 客户端截断显示 │
│ │
│ 6. 章节付费: │
│ - isChapterAvailableForBookId:chapterUid │
│ - getCouponBuyChapterWithBookId: │
│ - resetChapterPaidIfNeeded │
│ │
└─────────────────────────────────────────────┘
```
---
## 字体系统
```
内嵌字体:
├── SourceHanSerifCN-Medium.ttf # 思源宋体 (正文默认)
├── FZJuZhenXinFang.ttf # 方正聚珍新仿
├── Lora-Regular.ttf / Italic.ttf # Lora 衬线体
├── PlayfairDisplay-Regular.ttf # Playfair Display
├── WeReadLS-Regular/Medium/Bold # 读书 LS 系列
├── WeReadRN-Regular.ttf # RN 专用字体
├── WeRead-Icon.ttf # 图标字体
├── WeRead-Rating-Icon.ttf # 评分图标
├── OpenDyslexic-Regular.otf # 阅读障碍友好字体
├── WeChatNumber.ttf # 微信数字字体
├── SharpGroteskTRIAL*.ttf # Sharp Grotesk 系列
└── icon_font.ttf # 通用图标字体
动态字体:
├── CDN 下载: cdn.weread.qq.com/app/assets/font/
│ ├── FZLTHProGBK_B/DB/SB.zip # 方正兰亭黑系列
│ ├── FZQingKBYSJF-M.zip # 方正清刻本悦宋
│ ├── FZSKBXKK.zip # 方正书宋
│ ├── FZYBKSK.zip # 方正中楷
│ └── SourceHanSansCN-Heavy.zip # 思源黑体
└── WRFontsManager 管理:
- unZipFileAndRegisterFont:completionHandler:filePath:lateOverWrite:
- 动态下载 + 解压 + 注册
```
---
## 预加载与缓存策略
```
预加载策略:
├── WRForecastUtils.shouldPreloadChapterUidsForBook:type:bookRank:archiveRank:
│ - 根据书籍排名和用户行为预测需要预加载的章节
├── WRChapterDownloadManger._preloadChapterContentWithBook:type:bookRank:
│ - 后台预下载相邻章节
├── WRPreloadBookManager
│ - saveEncryptKey:forPath:bookId: (缓存密钥)
│ - saveFileNameDict:bookId: (缓存文件名映射)
│ - clearKV (清理缓存)
└── SDWebImage 缓存:
- epubImage 目录缓存书籍图片
- com.hackemist.SDWebImageCache.epubImage
缓存目录结构:
├── Documents/
│ └── {bookId}/
│ ├── plainBookDirectory/ (解密后的 EPUB 文件)
│ ├── epubImage/ (书籍图片缓存)
│ └── _DECRYPT.zip (下载的加密 ZIP)
└── Library/
└── {cachePath}/
├── epubImage/ (SDWebImage 缓存)
└── com.hackemist.SDWebImageCache.epubImage
```
---
## 朗读 (TTS) 集成
```
朗读流程:
├── WRReadAloudAudio.parseCGIInfos: (解析音频信息)
├── TTS 引擎: wxtts (微信语音合成)
│ ├── 离线资源: wxtts_offline_resource5.zip
│ ├── 在线资源: getWxttsSeginfo / getWxttsVoice
│ └── VITS/VALL-E 模型 (高保真语音)
├── 文本分段:
│ - weread-highlighter.js 中的 "tts" ClassApplier
│ - 标记当前朗读位置
└── 进度同步:
- lastListenedChapterOffset / lastListenedChapterUid
- MPReading/lastListenedReviewId (公众号朗读进度)
```
---
## Apple Pencil 手写笔记
```
WRReaderPencilNoteManager:
├── 数据存储:
│ - writeDrawingDataToLocal:reviewItemId:reviewId:isDraft:
│ - drawingFilePathWithReviewItemId:reviewId:isDraft:
│ - 本地草稿 + 云端同步
├── 数据上传:
│ - uploadPencilDrawing:colorStyle:onlyUploadImage:canRetry:
│ - uploadPencilNoteData:suffix:
│ - 使用腾讯云 COS 存储
│ - authCosForPencilDataWithSuffix: (COS 认证)
├── 数据下载:
│ - downloadDrawingDataFromCosWithUrl:desPath:callback:
│ - downloadDrawingWithReviewItemId:reviewId:drawingUrl:dataBlock:
└── 图片导出:
- imageFilePathWithReviewItemId:
- 手绘笔记可导出为图片
```
---
## 总结
| 特性 | 实现方式 |
|---|---|
| **EPUB 解析** | 自研 EPUB Parser解析 OPF/NCX/XHTML |
| **文字排版** | DTCoreText (CoreText 封装) + 自定义 WRCoreTextLayouter |
| **页面渲染** | CGContext 直接绘制,不用 UILabel/UITextView |
| **翻页效果** | UIPageViewController (UIPageCurl + Scroll) |
| **文本选择** | CoreText hit test + Rangy.js (WebView 场景) |
| **标注系统** | NSAttributedString 属性注入 + 服务器同步 |
| **图片处理** | NSTextAttachment + CDN 尺寸优化 + 白底透明化 |
| **DRM** | 逐章加密 + 逐书密钥 + 本地二次加密 |
| **公众号文章** | WKWebView + JS Bridge + Rangy 高亮 |
| **繁简转换** | CoreText 层面的 convertHansToHant |
| **字体** | 内嵌 Source Han Serif CN + 动态下载字体 |
| **预加载** | 后台预下载相邻章节 ZIP + 解密缓存 |
| **TTS 朗读** | wxtts 引擎 + VITS/VALL-E 高保真模型 |
| **手写笔记** | PencilKit + 腾讯云 COS 存储 |