- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
412 lines
19 KiB
Markdown
412 lines
19 KiB
Markdown
# ReadViewSDK 代码审查报告
|
||
|
||
> 审查范围:`Sources/RDEpubReaderView/` 全部 184 个 Swift 文件(约 32,300 行)
|
||
> 审查维度:并发安全、内存管理、API 正确性、Swift/iOS 平台特定、架构与依赖
|
||
> 审查日期:2026-06-26
|
||
|
||
---
|
||
|
||
## 严重程度定义
|
||
|
||
| 标记 | 含义 |
|
||
|------|------|
|
||
| 🔴 高 | 有明确代码路径可导致崩溃、数据丢失或严重功能缺陷 |
|
||
| 🟡 中 | 方向正确但风险程度取决于运行时条件;或当前安全但架构上脆弱 |
|
||
| 🟢 低 | 代码异味、可维护性问题或未来风险 |
|
||
|
||
---
|
||
|
||
## 🔴 高优先级问题
|
||
|
||
### H-01 · 内存警告未连接章节缓存清理
|
||
|
||
`RDEPUBChapterRuntimeStore.handleMemoryWarning()` 已实现(清空除当前章节外的缓存 + 清除图片缓存),但从未被调用。
|
||
|
||
- `RDEPUBReaderRuntime.handleMemoryWarning()` 只清理了 `backgroundCoverageStore`,未调用 `chapterRuntimeStore.handleMemoryWarning()`
|
||
- `RDEPUBReaderController` 未重写 `didReceiveMemoryWarning`
|
||
|
||
**影响:** 系统内存压力下,`RDEPUBChapterDataCache`(无上限字典)和 `imageCache`(仅 countLimit=50,无 totalCostLimit)不会主动释放内存。
|
||
|
||
**位置:**
|
||
- `RDEPUBChapterRuntimeStore.swift:103` — `handleMemoryWarning()` 方法存在
|
||
- `RDEPUBReaderRuntime.swift:606-620` — 只清理 backgroundCoverageStore
|
||
|
||
---
|
||
|
||
### H-02 · 主线程信号量等待
|
||
|
||
`RDEPUBChapterLoader.loadChapterSynchronouslyForMigration()` 使用 `DispatchSemaphore.wait()` 阻塞调用线程。当从 `RDEPUBPresentationRuntime.rebindVisibleLocation()` 以 `allowSynchronousLoad: true` 调用时,会在主线程等待。
|
||
|
||
**影响:** 如果章节数据未缓存,主线程被阻塞直到章节构建完成,可能导致 ANR 或死锁(如果构建过程需要主线程资源)。
|
||
|
||
**位置:**
|
||
- `RDEPUBChapterLoader.swift:206-268` — `semaphore.wait()`
|
||
- `RDEPUBPresentationRuntime.swift:282-286` — `allowSynchronousLoad: true`
|
||
|
||
---
|
||
|
||
### H-03 · asyncAfter 闭包中的强制解包
|
||
|
||
`RDEPUBReaderController+DataSource.swift:425-436` 在 0.3 秒延迟闭包中强制解包 `lastTextPaginationPageSize!`。方法入口的 `guard let lastTextPaginationPageSize` 不保护延迟闭包——0.3 秒后该值可能已变为 `nil`。
|
||
|
||
**位置:**
|
||
```swift
|
||
// line 432-433
|
||
let stillChanged = abs(currentSize.width - self.lastTextPaginationPageSize!.width) > 0.5
|
||
|| abs(currentSize.height - self.lastTextPaginationPageSize!.height) > 0.5
|
||
```
|
||
|
||
---
|
||
|
||
### H-04 · `unowned context` 在后台操作中的崩溃路径
|
||
|
||
以下两个类在后台操作中访问 `unowned let context: RDEPUBReaderContext`,若 controller 在操作期间释放则会崩溃:
|
||
|
||
**RDEPUBMetadataParseWorker:**
|
||
- Line 117: `let context = self.context` 在 `[weak self]` guard 之前将 unowned 引用捕获为局部变量
|
||
- Lines 197-324: `BlockOperation` 闭包直接使用该局部变量(unowned),无 `[weak self]` 保护
|
||
- Lines 296-301, 372-376: `DispatchQueue.main.async` 闭包捕获 unowned 局部变量
|
||
|
||
**RDEPUBReaderLoadCoordinator:**
|
||
- Line 28: 闭包只捕获 `[weak controller]`,未捕获 `[weak self]`
|
||
- Line 30: `self.context.makeParser()` 在后台队列访问 unowned 引用
|
||
- Lines 41-42, 53-54: `DispatchQueue.main.async` 闭包直接访问 `self.context`
|
||
|
||
**影响:** 若 `RDEPUBReaderController` 在后台操作期间释放(如用户快速退出阅读器),`unowned` 引用悬挂导致 EXC_BAD_ACCESS。
|
||
|
||
**位置:**
|
||
- `RDEPUBMetadataParseWorker.swift:117, 197-324, 296-301, 372-376`
|
||
- `RDEPUBReaderLoadCoordinator.swift:28-57`
|
||
|
||
---
|
||
|
||
### H-05 · 章节数据缓存无容量限制
|
||
|
||
`RDEPUBChapterDataCache` 是无上限的 `[Int: RDEPUBRuntimeChapter]` 字典,仅通过显式 `remove(spineIndex:)` 或 `removeAll()` 逐出。每章持有完整的 `typesetAttributedString`(50-200KB+),50 章可达 5-10MB+。
|
||
|
||
**位置:** `RDEPUBChapterDataCache.swift:5` — `private var storage: [Int: RDEPUBRuntimeChapter] = [:]`
|
||
|
||
---
|
||
|
||
### H-06 · 暗色图片静态 NSCache 无 totalCostLimit
|
||
|
||
`RDEPUBDarkImageAdjuster.imageCache` 是 static `NSCache<NSString, UIImage>()`,无 `totalCostLimit`,跨越整个 App 生命周期累积调整后的图片。
|
||
|
||
**位置:** `RDEPUBDarkImageAdjuster.swift:9`
|
||
|
||
---
|
||
|
||
### H-07 · 持久化操作静默丢弃错误
|
||
|
||
`RDEPUBReaderPersistence` 中所有编解码操作均使用 `try?`,失败时:
|
||
- **保存**:静默返回,数据丢失无任何反馈(lines 84, 98, 112, 131)
|
||
- **加载**:返回 `nil` 或空数组,损坏/版本不匹配的数据无法恢复(lines 80, 94, 108, 127)
|
||
|
||
**影响:** 用户书签和阅读位置可能在无声中丢失。
|
||
|
||
**位置:** `RDEPUBReaderPersistence.swift:76-131`
|
||
|
||
---
|
||
|
||
### H-08 · WKURLSchemeHandler 线程违规与取消回调缺失
|
||
|
||
两个确定 bug:
|
||
|
||
**(a) 不同队列回调:** `webView(_:start:)` 在某个串行队列(由 WebKit 调度)上被调用,但 `respondWithInMemoryData` 和 `respondWithStreaming` 将 `didReceive`/`didFinish`/`didFailWithError` 显式切到了 `ioQueue`(line 121, 151)。这至少违反了"回调应与 `start` 使用同一串行队列"的要求。
|
||
|
||
**(b) 取消时不调用完成回调:** 任务被 `stop` 后,`isTaskActive` 返回 false 时:
|
||
- streaming 路径(line 178):`guard self.isTaskActive(taskID) else { return }` — 仅 return,不调用 `didFinish` 或 `didFailWithError`
|
||
- in-memory 路径(line 128, 141):同理
|
||
|
||
这违反了"每个 started task 必须收到完成回调"的协议要求。
|
||
|
||
**位置:** `RDEPUBResourceURLSchemeHandler.swift:121-187`
|
||
|
||
---
|
||
|
||
### H-09 · 搜索在后台线程同步加载所有章节
|
||
|
||
`RDEPUBReaderSearchCoordinator` 在 `searchQueue` 上遍历所有 buildable spine 索引,逐个同步加载章节。每章持有完整 `typesetAttributedString`,大型书籍搜索时内存峰值显著。
|
||
|
||
**位置:** `RDEPUBReaderSearchCoordinator.swift:47, 173-236`
|
||
|
||
---
|
||
|
||
### H-10 · 依赖版本未锁定
|
||
|
||
`RDEpubReaderView.podspec` 中 DTCoreText、SnapKit、SSAlertSwift 无版本约束:
|
||
|
||
```ruby
|
||
s.dependency 'ZIPFoundation', '~> 0.9' # ✅ 已锁定
|
||
s.dependency 'DTCoreText' # ❌ 无约束
|
||
s.dependency 'SnapKit' # ❌ 无约束
|
||
s.dependency 'SSAlertSwift' # ❌ 无约束
|
||
```
|
||
|
||
**位置:** `RDEpubReaderView.podspec:17-20`
|
||
|
||
---
|
||
|
||
### H-11 · SnapKit 和 SSAlertSwift 疑似死依赖
|
||
|
||
全仓库搜索未找到任何 `snp.` 或 `SSAlert` 的使用点。这两个依赖可能增加了不必要的二进制体积和潜在冲突。
|
||
|
||
---
|
||
|
||
## 🟡 中优先级问题
|
||
|
||
### M-01 · `RDEPUBReaderState` 无同步保护(隐性风险)
|
||
|
||
所有属性(`parser`, `publication`, `activeBookmarks`, `pendingPageMapUpdates` 等)均为 `var`,无锁、无 `@MainActor`、无 `dispatchPrecondition`。当前所有访问确实在主线程(经追踪 16 个 `pendingPageMapUpdates` 访问点确认),但缺少形式化保障——未来开发者可能无意从后台线程访问而不自知。
|
||
|
||
**位置:** `RDEPUBReaderState.swift:1-71`
|
||
|
||
---
|
||
|
||
### M-02 · `currentSpineIndex` / `windowSpineIndices` 无锁但当前安全
|
||
|
||
这两个字段无锁保护,但追踪全部读写路径后确认当前均在主线程。风险是隐性的:store 对象被传入 `chapterLoadQueue` 上下文,未来开发者可能在该队列中访问这些属性而不自知。同文件的其他字段(`navigationLock`, `prefetchLock`, `buildingLock`, `cfiMapLock`)有锁保护,形成不一致的模式。
|
||
|
||
**位置:** `RDEPUBChapterRuntimeStore.swift:15-17`
|
||
|
||
---
|
||
|
||
### M-03 · `warmAnchors` 跨线程访问无同步
|
||
|
||
`RDEPUBBackgroundPriorityManager.warmAnchors` 在主线程写入(`addWarmAnchor`),在 `MetadataParseWorker` 的后台线程读取(`makeMetadataPriorityOrder`),无锁保护。
|
||
|
||
**位置:** `RDEPUBBackgroundPriorityPolicy.swift:65`
|
||
|
||
---
|
||
|
||
### M-04 · `assert(Thread.isMainThread)` 在 Release 中被移除
|
||
|
||
`RDEPUBReaderContext` 的 `makeLayoutSnapshot()`、`currentTextPageSize()` 等方法使用 `assert(Thread.isMainThread)` 检查。`assert` 在 `-O` 构建中被移除,生产环境中后台线程调用将静默访问 UIKit 属性,造成数据竞争且无诊断。
|
||
|
||
**应使用:** `dispatchPrecondition(condition: .onQueue(.main))` 或 `@MainActor` 标注。
|
||
|
||
**位置:** `RDEPUBReaderContext.swift:172, 188, 210, 215`
|
||
|
||
---
|
||
|
||
### M-05 · 磁盘缓存无自动淘汰策略
|
||
|
||
`RDEPUBChapterSummaryDiskCache` 和 `RDEPUBTextBookCache` 只有全量清除操作(`removeAll()`、`invalidateAll()`),无 LRU/大小/年龄驱逐策略。每次设置变更(字体、行距、主题)产生新缓存键,旧文件无限累积。写操作使用原子替换(tmp + replace),交错不会损坏数据,但可能读到旧值或看到不一致的目录状态。
|
||
|
||
**位置:**
|
||
- `RDEPUBChapterSummaryDiskCache.swift:89` — `removeAll()` 全清
|
||
- `RDEPUBTextBookCache.swift:191` — `invalidateAll()` 全清
|
||
|
||
---
|
||
|
||
### M-06 · CFI 范围分隔符可能与内容冲突
|
||
|
||
`RDEPUBCFICompatibility` 按 `".."` 和 `"-"` 分割 CFI 范围字符串,但这些字符可能出现在 CFI 文本断言内部(如 `[foo..bar]`)。代码先匹配 `".."` 再匹配 `"-"`,无歧义消解逻辑。
|
||
|
||
**位置:** `RDEPUBCFICompatibility.swift:18-21`
|
||
|
||
---
|
||
|
||
### M-07 · CFI 步索引对奇数输入容错不严谨
|
||
|
||
`RDEPUBCFIResolver` 中 `steps[1].index / 2 - 1` 对奇数索引做整除截断,可能映射到错误章节。`max(..., 0)` 仅防负索引,不防错误映射。属于 EPUB CFI 规范的输入验证问题。
|
||
|
||
**位置:** `RDEPUBCFIResolver.swift:38`
|
||
|
||
---
|
||
|
||
### M-08 · WKWebView 保留环需主动管理
|
||
|
||
`RDEPUBWebView` → `webView` → `configuration.userContentController` → `RDEPUBWebView`(作为 `WKScriptMessageHandler`)形成保留环。
|
||
|
||
主要解环路径是 `reset()` 和 `configureWebViewIfNeeded` 中主动调用 `teardownWebView()`。**注意:** `deinit` 虽也调用 `teardownWebView()`,但不能作为环会被打破的保证——保留环的存在意味着 `deinit` 可能永远不会触发。风险在于:若某条代码路径跳过了 `teardownWebView()`,环将持续且对象永远不会释放。
|
||
|
||
**位置:**
|
||
- `RDEPUBWebView.swift:129` — `deinit` 调用 `teardownWebView()`(不可靠兜底)
|
||
- `RDEPUBWebView.swift:138-160` — `reset()` 调用 `teardownWebView()`(主要解环路径)
|
||
|
||
---
|
||
|
||
### M-09 · `configuration` didSet 在 `isViewLoaded` 之前持久化
|
||
|
||
`RDEPUBReaderController.configuration` 的 `didSet` 在 `guard isViewLoaded` 之前执行了 `readerContext.configuration = configuration`、`applyWebViewDebugPolicy()` 和 `persistReaderSettingsIfNeeded()`。若在 view 加载前设置 configuration,这些副作用会在 UI 未就绪时触发。
|
||
|
||
**位置:** `RDEPUBReaderController.swift:8-20`
|
||
|
||
---
|
||
|
||
### M-10 · empty catch 吞掉章节加载错误
|
||
|
||
`RDEPUBReaderPaginationCoordinator` 中 `loadInitialInteractiveRuntimeChapters` 的 `catch {}` 为空,章节加载失败被静默忽略,无日志、无恢复。
|
||
|
||
**位置:** `RDEPUBReaderPaginationCoordinator.swift:283`
|
||
|
||
---
|
||
|
||
### M-11 · `RDEPUBReaderFontChoice.displayName` 始终返回中文
|
||
|
||
枚举的 `displayName` 计算属性硬编码中文字符串("系统"、"宋体"、"圆体"、"等宽"),无本地化基础设施。
|
||
|
||
**位置:** `RDEPUBReaderConfiguration.swift:18-28`
|
||
|
||
---
|
||
|
||
### M-12 · 大量硬编码中文字符串,本地化覆盖不完整
|
||
|
||
覆盖文件:`RDEPUBReaderSettingsViewController`、`RDEPUBReaderSearchBarView`、`RDEPUBReaderHighlightsViewController`、`RDEPUBReaderBookmarksViewController`、`RDEPUBReaderTopToolView`、`RDEPUBReaderBottomToolView`、`RDEPUBTextContentView`、`RDEPUBWebView+Configuration`、`RDEPUBChapterLoader`(错误信息)等。
|
||
|
||
项目中存在约 88 个硬编码中文字符串(如"拷贝"、"高亮"、"批注"、"目录"、"搜索"等),但同时也有使用 `NSLocalizedString` 的地方(如 `RDEPUBImageViewController.swift:126`)。本地化基础设施存在但覆盖不完整——新添加的 UI 文本大多直接硬编码中文而未走本地化流程。
|
||
|
||
---
|
||
|
||
### M-13 · DTCoreText 类型泄漏到 EPUBUI 层
|
||
|
||
5 个 EPUBUI 文件直接使用 `DTCoreTextLayoutFrame`、`DTCoreTextLayoutLine`、`DTCoreTextGlyphRun` 等 DTCoreText 类型(虽有 `#if canImport(DTCoreText)` 保护),意味着替换渲染引擎需修改 UI 层文件。
|
||
|
||
**位置:**
|
||
- `RDEPUBPageInteractionController.swift`
|
||
- `RDEPUBPageLayoutSnapshot.swift`
|
||
- `RDEPUBTextPageRenderView.swift`
|
||
- `RDEPUBTextContentView.swift`
|
||
- `RDEPUBDarkImageAdjuster.swift`
|
||
|
||
---
|
||
|
||
### M-14 · `imageCache` 有 countLimit 但无 totalCostLimit
|
||
|
||
`RDEPUBChapterRuntimeStore.imageCache` 设有 `countLimit = 50`,但无 `totalCostLimit`。50 张大尺寸书籍插图可达 150-250MB。
|
||
|
||
**位置:** `RDEPUBChapterRuntimeStore.swift:41`
|
||
|
||
---
|
||
|
||
### M-15 · `UIMenuController` 已废弃
|
||
|
||
`RDEPUBWebView+Configuration.swift:39` 和 `RDEPUBTextContentView.swift:747-758` 使用 `UIMenuController`,该 API 从 iOS 16 起废弃。应迁移至 `UIEditMenuInteraction`(iOS 16+),并为 iOS 15 保留 fallback。当前代码在 iOS 17+ 上仍可工作但随时可能失效。
|
||
|
||
---
|
||
|
||
### M-16 · Dynamic Type 未支持
|
||
|
||
UI 中所有字体使用硬编码大小(`UIFont.systemFont(ofSize: 13/14/16/18/19)`),未使用 `UIFontMetrics` 或 `preferredFont(forTextStyle:)`。启用 Dynamic Type 的用户看不到文字大小变化。
|
||
|
||
---
|
||
|
||
### M-17 · URL Scheme Handler 取消时 `clearTask` 可能被跳过
|
||
|
||
`respondWithInMemoryData` line 128 的 `guard self.isTaskActive(taskID) else { return }` 在取消时直接返回,跳过了 line 146 的 `self.clearTask(taskID)`。当前因 `stop` 已调用 `clearTask` 而无害,但结构上脆弱。
|
||
|
||
**位置:** `RDEPUBResourceURLSchemeHandler.swift:128`
|
||
|
||
---
|
||
|
||
### M-18 · 硬编码颜色值未集成到主题系统
|
||
|
||
`RDEPUBReaderSearchBarView` 含 18 个硬编码颜色字面量,默认高亮颜色 `"#F8E16C"` 硬编码在 `RDEPUBReaderController+PublicAPI.swift:65,75`。这些应属于 `RDEPUBReaderTheme`。
|
||
|
||
---
|
||
|
||
### M-19 · 硬编码时间常量
|
||
|
||
多个 `DispatchQueue.main.asyncAfter(deadline:)` 和 `Thread.sleep` 使用硬编码延迟值(0.08s、0.1s、0.15s、0.2s、0.25s、0.3s、1.0s、3.5s),无命名常量或可配置选项。
|
||
|
||
---
|
||
|
||
## 🟢 低优先级问题
|
||
|
||
### L-01 · `RDEPUBChapterLoader` 中 `[self]` 强捕获
|
||
|
||
Line 103 的 `store.chapterLoadQueue.async { [self] in` 强捕获 self,阻止 loader 在队列任务完成前释放。应改为 `[weak self]`。
|
||
|
||
**位置:** `RDEPUBChapterLoader.swift:103`
|
||
|
||
---
|
||
|
||
### L-02 · MetadataParseWorker Thread.sleep 忙等待
|
||
|
||
`waitForReadingInteractionToSettle()` 使用 `Thread.sleep(forTimeInterval: 0.08)` 轮询,无上限等待。应考虑 `DispatchSource` 或条件变量。
|
||
|
||
**位置:** `RDEPUBMetadataParseWorker.swift:385-390`
|
||
|
||
---
|
||
|
||
### L-03 · `RDEPUBTextBookCache.queue.sync` 潜在死锁
|
||
|
||
`load()` 和 `save()` 均使用 `queue.sync`。若从 queue 内部调用则会死锁。当前未发现此调用路径,但模式脆弱。
|
||
|
||
**位置:** `RDEPUBTextBookCache.swift:136-189`
|
||
|
||
---
|
||
|
||
### L-04 · `pendingLoads` 字典无超时清理
|
||
|
||
`RDEPUBChapterLoader.pendingLoads` 的完成闭包数组在章节加载失败且 `resolvePendingLoad` 未被调用时可能累积。当前所有路径均调用 `resolvePendingLoad`,但无防御性超时。
|
||
|
||
**位置:** `RDEPUBChapterLoader.swift:7-10`
|
||
|
||
---
|
||
|
||
### L-05 · `RDEPUBChapterWindowCoordinator` 是死代码
|
||
|
||
该类定义了但未被任何代码实例化或引用。其中对 `currentSpineIndex`/`windowSpineIndices` 的7个访问点均为死代码。
|
||
|
||
---
|
||
|
||
### L-06 · 平台版本不一致
|
||
|
||
- `podspec`: `s.platform = :ios, "15.0"`
|
||
- `Podfile`(根和 demo): `platform :ios, '15.6'`
|
||
|
||
---
|
||
|
||
### L-07 · `RDEPUBMetadataCancellationController` 微小竞争
|
||
|
||
`attach()` 方法在 `lock.unlock()` 和 `queue.cancelAllOperations()` 之间存在窄窗口,新操作可能被添加到队列但不会被取消。实际影响极小。
|
||
|
||
**位置:** `RDEPUBMetadataParseCancellationController.swift:attach()`
|
||
|
||
---
|
||
|
||
### L-08 · 页码标签硬编码偏移
|
||
|
||
`RDEPUBWebContentView` 和 `RDEPUBTextContentView` 的页码标签使用硬编码偏移(24pt, 20pt),在刘海屏设备上可能与安全区域重叠。
|
||
|
||
---
|
||
|
||
## 已排除项
|
||
|
||
以下原报告条目经核实后排除或合并:
|
||
|
||
| 原编号 | 原描述 | 排除原因 |
|
||
|--------|--------|----------|
|
||
| #6 | compactMap 结果越界 | 入口有 `childViewControllers.count == expectedCount` 检查,compactMap 后不会少于预期数量 |
|
||
| #7 | `result!` 强解包崩溃 | 信号量等待的真实风险是无限等待/卡住,不是 result 为 nil;signal 前必定赋值 |
|
||
| #21 | 搜索协调器 fallback 空 store | `?? RDEPUBChapterRuntimeStore()` 前的可选链若为 nil 则整个调用返回 nil,fallback 不会生效 |
|
||
| #23 | init 时加载所有 HTML 到内存 | 逐章取 HTML 算哈希后只保存 digest,HTML 字符串不长期保留 |
|
||
| #35 | `.nonPersistent()` 阻止 cookie/localStorage | 不持久化是有意设计,不是 bug;原始报告将"不持久化"误述为"不能用" |
|
||
| #48 | `didMoveToSuperview` 每次重复 makeUI | 有 `didBuildUI` 防重入保护 |
|
||
| 原L-04 | `activeTasks` 字典 `clearTask` 设 nil 不删键 | Swift 中 `dict[key] = nil` 等同于 `removeValue(forKey:)`,不保留空洞 |
|
||
| 原M-18 | MetadataParseWorker OperationQueue 未在 deinit 取消 | 外层 `[weak self]` guard 确保 self 存活才执行 queue 操作;`cancellationController` 提供显式取消路径 |
|
||
| 原L-08 | `ss_superViewController` 前缀暗示外部来源 | 命名风格猜测,非可验证缺陷 |
|
||
|
||
---
|
||
|
||
## 建议优先级排序
|
||
|
||
| 优先级 | 编号 | 修复复杂度 | 说明 |
|
||
|--------|------|-----------|------|
|
||
| P0 | H-01 | 低 | 在 `handleMemoryWarning` 中加一行 `chapterRuntimeStore.handleMemoryWarning()`,并在 controller 中转发 `didReceiveMemoryWarning` |
|
||
| P0 | H-04 | 中 | MetadataParseWorker 和 LoadCoordinator 中 `unowned context` 改为 `weak`,闭包加 `guard` |
|
||
| P0 | H-03 | 低 | `lastTextPaginationPageSize!` 改为 `guard let` 安全解包 |
|
||
| P1 | H-08 | 中 | URLSchemeHandler 回调改到与 start 相同队列,取消路径加 `didFailWithError` |
|
||
| P1 | H-02 | 中 | 主线程同步加载改为异步回调或确保不从主线程调用 |
|
||
| P1 | H-07 | 低 | 关键持久化操作加日志,至少记录失败原因 |
|
||
| P1 | H-10 | 低 | 给三个依赖加版本约束;移除 SnapKit 和 SSAlertSwift |
|
||
| P2 | H-05 | 中 | 为 ChapterDataCache 加 LRU 逐出策略或最大条目数限制 |
|
||
| P2 | H-06 | 低 | 为 DarkImageAdjuster.imageCache 设置 `totalCostLimit` |
|
||
| P2 | H-09 | 中 | 搜索改为流式/分批加载,避免一次性持有所有章节 |
|
||
| P2 | M-01/02 | 中 | 为 State 和 Store 关键属性加 `@MainActor` 或 `dispatchPrecondition` |
|
||
| P2 | M-04 | 低 | `assert` 改为 `dispatchPrecondition` 或 `precondition` |
|
||
| P2 | M-08 | 低 | 保留环加注释说明必须通过 `reset()` 打破 |
|
||
| P2 | M-15 | 中 | 迁移至 `UIEditMenuInteraction` |
|