# 读书 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 │ └──────────────────────┴──────────────────────────┘ ``` --- ## 路径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 加载 │ │ 注入脚本: │ ▼ 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 存储 |