335 lines
14 KiB
Markdown
335 lines
14 KiB
Markdown
# WXRead/decompiled 逆向代码说明
|
||
|
||
## 概述
|
||
|
||
此目录包含从读书 (WeRead) iOS 客户端二进制逆向还原的头文件和实现代码。共 44 个文件(22 对 .h/.m),分为三大模块:
|
||
|
||
1. **阅读器核心** — 阅读控制器、页面渲染、排版引擎、EPUB 解析
|
||
2. **标注系统** — 划线、高亮、书签、想法、Pencil 手写笔记
|
||
3. **DTCoreText 定制** — 基于开源 DTCoreText 库的读书私有定制
|
||
|
||
---
|
||
|
||
## 架构总览
|
||
|
||
```
|
||
WRReaderViewController (阅读器主控制器)
|
||
├── WRPageViewController (翻页控制器, 支持 curl/slide/fade/none)
|
||
│ └── WRPageView (页面渲染视图, CoreText 直接绘制)
|
||
│ └── WRCoreTextLayoutFrame (单页排版帧)
|
||
│ └── WRCoreTextLayoutLine (单行排版)
|
||
├── WREpubParser (EPUB 文件解析)
|
||
├── WREpubTypesetter (XHTML → NSAttributedString 排版)
|
||
│ └── DTHTMLAttributedStringBuilder (HTML DOM → 属性字符串)
|
||
│ └── DTHTMLElement (DOM 节点)
|
||
├── WRChapterData (章节数据模型)
|
||
├── WRChapterPageCount (分页计算器)
|
||
├── WRCoreTextLayouter (CoreText 排版器)
|
||
└── WRBookmark / WRPageHighlight / WRPageMark / WRPageUnderline (标注模型)
|
||
```
|
||
|
||
---
|
||
|
||
## 阅读器核心模块
|
||
|
||
### `WRReaderViewController`
|
||
|
||
阅读器的主入口控制器,431 个方法,管理阅读状态的完整生命周期。
|
||
|
||
**核心职责:**
|
||
- 管理 `WRReadingProgress`(阅读进度:章节索引、页码、字符偏移、滚动位置、阅读百分比)
|
||
- 章节导航:`gotoChapterIdx:position:positionOfFile:`、`jumpReadingToNextChapter`、`jumpReadingToPreChapter`
|
||
- 页面渲染:`renderPageView:progressData:source:` — 获取/计算章节数据、分页、创建/更新 WRPageView
|
||
- 排版重排:`recomposeCurrentPageViewWithSource:` — 字号/行距/主题变更后触发
|
||
- 进度持久化:`_saveReadingProgressAndIsAsync:`
|
||
- 支持涂鸦模式 (doodle mode) 和自动阅读 (auto-read)
|
||
- 通过 `WRReaderViewControllerDelegate` 回调通知外部进度变化和退出
|
||
|
||
### `WRPageViewController`
|
||
|
||
自定义的翻页控制器,支持四种翻页动画:
|
||
|
||
| 枚举值 | 样式 | 说明 |
|
||
|--------|------|------|
|
||
| `WRPageFlippingStyleCurl` | 纸张翻页 | 基于 UIPageViewController 的 curl 效果 |
|
||
| `WRPageFlippingStyleSlide` | 水平滑动 | UIScrollView 驱动的滑动翻页 |
|
||
| `WRPageFlippingStyleFade` | 淡入淡出 | 交叉渐变 |
|
||
| `WRPageFlippingStyleNone` | 无动画 | 瞬间切换 |
|
||
|
||
**关键特性:**
|
||
- 内部封装 `UIPageViewController`,提供 `WRPageViewControllerDelegate` / `WRPageViewControllerDataSource` 协议
|
||
- 包含 3 个故障修补方法,处理 Apple `UIPageViewController` 的已知崩溃:
|
||
- `detectNavigationDirectionCrashWithPageViewController:` — 检测导航方向崩溃
|
||
- `patchNavigationDirectionFault` — 修复导航方向状态不一致
|
||
- `patchNoViewControllerManagingPageViewFault` — 修复子 VC 丢失
|
||
- `patchUIPageCurlFault` — 修复快速翻页时的动画故障
|
||
|
||
### `WRPageView`
|
||
|
||
单页渲染视图,继承 `UIView`,通过 CoreText 的 `drawRect:` 直接绘制文本,不使用 `UILabel` 或 `UITextView`。
|
||
|
||
**核心绘制:**
|
||
- `drawInContext:withData:size:inRect:position:` — 将文本和行内图片直接绘制到 CGContext
|
||
- 通过 `CTLineGetStringIndexForPosition` 进行 CoreText 级别的坐标点击测试
|
||
|
||
**UI 覆盖层:**
|
||
- 章节标题头 (headerLabel)、背景图片 (backgroundImageView)
|
||
- 加载指示器 (activityIndicator)、加载进度条 (loadingProgressView)
|
||
- 状态提示 (statusLabel)、重试按钮 (retryButton)
|
||
- 分享按钮、书签按钮、字号调节按钮
|
||
- 好友想法按钮 (friendReviewsButton)
|
||
|
||
**文本选区:**
|
||
- `stringIndexForPoint:` / `lineRangeForStringIndex:` — CoreText 坐标与字符索引转换
|
||
- `isSelecting` / `selectedRange` — 选区状态
|
||
- `clearSelection` — 清除选区
|
||
|
||
---
|
||
|
||
## EPUB 解析与排版
|
||
|
||
### `WREpubParser`
|
||
|
||
EPUB 文件解析器,处理完整的 EPUB 结构:
|
||
|
||
**解析流程:** `container.xml` → `content.opf` → `spine` → 章节文件
|
||
|
||
**主要 API:**
|
||
- `initWithFilePath:book:` — 初始化
|
||
- `parse:` — 执行解析,返回 YES/NO
|
||
- `parseContainerXML:` — 解析 container.xml 获取 OPF 路径
|
||
- `parseOPFAtRelativePath:error:` — 解析 content.opf,填充章节列表和资源映射
|
||
- `parseNCX:` — 解析 toc.ncx 目录树
|
||
- `contentForChapterAtIndex:error:` — 读取单章 XHTML 内容
|
||
- `absolutePathForResource:` — 将相对资源路径解析为绝对路径
|
||
|
||
**错误码:**
|
||
|
||
| 错误码 | 含义 |
|
||
|--------|------|
|
||
| -1000 | 文件未找到 |
|
||
| -1001 | container.xml 解析失败 |
|
||
| -1002 | OPF 解析失败 |
|
||
| -1003 | NCX 解析失败 |
|
||
| -1004 | spine 为空 |
|
||
| -1005 | 章节加载失败 |
|
||
| -1006 | 解密失败 |
|
||
|
||
### `WREpubTypesetter`
|
||
|
||
EPUB 排版器,XHTML → `NSAttributedString` 的转换核心。全部为类方法,无需实例化。
|
||
|
||
**主入口方法:**
|
||
```objc
|
||
+ (NSAttributedString *)attributeStringWithFilePath:priority:
|
||
insertArticleToolAttachment:insertBookChapterToolAttachment:
|
||
insertRecommendView:book:chapter:pageFlippingStyle:
|
||
renderErrorReason:isStyleFileNotFound:options:
|
||
```
|
||
|
||
**处理流程:**
|
||
1. 加载并级联合并 CSS(用户设置 > EPUB 内嵌 > 默认样式)
|
||
2. 将 XHTML + 合并后的 CSS 喂入 `DTHTMLAttributedStringBuilder`
|
||
3. 遍历生成的元素树,修补图片、链接、自定义属性
|
||
4. 应用用户排版偏好(字体、行距、主题)
|
||
5. 可选截断用于免费试读预览
|
||
|
||
**自定义 CSS 属性:**
|
||
- `WREpubTypesetterVerticalCenterStyleAttribute` — 行内元素垂直居中(`wr-vertical-center-style`)
|
||
- `WREpubTypesetterPageRelateAttribute` — 跨页关联标记(`weread-page-relate`)
|
||
|
||
**翻译渲染错误上报:**
|
||
- `tryReportTranslationError:` — 双语(原文 + 翻译)渲染异常时上报分析
|
||
|
||
### `WREpubPositionConverter`
|
||
|
||
文件位置与字符位置的双向转换器。用于书签同步和阅读进度追踪。
|
||
|
||
**核心概念:** 将 `(fileIndex, row, column)` 三元组映射为全局字符偏移量。
|
||
|
||
**主要 API:**
|
||
- `initWithFilePaths:attributedStrings:offset:isContainIntroFlyleaf:` — 初始化
|
||
- `initIndices` — 构建内部索引表(调用前不能执行转换)
|
||
- `indicesInFile:forRowColumnPairs:stringIndices:` — 行/列 → 字符索引
|
||
- `stringRangeFromFileRange:` — 文件内范围 → 全局字符串范围
|
||
- `totalCharacterCount` — 全书总字符数
|
||
- `fileIndexForCharacterPosition:` — 全局位置 → 文件索引
|
||
- `localOffsetInFileAtIndex:forGlobalPosition:` — 全局位置 → 文件内偏移
|
||
|
||
---
|
||
|
||
## 分页与布局
|
||
|
||
### `WRChapterPageCount`
|
||
|
||
章节分页计算器。管理每页的 `NSRange`。
|
||
|
||
**关键属性:**
|
||
- `pageRanges` — 每页的字符范围数组 (NSValue-wrapped NSRange)
|
||
- `totalPages` — 总页数
|
||
|
||
**关键方法:**
|
||
- `currentCacheKeyWithBookId:` — 生成缓存键(编码书 ID + 当前排版设置,设置变更则失效)
|
||
- `rangeValueWithPageInfo:` — 从服务端分页数据提取范围
|
||
- `rangeForPageAtIndex:` / `pageIndexForCharacterIndex:` — 页码 ↔ 字符位置
|
||
- `recalculatePageRangesForAttributedString:drawingSize:margins:` — 重算分页
|
||
|
||
### `WRCoreTextLayouter`
|
||
|
||
读书封装的 CoreText 排版器,创建 `CTTypesetter` / `CTFramesetter` 并产出 `WRCoreTextLayoutFrame` 页面对象。
|
||
|
||
**初始化:**
|
||
- `initWithAttributedString:` / `initWithAttributedString:config:`
|
||
- `WRCoreTextLayoutConfig` 配置:frame 宽高、内边距、栏数、栏间距、孤行/寡行控制、连字符
|
||
|
||
**排版 API:**
|
||
- `createTypesetter` / `createFramesetter` / `invalidateTypesetter`
|
||
- `layoutFrameWithRange:frame:` — 单帧排版
|
||
- `layoutFramesForPageSize:` — 全文分页排版
|
||
- `numberOfPagesForPageSize:` / `rangeForPageAtIndex:pageSize:` — 分页查询
|
||
- `pageBackgroundImageAtRange:themeBgColor:` — 页面背景图
|
||
- `resizedImageForImagePath:` — 图片缩放适配
|
||
|
||
### `WRCoreTextLayoutFrame`
|
||
|
||
单页排版帧,封装 `CTFrame`,管理 `WRCoreTextLayoutLine` 行数组。
|
||
|
||
**核心功能:**
|
||
- 文本绘制:`drawInContext:image:size:inRect:position:`(含 CTFrameDraw + 图片附件 + 装饰元素)
|
||
- 分栏支持:`numberOfColumns` / `columnGap`
|
||
- 跨页避让:`avoidPageBreakInsideByRemovingLastLinesIfNeeded` — CSS `avoid-page-break-inside` 实现
|
||
- 点击测试:`characterIndexAtPoint:` / `lineIndexAtPoint:` / `rectForCharacterAtIndex:`
|
||
- 选区操作:`selectedText` / `selectedRanges` / `selectTextInRange:` / `clearSelection`
|
||
- 搜索高亮:`highlightSearchResults:` / `clearSearchHighlights`
|
||
- 装饰元素:删除线范围、下划线范围、高亮范围、链接范围
|
||
- 响应式更新:通过 `RACSubject` (ReactiveObjC) 发出布局变更通知
|
||
|
||
---
|
||
|
||
## 标注系统
|
||
|
||
### `WRBookmark`
|
||
|
||
书签/标注的数据模型,支持 5 种类型:
|
||
|
||
| 类型 | 说明 |
|
||
|------|------|
|
||
| `WRBookmarkTypeHighlight` | 彩色高亮 |
|
||
| `WRBookmarkTypeUnderline` | 下划线 |
|
||
| `WRBookmarkTypeMark` | 页面书签(折角) |
|
||
| `WRBookmarkTypeNote` | 文字笔记 |
|
||
| `WRBookmarkTypePencil` | Apple Pencil 手绘 |
|
||
|
||
**关键属性:** `bookmarkId`、`bookId`、`chapterUid`、`chapterOffset`、`markText`、`startPos`/`endPos`、`colorStyle`、`anchorId`、`rangeKey`、`syncKey`
|
||
|
||
**持久化:** `toDictionary` / `fromDictionary`、`toJSONData` / `fromJSONData`
|
||
|
||
**工厂方法:**
|
||
- `bookmarkWithBookId:chapterUid:offset:text:type:`
|
||
- `highlightWithBookId:chapterUid:startPos:endPos:text:colorStyle:`
|
||
|
||
### `WRPageHighlight`
|
||
|
||
文本高亮标注模型。5 种预设颜色:黄、蓝、红、绿、紫。
|
||
|
||
**方法:**
|
||
- `initWithBookId:chapterUid:startPos:endPos:text:color:` — 创建高亮
|
||
- `toBookmark` — 转为 WRBookmark 进行持久化/同步
|
||
- `uiColor` — 返回渲染用 UIColor
|
||
|
||
### `WRPageUnderline`
|
||
|
||
下划线标注模型。支持 4 种下划线样式:实线、虚线、波浪线、点线。
|
||
|
||
**方法:**
|
||
- `toBookmark` / `fromBookmark:` — 与 WRBookmark 双向转换
|
||
- `underlinePathForRect:` — 返回渲染用 UIBezierPath
|
||
- `containsPosition:` — 范围查询
|
||
- `mergeWithUnderline:` — 合并相邻下划线
|
||
- `setNote:` — 附加笔记
|
||
|
||
### `WRPageMark`
|
||
|
||
页面书签(折角)模型。25 个方法,功能最丰富的标注类型。
|
||
|
||
**关键特性:**
|
||
- 支持 3 种展示模式:图标、列表、行内
|
||
- 关联标注:`linkedHighlights` / `linkedUnderlines` — 书签位置上附带的高亮和下划线
|
||
- 批量操作:`marksSortedByPosition:` / `marksSortedByDate:` / `marksInChapter:fromMarks:` / `marksInRange:fromMarks:`
|
||
- 序列化:`toDictionary` / `fromDictionary`、`toJSONData` / `fromJSONData`
|
||
|
||
### `WRReaderPencilNoteManager`
|
||
|
||
Apple Pencil 手写笔记管理器。11 个类方法,全部为类方法调用。
|
||
|
||
**支持 9 种画笔颜色/样式:** 黑、灰、红、蓝、黄、绿、铅笔、钢笔、荧光笔
|
||
|
||
**本地存储:**
|
||
- `drawingFileDirectory` / `drawingFilePathWithReviewItemId:` — 文件路径管理
|
||
- `writeDrawingDataToLocal:` / `writeDrawingToLocal:` — 本地写入(支持 draft/published 状态)
|
||
- `checkDrawingExistsWithReviewItemId:` — 存在性检查
|
||
- `deleteDrawingWithReviewItemId:` / `deleteAllReviewDrawings` — 删除
|
||
|
||
**云端同步:**
|
||
- `uploadPencilDrawing:colorStyle:onlyUploadImage:canRetry:` — 上传绘制图片和数据到腾讯云 COS
|
||
- `uploadPencilNoteData:suffix:` — 上传 PKDrawing 序列化数据
|
||
- `downloadDrawingDataFromCosWithUrl:desPath:callback:` — 从 COS 下载
|
||
|
||
---
|
||
|
||
## DTCoreText 定制模块
|
||
|
||
读书基于开源库 [DTCoreText](https://github.com/Cocoanetics/DTCoreText) 做了大量私有定制。
|
||
|
||
### `DTHTMLAttributedStringBuilder`
|
||
|
||
HTML DOM → `NSAttributedString` 的 SAX 风格解析构建器。`WREpubTypesetter` 的底层依赖。
|
||
|
||
**WeRead 自定义属性键:**
|
||
- `DTPageBackgroundColorAttribute` / `DTPageBackgroundImageAttribute` — 页面背景
|
||
- `DTPageBreakAfterAttribute` / `DTPageBreakBeforeAttribute` — 分页控制
|
||
- `DTPageBreakInsideAvoidAttribute` — 跨页避让
|
||
- `DTPageRelateAttribute` — 跨页关联
|
||
- `DTHTMLVerticalCenterAttribute` — 垂直居中
|
||
- `DTHTMLTranslateTagAttribute` — 翻译标签
|
||
|
||
### `DTHTMLElement`
|
||
|
||
HTML 元素 DOM 节点。构建解析时的 DOM 树,每个节点可通过 `attributedString` 方法生成对应的属性字符串。
|
||
|
||
**关键属性:** `tagName`、`parent`/`children`、`styleDictionary`、`isBlockElement`、`shouldAvoidPageBreakInside`
|
||
|
||
### `DTCoreTextLayouter` / `DTCoreTextLayoutFrame` / `DTCoreTextLayoutLine` / `DTCoreTextGlyphRun`
|
||
|
||
DTCoreText 的四层排版层次结构(与 `WR` 前缀版本功能对应,是底层实现):
|
||
|
||
```
|
||
DTCoreTextLayouter (排版器, CTTypesetter 包装)
|
||
└── DTCoreTextLayoutFrame (排版帧, CTFrame 包装)
|
||
└── DTCoreTextLayoutLine (排版行, CTLine 包装)
|
||
└── DTCoreTextGlyphRun (字形运行, CTRun 包装)
|
||
```
|
||
|
||
- **Layouter** — 创建 CTTypesetter,产出 LayoutFrame,支持帧缓存
|
||
- **LayoutFrame** — 管理排版行数组,支持多栏、分页避让、点击测试、选区、搜索
|
||
- **LayoutLine** — 封装 CTLine,管理 GlyphRun 数组,提供排版指标(ascent/descent/leading/width)和点击测试
|
||
- **GlyphRun** — 封装 CTRun,管理字形(glyph)级别操作:位置、路径绘制、附件(图片)、字体属性、绘制方向
|
||
|
||
---
|
||
|
||
## 数据流
|
||
|
||
```
|
||
EPUB 文件 (.epub)
|
||
↓ WREpubParser.parse
|
||
解析 container.xml → content.opf → toc.ncx
|
||
↓ WREpubTypesetter.attributeStringWithFilePath
|
||
XHTML + CSS → DTHTMLAttributedStringBuilder → NSAttributedString
|
||
↓ WRCoreTextLayouter / WRChapterPageCount
|
||
分页排版 → [NSRange per page]
|
||
↓ WRPageView.drawInContext
|
||
CoreText 绘制到 CGContext
|
||
↓ WRPageViewController
|
||
翻页展示
|
||
```
|