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

23 KiB
Raw Permalink Blame History

读书 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 存储