- 新增 RDEPUBChapterDisplayContentCache(LRU 2,主线程限定,控制器
持有):整章显示串构建时一次性注入全章主题色与暗黑图替换,配套
共享 DTCoreTextLayouter;签名 = 章节内容对象标识 + 主题双色 +
暗黑图配置,设置/主题变化自动失效重建
- RDEPUBTextContentView.configure 改为引用共享 entry,删除每次翻页
的整章可变拷贝与按页属性写入
- 高亮/下划线改走 overlay decoration(高亮背景层、下划线前景层),
移除 applyHighlightsToContent 与 render view 的属性绘制路径,
高亮增删不再触发整章重建
- 内存警告时清空 display cache;shouldAvoidReaderPageCaching 保持
保守(P1-4 待真机数据)
验证:编译通过;高亮/批注/选区/翻页/设置 13 项 UI 回归全过。
SearchTests 10 项失败经二分确认为先存回归(5a41066 与 e4e629a 均
复现,最后已知通过为 2026-06-08),与本次改动无关,待单独排查。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
9.7 KiB
长章节内存优化 — 代码实施清单
背景:DTCoreText 文本页已采用"整章上下文布局",分页与显示边界一致(不可回退)。现状代价:每次翻页/预加载都整章重拷贝 + 重排版后丢弃,且章节缓存里每页常驻一份子串副本。
总原则:保留"整章参与排版",去掉"整章按页复制、按页可变、按页缓存"。不改分页器,不上窗口化。
P0:立即可做
P0-1 削减每页 content 子串常驻副本
现状:分页产物为每页保存整段 attributedSubstring,全章加总约等于又一份整章副本,随邻接窗口(radius 1,共 3 章)常驻。
生成点:
Sources/RDReaderView/EPUBUI/ReaderController/ChapterRuntime/RDEPUBChapterLoader.swift:494(buildPagesFromRanges)Sources/RDReaderView/EPUBTextRendering/BuildPipeline/RDEPUBTextBookBuilder.swift:294Sources/RDReaderView/EPUBTextRendering/RDPlainTextBookBuilder.swift:78
现存全部消费点(已盘点确认,仅 3 处):
RDEPUBTextContentView.swift:562— 封面检测coverImage(from: page.content),仅pageIndexInChapter == 0且 href 含 "cover"RDEPUBTextContentView.swift:641—normalizedPageContent,仅非 DTCoreText 的#else回退路径调用RDEPUBTextBookBuilder.swift:93— 构建期 debug 日志 preview
任务:
- 将
RDEPUBTextPage.content改为按需构造:已改为基于chapterContent+contentRange的计算属性(带范围钳制),公开读取 API 不变 - 三个构造点(
RDEPUBChapterLoader/RDEPUBTextBookBuilder/RDPlainTextBookBuilder)不再生成每页子串;消费点经计算属性透明按需构造 Equatable确认:content为派生值,由contentRange+chapterContent判等覆盖,语义不变
API 兼容性注意:
RDEPUBTextPage当前是public struct,content也是公开存储属性;如果 SDK 已存在外部接入方直接读取page.content,则不应直接移除该字段- 优先方案是保留
content对外访问语义,但将其改为按需构造的计算属性或受控访问器,避免对外 API 断裂 - 若确认当前版本尚无稳定外部依赖,才可以评估把
content从存储属性调整为内部实现细节 - 在实施前应补做一次全仓库与接入侧检索,确认是否有外部调用依赖
page.content的“可变存储”特性
验收:打开 3 章窗口后常驻内存下降约"一整章文本规模 × 窗口章数";封面页显示正常;非 DTCoreText 回退路径回归通过;--demo-pagination-validate 命中数不回升。
P0-2 增加内存打点
- 已新增
RDEPUBMemoryProbe(--demo-memory-probe启动参数开启,输出 phys_footprint);挂载点:RDEPUBChapterRuntimeStore.insertChapter、pageNum回调每 20 次翻页、invalidateAllForSettingsChange、viewWillTransition转屏完成 - 用
--demo-memory-probe运行 Demo 即可采集对比基线
P0-3 保持现有缓存限制,防止错误复用
shouldAvoidReaderPageCaching(RDEPUBTextContentView.swift:319)保持现状,已补注释说明放开的前置条件是 P1-1 落地- 本阶段未改
RDReaderPreloadController行为
P1:第二波(P1-1 与 P1-2 必须同批落地)
P1-1 章节级共享 displayContent
现状:RDEPUBTextContentView.configure 每次创建整章可变副本(RDEPUBTextContentView.swift:443)并按页写入三类属性:页范围主题色(453)、页范围暗黑图附件替换(448)、按页裁剪的高亮属性(459 applyHighlightsToContent)。
约束:共享字符串一旦被任一页的 DTCoreTextLayouter/framesetter 持有,就不得再修改——所有属性必须在共享内容构建时一次性注入,或转为 overlay(见 P1-2)。
任务:
- 已新建
RDEPUBChapterDisplayContentCache(LRU 容量 2,主线程限定,RDEPUBReaderController持有):content+layouter+signature - 构建共享内容时一次性注入:全章范围主题色;高亮/下划线因 P1-2 转 overlay,不进共享内容也不进签名
- 暗黑图策略选 a:构建时全章一次性替换,复用
RDEPUBDarkImageAdjuster的 NSCache RDEPUBTextContentView.configure已改为接收displayCache参数并引用共享 entry,整章拷贝已删除- 签名 = 章节内容对象标识 + 长度 + 主题双色 + 暗黑图配置;设置/主题变化经签名自动失效重建(高亮变化不触发重建,仅重建 overlay 装饰)
- 控制器在
didReceiveMemoryWarning清空 display cache;设置变更重建 chapterContent 实例后由签名兜底换代
验收:连续翻页不再出现整章拷贝(打点确认);翻页延迟下降;高亮显示、高亮点击菜单、underline、搜索 currentMatch、附件点击、选区拖拽全部回归通过。
2026-07-08 回归记录:高亮/批注/选区/翻页/设置 13 项 UI 用例全过;SearchTests 10 项失败,但已在会话前提交
5a41066与 P0 提交e4e629a上复现同样失败(搜索 0 命中),确认为先于本优化存在的独立回归(最后一次已知通过是 2026-06-08 全量跑),需单独排查,与 P1 改动无关。
P1-2 高亮/下划线剥离为 overlay(与 P1-1 绑定)
现状:搜索高亮已走 overlay(RDEPUBTextContentView.swift:759 处 buildDecorations 传 highlights: [],注释明确为避免双画);普通高亮/下划线仍靠写 kRDEPUBHighlightAttributeName / kRDEPUBUnderlineAttributeName 属性由 render view 绘制。
任务:
buildDecorations已传入真实 highlights,高亮进背景层(文字下方)、下划线进前景层applyHighlightsToContent与 render view 的drawHighlights/computeHighlightRects/attributedDisplayContent已整体移除,无双画- 高亮命中与菜单锚点继续走
interactionController.selectionRects,坐标保持 chapter-absolute(未改动)
验收:高亮/下划线视觉与改前一致(含跨页高亮的页内裁剪);高亮点击弹菜单正常;与搜索高亮叠加时无双画。
P1-3 章节级共享 display layouter
现状:每个页面视图各建一个 DTCoreTextLayouter(RDEPUBTextContentView.swift:465);章节运行时已有分页用 RDEPUBTextLayouter(RDEPUBRuntimeChapter.layouter),display layouter 向同一模式靠拢。
任务:
DTCoreTextLayouter已随共享内容放入 display cache,与 signature 同生命周期- 页面按
bounds+page.contentRange取layoutFrame;shouldCacheLayoutFrames = false保持,layoutFrame 按页新建(VerticalJustifier 的修改不会污染共享对象) - framesetter 只依赖字符串、与 bounds 无关,bounds 变化无需失效共享对象;layoutFrame 随页面视图释放
RDEPUBTextPageBoundaryValidator校验路径不变
验收:翻页时不再重建 framesetter(打点确认);横竖屏切换后布局正确;--demo-pagination-validate 不回升。
P1-4 评估重新放开文本页预加载缓存
前置:P1-1~P1-3 全部落地后,页面视图不再持有整章副本。
- 调整
shouldAvoidReaderPageCaching判定,允许轻量化后的文本页进入RDReaderPreloadController缓存 - 对比放开前后的翻页流畅度与峰值内存,数据不佳则回退此项
P2:视 P0/P1 打点数据决定
P2-1 章节级 LRU 分层淘汰
现状:RDEPUBChapterRuntimeStore 已有邻接窗口(windowSpineIndices,radius 1)、evictableSpineIndices / evict / evictAllExceptCurrent / handleMemoryWarning / invalidateAllForSettingsChange;磁盘侧有 RDEPUBChapterSummaryDiskCache(分页结果 + cfiMap + metadata)。当前邻接章节仍持有完整 RDEPUBRuntimeChapter(typesetString + pages + layouter)。
任务:
- 邻接章节降级:保留 page map / chapter summary / 分页结果,释放
typesetAttributedString、display content、layouter - 进入邻接章节时按 summary 重建,测量重建延迟;延迟不可接受则维持现状
- 远距章节仅保留磁盘缓存 + 轻量 metadata(现有 evict 已覆盖,确认即可)
P2-2 图片附件深化
现状:暗黑图已走 NSCache(50 MB / 100 张,RDEPUBDarkImageAdjuster.swift:9)且仅处理当前页范围;章节运行时另有 100 MB imageCache。
任务:
- 大图按显示尺寸下采样后再参与 attachment
- 页面离屏后释放 attachment 强引用的已解码大图;避免共享 content 长期持有
- 回归:图片点击查看、暗黑模式切换、附件位置与命中
P3:仅在 P0–P2 之后数据仍不达标时
- 上下文窗口化布局(当前页所在段落块 + 前后足以影响断行的文本窗口)。风险最高:易重新引入分页/显示边界不一致(CTTypesetter 断行上下文敏感,为已知已解 bug 的根因),选区/搜索/高亮/附件索引需全部重验。默认不做。
明确不做
- 回退"页内子串重新布局"(重新引入显示不全/页末孤字)
- 取消 chapter-absolute 索引(破坏点击/选区/高亮定位一致性)
- 先改分页器(正确性与性能问题混杂,无法回归)
- 未落地 P1 前放开文本页 reader 级缓存(会驻留多份整章副本)
各阶段统一验收指标
正确性:--demo-pagination-validate 命中数不回升;长章节页末无孤字/缺字;高亮显示、搜索高亮、附件点击、选区拖拽行为一致。
内存(对比 P0-2 基线):打开长章节后常驻;连续翻页 20 页峰值;设置变更前后峰值;横竖屏切换峰值。
性能:首次打开章节耗时;连续翻页帧稳定性;设置切换恢复时间。