docs: 大书优化方案文档与架构分析更新
新增大书优化实施方案(内存与主线程、快速进入阅读器)和 WXRead 内存策略分析文档, 更新架构对比分析文档,完善章节级缓存运行时、串行加载、轻量磁盘摘要等设计细节。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1c6108061c
commit
488350d956
459
Doc/WXRead/内存优化方案_WXRead策略.md
Normal file
459
Doc/WXRead/内存优化方案_WXRead策略.md
Normal file
@ -0,0 +1,459 @@
|
||||
# WXRead 阅读器内存优化方案
|
||||
|
||||
> 基于读书 v10.0.3 逆向代码分析,提炼 WXRead 处理大书内存的核心策略。
|
||||
> 创建日期:2026-06-02
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心设计原则
|
||||
|
||||
WXRead 的内存管理建立在一个关键架构决策之上:**永远不持有全书数据模型**。
|
||||
|
||||
与"先构建整本 TextBook 再进入阅读器"不同,WXRead 的数据流是:
|
||||
|
||||
```
|
||||
单章 XHTML
|
||||
→ WREpubTypesetter.attributeStringWithFilePath: (单章排版)
|
||||
→ WRChapterData (单章数据模型)
|
||||
→ WRChapterPageCount (单章分页)
|
||||
→ WRPageView.drawRect: (单页渲染)
|
||||
```
|
||||
|
||||
每一步都是章节级或页面级操作,从不需要同时持有全书的 `NSAttributedString`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 章节级缓存模型
|
||||
|
||||
### 2.1 缓存结构
|
||||
|
||||
```objc
|
||||
// WRReaderViewController ()
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, WRChapterData *> *chapterDataCache;
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSString *, WRChapterPageCount *> *pageCountCache;
|
||||
```
|
||||
|
||||
关键设计决策:
|
||||
|
||||
| 设计点 | 选择 | 原因 |
|
||||
|--------|------|------|
|
||||
| 缓存类型 | `NSMutableDictionary` 而非 `NSCache` | 需要精确控制淘汰时机,NSCache 的自动淘汰不可预测 |
|
||||
| 缓存粒度 | 按章节索引(`NSNumber *`) | 每章独立加载、独立释放 |
|
||||
| 分页缓存 | 按缓存键(含排版设置) | 设置变更时整批失效 |
|
||||
|
||||
### 2.2 缓存内容
|
||||
|
||||
每个 `WRChapterData` 持有:
|
||||
|
||||
```objc
|
||||
@interface WRChapterData : NSObject
|
||||
@property (nonatomic, strong) NSMutableAttributedString *typesetAttributedString; // 排版后富文本
|
||||
@property (nonatomic, strong) WRCoreTextLayouter *layouter; // 排版器
|
||||
@property (nonatomic, strong) NSArray<NSValue *> *pageRanges; // 页范围数组
|
||||
@property (nonatomic, strong) NSArray<NSDictionary *> *highlights; // 高亮
|
||||
@property (nonatomic, strong) NSArray<NSDictionary *> *underlines; // 下划线
|
||||
@property (nonatomic, strong) NSSet<NSNumber *> *bookmarkedPages; // 书签
|
||||
@property (nonatomic, strong) NSAttributedString *sourceAttributedString; // 源文本
|
||||
@end
|
||||
```
|
||||
|
||||
**注意**:`WRChapterData` 不持有页面视图、不持有其他章节的引用、不持有全书索引表。章节之间完全解耦。
|
||||
|
||||
---
|
||||
|
||||
## 3. 内存警告处理
|
||||
|
||||
### 3.1 监听注册
|
||||
|
||||
```objc
|
||||
// WRReaderViewController.initWithBook:progress:...
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserver:self
|
||||
selector:@selector(_handleMemoryWarning:)
|
||||
name:UIApplicationDidReceiveMemoryWarningNotification
|
||||
object:nil];
|
||||
```
|
||||
|
||||
在 `initWithBook:` 中注册,确保从阅读器创建之初就监听。
|
||||
|
||||
### 3.2 处理逻辑
|
||||
|
||||
```objc
|
||||
- (void)_handleMemoryWarning:(NSNotification *)note {
|
||||
NSLog(@"[WRReader] Memory warning received, purging chapter cache.");
|
||||
|
||||
// 1. 保留当前章节
|
||||
NSUInteger currentIdx = self.readingProgress.chapterIndex;
|
||||
WRChapterData *currentData = self.chapterDataCache[@(currentIdx)];
|
||||
|
||||
// 2. 清空全部缓存
|
||||
[self.chapterDataCache removeAllObjects];
|
||||
|
||||
// 3. 恢复当前章节
|
||||
if (currentData) {
|
||||
self.chapterDataCache[@(currentIdx)] = currentData;
|
||||
}
|
||||
|
||||
// 4. 清空分页缓存(可在需要时重新计算)
|
||||
[self.pageCountCache removeAllObjects];
|
||||
}
|
||||
```
|
||||
|
||||
策略总结:
|
||||
|
||||
| 步骤 | 操作 | 目的 |
|
||||
|------|------|------|
|
||||
| 1 | 保存当前章节引用 | 当前页不能中断 |
|
||||
| 2 | `removeAllObjects` | 一次性释放所有非当前章 |
|
||||
| 3 | 恢复当前章节 | 保证阅读不中断 |
|
||||
| 4 | 清空分页缓存 | `pageCountCache` 可重建,释放额外内存 |
|
||||
|
||||
### 3.3 为什么不用 NSCache
|
||||
|
||||
WXRead 选择 `NSMutableDictionary` + 手动淘汰而非 `NSCache`,原因:
|
||||
|
||||
1. **确定性**:内存警告时必须立即释放,NSCache 的淘汰时机不可控
|
||||
2. **当前章保护**:NSCache 无法 pin 住当前章不被淘汰
|
||||
3. **可预测性**:开发和调试时行为一致,不会因系统内存压力变化而变化
|
||||
|
||||
---
|
||||
|
||||
## 4. 章节加载与预取
|
||||
|
||||
### 4.1 串行后台队列
|
||||
|
||||
```objc
|
||||
// 初始化
|
||||
_chapterLoadQueue = dispatch_queue_create("com.weread.chapterload", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
// 加载章节
|
||||
- (void)_loadChapterAtIndex:(NSUInteger)index
|
||||
completion:(void (^)(WRChapterData *, NSError *))completion {
|
||||
// 先查缓存
|
||||
WRChapterData *cached = self.chapterDataCache[@(index)];
|
||||
if (cached) {
|
||||
if (completion) completion(cached, nil);
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复加载
|
||||
if (self.isLoadingChapter) return;
|
||||
self.isLoadingChapter = YES;
|
||||
|
||||
dispatch_async(self.chapterLoadQueue, ^{
|
||||
// 后台:获取内容 → 排版 → 生成 WRChapterData
|
||||
NSError *error = nil;
|
||||
WRChapterData *chapterData = [self _fetchChapterDataForIndex:index error:&error];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.isLoadingChapter = NO;
|
||||
if (chapterData) {
|
||||
self.chapterDataCache[@(index)] = chapterData;
|
||||
if (completion) completion(chapterData, nil);
|
||||
} else {
|
||||
// 重试逻辑
|
||||
if (self.loadRetryCount < self.maxRetryCount) {
|
||||
self.loadRetryCount++;
|
||||
[self _loadChapterAtIndex:index completion:completion];
|
||||
} else {
|
||||
self.loadRetryCount = 0;
|
||||
if (completion) completion(nil, error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
设计要点:
|
||||
|
||||
| 要点 | 实现 | 目的 |
|
||||
|------|------|------|
|
||||
| 串行队列 | `com.weread.chapterload` | 避免并发加载导致内存峰值叠加 |
|
||||
| 防重复 | `isLoadingChapter` 标志 | 同时只加载一章,控制内存瞬时占用 |
|
||||
| 先查缓存 | `chapterDataCache[@(index)]` | 命中则直接返回,不触发后台任务 |
|
||||
| 重试机制 | `maxRetryCount = 3` | 网络异常时自动重试 |
|
||||
|
||||
### 4.2 相邻章节预取
|
||||
|
||||
```objc
|
||||
- (void)_prefetchAdjacentChaptersForIndex:(NSUInteger)index {
|
||||
// 预取下一章
|
||||
if (index + 1 < self.totalChapters) {
|
||||
NSUInteger nextIdx = index + 1;
|
||||
if (!self.chapterDataCache[@(nextIdx)]) {
|
||||
dispatch_async(self.chapterLoadQueue, ^{
|
||||
[self _fetchChapterDataForIndex:nextIdx error:NULL];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 预取上一章
|
||||
if (index > 0) {
|
||||
NSUInteger prevIdx = index - 1;
|
||||
if (!self.chapterDataCache[@(prevIdx)]) {
|
||||
dispatch_async(self.chapterLoadQueue, ^{
|
||||
[self _fetchChapterDataForIndex:prevIdx error:NULL];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**触发时机**:
|
||||
1. `renderPageView:` 渲染完成后(第 282 行)
|
||||
2. `didFlipPage` 翻页完成后(第 409 行)
|
||||
|
||||
**预取窗口**:仅当前章 ± 1 章,不做全书预加载。这保证内存占用与用户实际阅读位置绑定。
|
||||
|
||||
---
|
||||
|
||||
## 5. 图片缓存
|
||||
|
||||
### 5.1 NSCache 限制
|
||||
|
||||
```objc
|
||||
// WRCoreTextLayouter.commonInit
|
||||
_imageCache = [[NSCache alloc] init];
|
||||
_imageCache.countLimit = 50; // 最多缓存 50 张缩放后的图片
|
||||
```
|
||||
|
||||
图片是内存大户,WXRead 对图片缓存的处理策略:
|
||||
|
||||
| 策略 | 实现 | 原因 |
|
||||
|------|------|------|
|
||||
| 使用 `NSCache` | 自动在内存压力时淘汰 | 图片可以重新生成,丢失代价低 |
|
||||
| `countLimit = 50` | 限制数量 | 防止图片无限累积 |
|
||||
| 按章节归属 | 每个 `WRCoreTextLayouter` 独立持有 | 章节释放时图片一起释放 |
|
||||
|
||||
### 5.2 CoreText 对象释放
|
||||
|
||||
```objc
|
||||
// WRCoreTextLayouter.dealloc
|
||||
- (void)dealloc {
|
||||
if (_typesetter) {
|
||||
CFRelease(_typesetter);
|
||||
_typesetter = NULL;
|
||||
}
|
||||
if (_framesetter) {
|
||||
CFRelease(_framesetter);
|
||||
_framesetter = NULL;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`CTTypesetter` 和 `CTFramesetter` 是 C 对象,不会被 ARC 自动释放。WXRead 在 `dealloc` 中显式释放,防止内存泄漏。
|
||||
|
||||
---
|
||||
|
||||
## 6. 预加载缓存管理
|
||||
|
||||
### 6.1 预加载场景
|
||||
|
||||
```objc
|
||||
typedef NS_ENUM(NSInteger, WRPreloadScene) {
|
||||
WRPreloadSceneNone = 0,
|
||||
WRPreloadSceneShelf = 1, // 书架页预加载
|
||||
WRPreloadSceneReading = 2, // 阅读时预加载后续章节
|
||||
WRPreloadSceneWiFi = 3, // WiFi 下激进预加载
|
||||
WRPreloadSceneManual = 4, // 用户手动触发
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 缓存清理接口
|
||||
|
||||
```objc
|
||||
// 清理指定书籍的预加载数据
|
||||
+ (void)removeKVWithBookId:(NSString *)bookId;
|
||||
|
||||
// 清理全部预加载缓存
|
||||
+ (void)clearKV;
|
||||
|
||||
// 计算并可选清理预加载缓存
|
||||
- (void)calcAndClearPreloadBookWithCompletion:(void (^)(NSUInteger totalSize))completion
|
||||
onlyCalc:(BOOL)onlyCalc;
|
||||
```
|
||||
|
||||
预加载数据(已下载但未阅读的章节)存储在磁盘,不占用运行时内存。清理接口用于管理磁盘空间。
|
||||
|
||||
---
|
||||
|
||||
## 7. 排版重排的内存影响
|
||||
|
||||
### 7.1 设置变更时的缓存处理
|
||||
|
||||
```objc
|
||||
- (void)recomposeCurrentPageViewWithSource:(NSString *)source {
|
||||
self.isRecomposing = YES;
|
||||
|
||||
// 清空分页缓存(排版参数变了,旧分页无效)
|
||||
[self.pageCountCache removeAllObjects];
|
||||
|
||||
// 重新排版当前章节
|
||||
WRChapterData *chapterData = self.currentChapterData;
|
||||
if (chapterData) {
|
||||
[self _reTypesetChapterData:chapterData];
|
||||
}
|
||||
|
||||
// 重新渲染
|
||||
[self renderPageView:self.activePageViews.firstObject
|
||||
progressData:self.readingProgress
|
||||
source:source];
|
||||
|
||||
self.isRecomposing = NO;
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 全局设置变更
|
||||
|
||||
```objc
|
||||
- (void)reloadPageViewsWithProgressData:(WRReadingProgress *)progressData
|
||||
source:(NSString *)source {
|
||||
// 清空所有缓存(字号/行距变化影响所有章节的排版)
|
||||
[self.chapterDataCache removeAllObjects];
|
||||
[self.pageCountCache removeAllObjects];
|
||||
|
||||
// 重新加载当前章
|
||||
[self _loadChapterAtIndex:progressData.chapterIndex
|
||||
completion:^(WRChapterData *chapterData, NSError *error) {
|
||||
if (chapterData) {
|
||||
[self _displayChapter:chapterData
|
||||
atPageIndex:progressData.pageIndex
|
||||
animated:NO];
|
||||
}
|
||||
}];
|
||||
}
|
||||
```
|
||||
|
||||
全局设置变更时清空全部缓存,因为排版参数影响所有章节。这是一次性内存释放,后续按需重新加载。
|
||||
|
||||
---
|
||||
|
||||
## 8. 位置持久化
|
||||
|
||||
### 8.1 多字段模型
|
||||
|
||||
```objc
|
||||
@interface WRReadingProgress : NSObject
|
||||
@property (nonatomic, copy) NSString *bookId;
|
||||
@property (nonatomic, assign) NSUInteger chapterIndex; // 章节索引
|
||||
@property (nonatomic, assign) NSUInteger pageIndex; // 章内页码
|
||||
@property (nonatomic, assign) NSUInteger charIndex; // 章内字符偏移
|
||||
@property (nonatomic, assign) CGFloat scrollOffset; // 滚动偏移
|
||||
@property (nonatomic, copy) NSString *chapterId;
|
||||
@property (nonatomic, assign) double readPercentage; // 阅读百分比
|
||||
@end
|
||||
```
|
||||
|
||||
### 8.2 保存时机
|
||||
|
||||
```objc
|
||||
// 1. 每次翻页
|
||||
- (void)didFlipPage {
|
||||
// ...
|
||||
[self _saveReadingProgressAndIsAsync:YES];
|
||||
}
|
||||
|
||||
// 2. 30 秒定时器
|
||||
_progressSaveTimer = [NSTimer scheduledTimerWithTimeInterval:30.0
|
||||
target:self
|
||||
selector:@selector(_periodicProgressSave)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
// 3. 退出时同步保存
|
||||
- (void)_periodicProgressSave {
|
||||
[self _saveReadingProgressAndIsAsync:YES];
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 保存格式
|
||||
|
||||
```objc
|
||||
- (void)_saveReadingProgressAndIsAsync:(BOOL)isAsync {
|
||||
NSDictionary *dict = @{
|
||||
@"bookId": progress.bookId ?: @"",
|
||||
@"chapterIndex": @(progress.chapterIndex),
|
||||
@"pageIndex": @(progress.pageIndex),
|
||||
@"charIndex": @(progress.charIndex),
|
||||
@"scrollOffset": @(progress.scrollOffset),
|
||||
@"chapterId": progress.chapterId ?: @"",
|
||||
@"readPercentage": @(progress.readPercentage),
|
||||
@"timestamp": @([[NSDate date] timeIntervalSince1970]),
|
||||
};
|
||||
|
||||
if (isAsync) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:key];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
});
|
||||
} else {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:key];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**位置恢复精度**:`charIndex` 是主锚点。页面变化(字号、屏幕尺寸)不影响恢复,因为字符偏移量是稳定的。
|
||||
|
||||
---
|
||||
|
||||
## 9. 内存占用模型
|
||||
|
||||
### 9.1 单章内存估算
|
||||
|
||||
假设一章 EPUB 约 5000 字符:
|
||||
|
||||
| 组件 | 估算大小 | 说明 |
|
||||
|------|----------|------|
|
||||
| `typesetAttributedString` | ~200-500 KB | 含字体、段落样式、图片附件 |
|
||||
| `WRCoreTextLayouter` | ~50-100 KB | CTTypesetter + CTFramesetter |
|
||||
| `pageRanges` | ~1-5 KB | NSRange 数组 |
|
||||
| 高亮/下划线/书签 | ~1-10 KB | 取决于标注数量 |
|
||||
| 图片缓存 | ~0-2 MB | 取决于章内图片数量 |
|
||||
| **单章合计** | **~0.3-3 MB** | 图片是主要变量 |
|
||||
|
||||
### 9.2 全书内存估算
|
||||
|
||||
WXRead 的内存占用 = 当前章 + 预取章(±1)+ 系统开销:
|
||||
|
||||
| 场景 | 缓存章节数 | 估算内存 |
|
||||
|------|------------|----------|
|
||||
| 正常阅读 | 1(当前)+ 2(预取)= 3 章 | ~1-9 MB |
|
||||
| 内存警告后 | 1 章(仅当前) | ~0.3-3 MB |
|
||||
| 快速连续翻页 | 最多 3 章(串行加载限制) | ~1-9 MB |
|
||||
|
||||
**对比**:如果持有全书数据(500 章 × 1 MB/章 = 500 MB),WXRead 的策略将其控制在个位数 MB。
|
||||
|
||||
---
|
||||
|
||||
## 10. 策略总结
|
||||
|
||||
| 策略 | 实现 | 效果 |
|
||||
|------|------|------|
|
||||
| **章节级缓存** | `NSMutableDictionary<NSNumber *, WRChapterData *>` | 内存与阅读位置绑定,不随全书增长 |
|
||||
| **手动淘汰** | 内存警告时清空非当前章 | 确定性释放,当前章不中断 |
|
||||
| **串行加载** | `com.weread.chapterload` 队列 | 避免并发加载内存峰值叠加 |
|
||||
| **±1 预取** | 翻页后异步预取相邻章 | 平衡流畅性与内存占用 |
|
||||
| **图片 NSCache** | `countLimit = 50` | 图片自动淘汰,章节释放时一起释放 |
|
||||
| **C 对象显式释放** | `dealloc` 中 `CFRelease` | 防止 CoreText 内存泄漏 |
|
||||
| **不分页全书** | 按章节独立分页 | 避免持有全书页范围数组 |
|
||||
| **不持有全书索引** | 无全局 `RDEPUBTextIndexTable` 等价物 | 按需查询,非常驻 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:WXRead 关键文件索引
|
||||
|
||||
| 文件 | 职责 | 内存相关 |
|
||||
|------|------|----------|
|
||||
| `WRReaderViewController.m` | 阅读器主控制器 | `chapterDataCache`、内存警告处理、预取 |
|
||||
| `WRChapterData.h/m` | 章节数据模型 | 持有 `typesetAttributedString`、`layouter` |
|
||||
| `WRCoreTextLayouter.h/m` | CoreText 排版器 | 图片缓存 `NSCache(countLimit=50)`、C 对象释放 |
|
||||
| `WRChapterPageCount.h/m` | 章节分页计算 | `pageRanges` 数组 |
|
||||
| `WRPreloadBookManager.h/m` | 预加载管理 | 磁盘缓存管理,不占运行时内存 |
|
||||
| `WREpubTypesetter.m` | EPUB 排版器 | 单章排版,不持有全书状态 |
|
||||
|
||||
---
|
||||
|
||||
*基于读书 v10.0.3 逆向分析*
|
||||
*文档创建:2026-06-02*
|
||||
2827
Doc/大书优化方案_内存与主线程.md
Normal file
2827
Doc/大书优化方案_内存与主线程.md
Normal file
File diff suppressed because it is too large
Load Diff
438
Doc/大书快速进入阅读器方案_凡人修仙传.md
Normal file
438
Doc/大书快速进入阅读器方案_凡人修仙传.md
Normal file
@ -0,0 +1,438 @@
|
||||
# 《凡人修仙传》快速进入阅读器方案
|
||||
|
||||
> 适用场景:`textReflowable` 路径打开超大正文 EPUB,典型样本为《凡人修仙传》精校版全本。
|
||||
> 目标:把“进入阅读器前必须等全书分页完成”改成“先快速可读,再后台补齐全书能力”。
|
||||
> 结论先行:当前首屏慢的主因不是单章分页太慢,而是 **打开流程要求先完成全书 `RDEPUBTextBookBuilder.build()`**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前慢在哪里
|
||||
|
||||
结合当前代码,打开 reflowable EPUB 的主链路是:
|
||||
|
||||
```text
|
||||
RDEPUBReaderController.viewDidLoad
|
||||
-> RDEPUBReaderLoadCoordinator.loadPublication()
|
||||
-> applyParsedPublication(...)
|
||||
-> RDEPUBReaderPaginationCoordinator.paginatePublication()
|
||||
-> if publication.readingProfile == .textReflowable
|
||||
-> RDEPUBTextBookBuilder.build(...)
|
||||
-> 遍历全部 spine item
|
||||
-> 每章 render + paginate
|
||||
-> 汇总成完整 RDEPUBTextBook
|
||||
-> applyTextBook(...)
|
||||
-> readerView.reloadData()
|
||||
-> restoreReadingLocation(...)
|
||||
```
|
||||
|
||||
关键事实:
|
||||
|
||||
- [RDEPUBReaderPaginationCoordinator.swift](/Users/shenlei/Work/ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift) 在 `textReflowable` 路径里会先 `showLoading()`,然后后台执行 `builder.build(...)`,构建完成前不会进入正文。
|
||||
- [RDEPUBTextBookBuilder.swift](/Users/shenlei/Work/ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift) 的 `build()` 会遍历全部 `publication.spine`,逐章完成:
|
||||
- HTML 读取
|
||||
- `DTCoreText` 渲染
|
||||
- CoreText 分页
|
||||
- 页面模型组装
|
||||
- 全书 `RDEPUBTextBook` 汇总
|
||||
- 也就是说,对《凡人修仙传》这种章节数多、正文长的大书,当前实际是“**全书构建完成后才能看到第一页**”。
|
||||
|
||||
这条链路的体验问题是:
|
||||
|
||||
- 首屏等待时间和“全书总字数/总章节数”线性相关,而不是和“当前阅读位置附近内容规模”相关。
|
||||
- 即使用户只想看第一页,也要先支付整本书的 render + paginate 成本。
|
||||
- 恢复到历史位置时也是同样问题,因为当前恢复逻辑依赖完整 `RDEPUBTextBook` 的页码与位置映射。
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案目标
|
||||
|
||||
### 用户目标
|
||||
|
||||
- 点击书籍后,阅读器应尽快进入正文页,而不是长时间停留在 loading。
|
||||
- 即使全书尚未构建完成,也至少能:
|
||||
- 看到当前章节
|
||||
- 翻当前章节内的页
|
||||
- 恢复到“接近上次阅读位置”的章节
|
||||
|
||||
### 技术目标
|
||||
|
||||
- 首屏进入从“全书 ready”改成“当前章节 ready”。
|
||||
- 全书构建改为后台增量完成。
|
||||
- 不破坏现有 `RDEPUBReaderController` / `RDReaderView` 的主公开 API。
|
||||
- 保留当前 `RDEPUBTextBookCache` 的价值,但把缓存粒度从“整本书一次命中”扩展到“单章可命中”。
|
||||
|
||||
### 首期验收指标
|
||||
|
||||
- 大书首次打开时,进入正文的等待时间显著短于当前实现。
|
||||
- 首屏进入只依赖“当前章节”或“当前位置附近章节”构建完成。
|
||||
- 后台继续构建剩余章节时,不阻塞阅读。
|
||||
|
||||
---
|
||||
|
||||
## 3. 推荐方案:两阶段进入 + 章节级增量构建
|
||||
|
||||
## 阶段 A:快速进入
|
||||
|
||||
打开书后只做这些事情:
|
||||
|
||||
1. 解析 EPUB 基础元数据、spine、TOC
|
||||
2. 确定恢复位置对应的 `spineIndex`
|
||||
3. 只构建当前章节,必要时附带相邻 `±1` 章
|
||||
4. 先生成一个“局部 TextBook / 局部 Snapshot”
|
||||
5. 立即进入阅读器并恢复到该章节内位置
|
||||
|
||||
这一阶段的原则是:
|
||||
|
||||
- 先解决“能进入”
|
||||
- 不要求立刻具备全书搜索、全书目录页码、全书绝对页码精度
|
||||
|
||||
## 阶段 B:后台补全
|
||||
|
||||
进入正文后,再后台串行完成:
|
||||
|
||||
1. 当前章节相邻章节预构建
|
||||
2. 剩余章节逐步构建
|
||||
3. 持续补齐全书:
|
||||
- `RDEPUBTextBook.chapters`
|
||||
- `RDEPUBTextBook.pages`
|
||||
- `RDEPUBTextIndexTable`
|
||||
- 全书页码/位置映射
|
||||
4. 构建完成后静默替换到完整模型
|
||||
|
||||
这样用户的体感是:
|
||||
|
||||
- 很快进书
|
||||
- 越读越完整
|
||||
- 而不是“先等很久,之后一次性全有”
|
||||
|
||||
---
|
||||
|
||||
## 4. 为什么这是最快可落地的方案
|
||||
|
||||
相比继续优化单次 `build()` 的 CPU 细节,这个方案收益更直接:
|
||||
|
||||
- 《凡人修仙传》的核心问题是“全书串行工作量太大”,不是“当前章节单章慢到不可接受”。
|
||||
- 当前代码已经天然按“章节”组织:
|
||||
- `publication.spine`
|
||||
- `RDEPUBTextChapter`
|
||||
- `RDEPUBChapterData`
|
||||
- 每章 `render + paginate`
|
||||
- [阅读器功能开发计划.md](/Users/shenlei/Work/ReadViewSDK/Doc/阅读器功能开发计划.md) 里也已经明确把“增量构建”列为方向,说明这条路和现有架构一致。
|
||||
|
||||
换句话说,这不是推翻重做,而是把现有 `RDEPUBTextBookBuilder.build()` 从“必须一次性跑完整本书”拆成“可按章节单独执行”。
|
||||
|
||||
---
|
||||
|
||||
## 5. 具体改造点
|
||||
|
||||
## 5.1 构建层:把全书构建拆成章节级能力
|
||||
|
||||
当前:
|
||||
|
||||
- [RDEPUBTextBookBuilder.swift](/Users/shenlei/Work/ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift) 只有全书 `build(...)`
|
||||
|
||||
建议新增:
|
||||
|
||||
```swift
|
||||
func buildChapter(
|
||||
parser: RDEPUBParser,
|
||||
publication: RDEPUBPublication,
|
||||
spineIndex: Int,
|
||||
pageSize: CGSize,
|
||||
style: RDEPUBTextRenderStyle
|
||||
) throws -> RDEPUBTextChapterBuildResult
|
||||
```
|
||||
|
||||
建议返回:
|
||||
|
||||
- `chapter: RDEPUBTextChapter`
|
||||
- `paginationDiagnostic`
|
||||
- `resourceDiagnostics`
|
||||
- `performanceSample`
|
||||
|
||||
这样做的好处:
|
||||
|
||||
- 章节构建逻辑可以和现有 `build()` 共享
|
||||
- 后续“首屏只构建一章”和“后台补全整本书”都走同一套实现
|
||||
|
||||
## 5.2 数据层:允许 TextBook 从“不完整”逐步变完整
|
||||
|
||||
当前:
|
||||
|
||||
- `RDEPUBTextBook` 默认假设 `chapters/pages/indexTable` 已经是完整全书
|
||||
|
||||
建议新增一个运行时模型,例如:
|
||||
|
||||
```swift
|
||||
final class RDEPUBIncrementalTextBookStore
|
||||
```
|
||||
|
||||
职责:
|
||||
|
||||
- 保存已完成构建的章节
|
||||
- 维护 `spineIndex -> chapter` 映射
|
||||
- 动态生成当前可用的 pages snapshot
|
||||
- 在“部分章节可用”时提供章节内阅读支持
|
||||
|
||||
建议暴露能力:
|
||||
|
||||
- `chapter(forSpineIndex:)`
|
||||
- `availablePagesSnapshot()`
|
||||
- `merge(chapter:)`
|
||||
- `isChapterReady(_:)`
|
||||
- `readyChapterRange(around:)`
|
||||
|
||||
原因:
|
||||
|
||||
- `RDEPUBTextBook` 更适合“完整产物”
|
||||
- 增量加载需要一个“半成品但可读”的状态容器
|
||||
|
||||
## 5.3 分页协调层:把一次性 loading 改成两阶段 loading
|
||||
|
||||
当前:
|
||||
|
||||
- [RDEPUBReaderPaginationCoordinator.swift](/Users/shenlei/Work/ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swift) 中 `paginatePublication()` 直接把全书构建作为进入阅读器前置条件
|
||||
|
||||
建议改成:
|
||||
|
||||
### Step 1:首次只构建目标章节
|
||||
|
||||
- 根据 `restoreLocation` 算出目标 `spineIndex`
|
||||
- 若无恢复位置,默认 `spineIndex = 0`
|
||||
- 只构建该章,必要时加 `±1` 章
|
||||
|
||||
### Step 2:先应用局部 snapshot
|
||||
|
||||
- `readerView.reloadData()`
|
||||
- 允许用户开始阅读
|
||||
- tool chrome 可先显示,但某些依赖全书的能力先降级
|
||||
|
||||
### Step 3:后台继续全书补建
|
||||
|
||||
- 串行队列逐章构建剩余章节
|
||||
- 每完成一章,就 merge 进 store
|
||||
- 必要时再刷新目录页码、搜索索引、页码总数
|
||||
|
||||
## 5.4 位置恢复:首期优先恢复“章节”,二期补齐“页内精度”
|
||||
|
||||
当前:
|
||||
|
||||
- 恢复逻辑大量依赖完整 `RDEPUBTextBook` 的 pageNumber / location 映射
|
||||
|
||||
首期建议:
|
||||
|
||||
- 打开时先把恢复目标收敛到 `spineIndex + fragment/rangeAnchor`
|
||||
- 只要目标章节 ready,就先进入该章节
|
||||
- 若该章节内页内恢复信息尚未完整,先恢复到该章节接近位置
|
||||
|
||||
这样能显著减少“为了恢复精确页码,必须先构建全书”的耦合。
|
||||
|
||||
## 5.5 缓存层:从整书缓存扩展到单章缓存
|
||||
|
||||
当前:
|
||||
|
||||
- `RDEPUBPaginationCacheCoordinator` / `RDEPUBTextBookCache` 更偏整书结果
|
||||
|
||||
建议:
|
||||
|
||||
- 缓存 key 增加 `spineIndex`
|
||||
- 支持单章 page ranges 命中
|
||||
- 首次打开大书时,优先读取:
|
||||
- 当前章节缓存
|
||||
- 相邻章节缓存
|
||||
|
||||
收益:
|
||||
|
||||
- 同一本大书二次打开时,可直接秒开到当前章节
|
||||
- 不用再等待整本书缓存完全重建
|
||||
|
||||
---
|
||||
|
||||
## 6. 首期最小可交付版本
|
||||
|
||||
为了最快解决《凡人修仙传》慢启动,建议首期只做下面这些:
|
||||
|
||||
1. 为 `RDEPUBTextBookBuilder` 抽出章节级构建接口
|
||||
2. `RDEPUBReaderPaginationCoordinator` 首次打开时只构建目标章节
|
||||
3. 用“局部 pages snapshot”驱动 `RDReaderView`
|
||||
4. 后台串行构建剩余章节
|
||||
5. 当前章节缓存命中优先
|
||||
|
||||
首期明确不做:
|
||||
|
||||
- 全书搜索实时可用
|
||||
- 全书总页数一开始就准确
|
||||
- 目录面板一开始就显示所有章节页码
|
||||
|
||||
这些能力可以在后台补建完成后逐步恢复。
|
||||
|
||||
---
|
||||
|
||||
## 7. UI 与交互建议
|
||||
|
||||
为了让“快速进入但后台仍在准备”体验自然,建议增加轻量提示:
|
||||
|
||||
### 阅读器内状态提示
|
||||
|
||||
- 首屏进入后不再是全屏 loading
|
||||
- 改为顶部或底部轻提示:
|
||||
- `正在准备后续章节...`
|
||||
- `已进入阅读,可继续翻页`
|
||||
|
||||
### 未就绪章节翻页策略
|
||||
|
||||
当用户快速翻到尚未构建的章节时:
|
||||
|
||||
- 优先命中后台预构建结果
|
||||
- 如果还未就绪:
|
||||
- 显示章节级 loading skeleton
|
||||
- 不要退回全屏 blocking loading
|
||||
|
||||
### 目录与搜索降级
|
||||
|
||||
- 目录先显示标题,不强依赖页码
|
||||
- 搜索面板可在全书索引未完成时显示:
|
||||
- `正文已可阅读,全文搜索仍在准备中`
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与应对
|
||||
|
||||
## 风险 1:现有很多 API 默认依赖完整 TextBook
|
||||
|
||||
例如:
|
||||
|
||||
- `pageNumber(for:)`
|
||||
- `location(forPageNumber:)`
|
||||
- `chapterData(forPageNumber:)`
|
||||
|
||||
应对:
|
||||
|
||||
- 首期不要强行让这些 API 在“半本书”状态下也完整成立
|
||||
- 先给增量模式增加“可用性边界”
|
||||
- 在运行时根据 `isFullyBuilt` / `isChapterReady` 分流
|
||||
|
||||
## 风险 2:局部 snapshot 和完整 snapshot 切换时页码跳动
|
||||
|
||||
应对:
|
||||
|
||||
- 局部模式优先用章节内位置恢复,不强调绝对页码稳定
|
||||
- 后台切换到完整模型时,按 `location` 而不是按 `pageNumber` 恢复
|
||||
|
||||
## 风险 3:后台补建打断当前交互
|
||||
|
||||
应对:
|
||||
|
||||
- 章节构建使用串行后台队列
|
||||
- UI 合并更新节流,例如“每完成 N 章再刷新一次目录状态”
|
||||
- 当前可见章节不重复重建
|
||||
|
||||
---
|
||||
|
||||
## 9. 推荐实施顺序
|
||||
|
||||
### Phase 1:快速进入 MVP
|
||||
|
||||
- 抽 `buildChapter(...)`
|
||||
- 首次打开只构建目标章节
|
||||
- 局部 snapshot 驱动阅读器
|
||||
- 后台补建剩余章节
|
||||
|
||||
### Phase 2:缓存加速
|
||||
|
||||
- 单章分页缓存
|
||||
- 恢复位置附近章节优先命中
|
||||
- 二次打开大书进一步提速
|
||||
|
||||
### Phase 3:全书能力渐进恢复
|
||||
|
||||
- 全书搜索索引后台构建
|
||||
- TOC 页码后台补齐
|
||||
- 全书总页数在构建完成后更新
|
||||
|
||||
---
|
||||
|
||||
## 10. 建议验收方式
|
||||
|
||||
建议拿《凡人修仙传》精校版全本做专项验证,记录以下指标:
|
||||
|
||||
- 点击书籍到首屏可读的耗时
|
||||
- 点击书籍到全书构建完成的耗时
|
||||
- 首屏进入时用户是否已经可以翻当前章节
|
||||
- 翻到下一章节时是否出现明显阻塞
|
||||
- 二次打开同一本书时是否明显快于首次
|
||||
|
||||
重点不是只看“总构建时长”,而是看:
|
||||
|
||||
- `Time To First Readable Page`
|
||||
- `Time To Full Book Ready`
|
||||
|
||||
这两个指标在大书场景里要分开看。
|
||||
|
||||
---
|
||||
|
||||
## 11. 最终建议
|
||||
|
||||
对《凡人修仙传》这类超大正文书,最快见效的方案不是继续压榨单次全书分页性能,而是:
|
||||
|
||||
**把阅读器打开流程从“全书先构建完”改成“当前章节先可读,剩余章节后台补齐”。**
|
||||
|
||||
这是当前代码架构下收益最高、侵入性也相对可控的做法,因为:
|
||||
|
||||
- 现有数据天然按章节组织
|
||||
- `RDEPUBTextBookBuilder` 已经具备章节级循环结构
|
||||
- `RDEPUBReaderPaginationCoordinator` 也已经是集中调度入口
|
||||
|
||||
如果只允许做一件事来解决《凡人修仙传》打开慢的问题,我建议优先做:
|
||||
|
||||
**Phase 1:章节级增量构建 + 两阶段进入阅读器。**
|
||||
|
||||
---
|
||||
|
||||
## 12. 当前落地状态(2026-06-02)
|
||||
|
||||
本轮已按 Phase 1 做了首期快速进入优化,当前实现状态如下。
|
||||
|
||||
### 已完成
|
||||
|
||||
- `RDEPUBTextBookBuilder` 已抽出章节级构建接口 `buildChapter(...)`,单章构建复用原有 render、分页、尾页规范化、诊断和分页缓存逻辑。
|
||||
- `RDEPUBReaderPaginationCoordinator` 的 `textReflowable` 路径已改为两阶段:
|
||||
- 第一阶段:按恢复位置优先构建可用章节,并立即应用局部 `RDEPUBTextBook` 进入阅读器。
|
||||
- 第二阶段:后台继续按章节增量构建,但增量结果会先暂存,只在用户空闲时再合并到当前可读内容,最终补齐为完整全书模型。
|
||||
- 快速进入阶段不只尝试单个 spine,而是按离恢复位置最近的可构建 HTML/XHTML spine 逐个尝试,避免封面、版权页、空白扉页导致首屏快速路径落空。
|
||||
- 已修正阅读路径误判:普通静态 SVG 封面不再把整本小说误判为 `webInteractive`,避免错误掉回 `WKWebView` 分页路径。
|
||||
- 首包策略已调整为“目标章节 + 后续 2 章”,默认先提供 3 章连续可读内容。
|
||||
- 后台补齐策略已调整为“每新增 20 章生成一份新的局部结果”,并优先补当前可读窗口之后的章节,再回补前文。
|
||||
- 增量结果不再一生成就立即 `applyTextBook`,而是只保留最近一份 staged `RDEPUBTextBook`,等用户停止翻页且阅读器回到 idle 后再统一合并,减少翻页过程中的 UI reload 和主线程抖动。
|
||||
- 已对后台分页任务做降干扰处理:章节补建切到较低优先级队列,用户刚翻页时后台任务会短暂停让,减少 pageCurl 翻页时的 CPU 抢占。
|
||||
- 后台完整构建失败时,如果局部章节已经成功进入阅读器,则不再把用户退回阻塞式错误流程,只结束 loading;如果局部章节也未成功,则按原错误处理。
|
||||
|
||||
### 仍未完成
|
||||
|
||||
- 还没有引入独立的 `RDEPUBIncrementalTextBookStore`,当前首期实现采用“局部 `RDEPUBTextBook` 先应用,完整 `RDEPUBTextBook` 后替换”的 MVP 路径。
|
||||
- 目录页码、全文搜索、全书总页数仍依赖后台完整构建完成后恢复。
|
||||
- 尚未加入可视化的“后续章节准备中”轻提示。
|
||||
|
||||
### 当前预期效果
|
||||
|
||||
对《凡人修仙传》精校版全本这类章节多、正文长的大书,首屏进入不再等待整本书逐章 render + paginate 全部完成,而是先等待目标正文附近 3 章构建完成。全书分页仍会继续执行,但它从“进入阅读器前置条件”变成了“阅读器内后台补齐任务”;后台每补齐 20 章会生成一份新的局部结果,不过这份结果会先暂存,等用户空闲时再并入当前可读内容。
|
||||
|
||||
### 验证状态
|
||||
|
||||
已通过 CocoaPods workspace 构建 Demo,确认本轮快速进入优化可编译:
|
||||
|
||||
```text
|
||||
xcodebuild -workspace ReadViewDemo/ReadViewDemo.xcworkspace \
|
||||
-scheme ReadViewDemo \
|
||||
-configuration Debug \
|
||||
-derivedDataPath /private/tmp/ReadViewDemoDerivedData \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
CODE_SIGNING_REQUIRED=NO \
|
||||
build
|
||||
```
|
||||
|
||||
结果:`BUILD SUCCEEDED`。
|
||||
|
||||
注意:直接构建 `ReadViewDemo.xcodeproj` 会绕开 Pods,导致 `import RDReaderView` 模块解析失败;验证时应使用 `ReadViewDemo/ReadViewDemo.xcworkspace`。
|
||||
|
||||
后续仍需要在真机或模拟器上用《凡人修仙传》精校版全本做实际打开耗时对比,重点记录 `Time To First Readable Page` 和 `Time To Full Book Ready`。
|
||||
@ -1,6 +1,6 @@
|
||||
# 架构对比分析:读书 vs ReadViewSDK
|
||||
|
||||
> 基于读书 v10.0.3 (Build 79) 逆向文档,与当前 ReadViewSDK 代码在 2026-05-24 的核查结果整合。
|
||||
> 基于读书 v10.0.3 (Build 79) 逆向文档,与当前 ReadViewSDK 代码在 2026-06-02 的核查结果整合。
|
||||
> 本文档已吸收原 [WXRead剩余问题修复计划.md](/Users/shen/Work/Code/ReadViewSDK/Doc/WXRead剩余问题修复计划.md) 的阶段方案,后续以本文档作为单一真值。
|
||||
|
||||
> 标注说明:
|
||||
@ -22,10 +22,11 @@ ReadViewSDK 当前已经不是“旧 UITextView 阅读器”了,文本主链
|
||||
- 页面级 hit test / 选区 / 高亮 / 批注
|
||||
- 字符锚点与 `fileIndex/row/column` 语义的完整位置模型
|
||||
|
||||
核查结论:在当前约定范围内,ReadViewSDK 已经完成 EPUB 阅读器对 WXRead 文档主链路的复刻。当前文档里不再保留主链路级 `⚠️` 项,剩余仅有两类:
|
||||
核查结论:ReadViewSDK 已经完成 EPUB 阅读器主链路的大部分复刻,但当前代码里仍有少量“能力面已接入、实现路径未完全等价”的差异。和 2026-05-24 版本文档相比,当前最需要重新标注的是:
|
||||
|
||||
1. `replaceForMPChapter.css` 这类公众号文章专用资源,对 EPUB 主链路不适用
|
||||
2. 繁简转换、TTS / DRM / Pencil 这类已明确排除在本次复刻范围之外的外围能力
|
||||
1. 文本分页主路径现已真正消费多栏 path,并补上 `avoidOrphans` / `avoidWidows` / `hyphenation` 行为
|
||||
2. 代码仍保留若干 fallback / degrade 路径,与 WXRead 的单一主链路实现方式不同
|
||||
3. `replaceForMPChapter.css`、繁简转换、TTS / DRM / Pencil 仍属于不适用或明确不实现范围
|
||||
|
||||
---
|
||||
|
||||
@ -43,11 +44,14 @@ ReadViewSDK 当前已经不是“旧 UITextView 阅读器”了,文本主链
|
||||
- 分页器已经补上 inline footnote attachment 不整段挪页、标题 `keepWithNext`、`weread-page-relate` 页首借行这几类 WXRead 风格规则。
|
||||
- 章节尾部“仅空白/段落分隔符”的尾页丢弃,以及极短尾页回并已经落地,`宝山辽墓材料与释读` 第 31 页空白问题已修复。
|
||||
- `RDEPUBTextLayoutConfig` 已补齐到 WXRead 同级配置面:`frameWidth/frameHeight/edgeInsets/numberOfColumns/columnGap/avoidOrphans/avoidWidows/hyphenation`,并已接入分页入口与缓存键。
|
||||
- CoreText 分页路径现已消费多栏 layout path,不再把 `numberOfColumns` 只停留在配置层。
|
||||
- `avoidOrphans`、`avoidWidows`、`hyphenation` 现已接入实际排版/分页行为,而不是仅存在于配置模型。
|
||||
- `RDEPUBChapterData` 已统一承载章节分页结果、位置查询、搜索/高亮回查与目录语义,章节模型主链路已经收口。
|
||||
|
||||
### ⚠️ 有差异/有问题
|
||||
|
||||
- 无主链路遗留项;当前仅保留明确不适用或明确不实现的范围说明。
|
||||
- 仍保留降级链路:`RDEPUBDTCoreTextRenderer` 在 `DTCoreText` 不可用或构建失败时会退回 HTML 纯文本渲染;`RDURLReaderController` 在纯文本分页失败时会退回 `UITextView` 展示。这说明 ReadViewSDK 仍不是 WXRead 那种完全单一路径的生产态实现。
|
||||
- `RDReaderGestureController` 仍是未接入的占位组件,实际点击分区逻辑直接落在 `RDReaderView`,与 WXRead 更完整的翻页/手势控制器拆分相比,宿主层职责仍稍偏重。
|
||||
|
||||
### ❌ 未实现
|
||||
|
||||
@ -81,10 +85,10 @@ ReadViewSDK 当前已经不是“旧 UITextView 阅读器”了,文本主链
|
||||
| 标题 keep-with-next | 标题不能孤悬页尾 | 已补 `keepWithNext` 语义与页尾回退 | ✅ 已实现 |
|
||||
| `weread-page-relate` | 页首关联块需要借上一页一行 | 已补页首 `pageRelate` 借行规则 | ✅ 已实现 |
|
||||
| 章节尾页收口 | 丢弃空白尾页、合并极短尾页 | 已补尾页空白丢弃与超短尾页回并 | ✅ 已实现 |
|
||||
| 分页配置 | `WRCoreTextLayoutConfig`(含多栏等) | `RDEPUBTextLayoutConfig` 已补齐同级参数,并接入分页入口/缓存键/列路径构建 | ✅ 已实现 |
|
||||
| 分页配置 | `WRCoreTextLayoutConfig`(含多栏等) | `RDEPUBTextLayoutConfig` 已补齐同级参数,并已接入多栏 path、孤行/寡行保护与 hyphenation 行为 | ✅ 已实现 |
|
||||
| 缓存 | 按书籍 + 排版设置缓存 | 已有 `RDEPUBTextBookCache` 磁盘缓存 | ✅ 已实现 |
|
||||
|
||||
**结论:** 分页主链路已经按 WXRead 收口,当前差异不再落在分页引擎或分页配置能力面上。
|
||||
**结论:** 分页主链路已经按 WXRead 的主要思路收口;当前剩余差异不再集中在分页配置或分页算法本身,而更多落在 fallback 路径和宿主层实现细节。
|
||||
|
||||
---
|
||||
|
||||
@ -175,6 +179,8 @@ ReadViewSDK 当前已经不是“旧 UITextView 阅读器”了,文本主链
|
||||
| 页面几何模型完全等价 `WRCoreTextLayoutFrame` | ✅ 已实现 | - |
|
||||
| `WRBookmark` 统一标注模型 | ✅ 已实现 | - |
|
||||
| 多栏排版 | ✅ 已实现 | - |
|
||||
| `avoidOrphans / avoidWidows / hyphenation` 行为落地 | ✅ 已实现 | - |
|
||||
| 纯文本/渲染失败 fallback 清理 | ⚠️ 仍保留纯文本 fallback 路径 | 低 |
|
||||
| 字体动态加载 | ✅ 已实现 | - |
|
||||
| 繁简转换 | ❌ 明确不实现 | - |
|
||||
| TTS / DRM / Pencil | ❌ 明确不实现 | - |
|
||||
@ -216,6 +222,17 @@ ReadViewSDK 当前已经不是“旧 UITextView 阅读器”了,文本主链
|
||||
|
||||
---
|
||||
|
||||
## 补充说明:本次复核新增结论(2026-06-02)
|
||||
|
||||
和上一篇版本相比,当前最重要的修正不是“新增缺很多能力”,而是把原先写得过满的结论收回来:
|
||||
|
||||
- `RDEPUBTextLayoutConfig` 这一轮已经从“配置面补齐”推进到“行为面落地”,`avoidOrphans`、`avoidWidows`、`hyphenation` 不再只是模型字段。
|
||||
- 多栏能力这一轮已经接到文本 `CoreText` 分页主路径,`numberOfColumns` 不再只影响 Web/CSS 与设置入口。
|
||||
- 文本阅读主链路虽然已经不再依赖旧 `UITextView`,但仓库中仍保留 `DTCoreText` 不可用时的纯文本 fallback,以及纯文本分页失败时的 `UITextView` fallback,应视作与 WXRead 的实现差异,而不是主链路能力。
|
||||
- 公众号文章专用 `replaceForMPChapter.css` 仍不属于当前 EPUB 主链路缺口,应继续按“不适用”记录,而不是“待补齐”。
|
||||
|
||||
---
|
||||
|
||||
## 10. 读书关键类职责速查
|
||||
|
||||
| 类名 | 职责 | ReadViewSDK 对应 | 核查 |
|
||||
|
||||
Loading…
Reference in New Issue
Block a user