23 KiB
23 KiB
微信读书 EPUB 阅读器实现架构分析
分析日期: 2026-05-18 基于逆向工程: 微信读书 v10.0.3 (Build 79)
核心结论:双渲染引擎架构
微信读书使用了 两套渲染引擎,根据内容类型选择不同的渲染路径:
┌─────────────────────────────────────────────────┐
│ 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 │
└──────────────────────┴──────────────────────────┘
路径A:EPUB 正文原生渲染 (核心路径)
这是 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:)
路径B:WebView 渲染 (公众号/文集文章)
用于渲染微信公众号文章、文集、书评等富媒体内容。
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 存储 |