Phase 1: 稳定性优先 - 新增 RDEPUBJumpSession 保护机制,防止远距跳转后翻页串章 - 升级页图接管条件,增加 JumpSession 保护区检查 - 窗口扩展改为基于当前权威窗口方向 Phase 2: 补全优先级重排 - 新增 RDEPUBBackgroundPriorityPolicy 策略配置 - 实现 hot/warm/cold zone 优先级排序 - 添加失败重试机制(指数退避,最多3次) Phase 3: 分段覆盖与最终收敛 - 新增 RDEPUBBackgroundCoverageStore 分段存储 - 新增 RDEPUBPageMapReconciliationCoordinator 页图接管仲裁 - 实现 LRU 淘汰和内存警告处理 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
29 KiB
大书远距目录跳转与后台补全优化方案
最后更新:2026-06-15 适用范围:
RDEPUBReaderPaginationCoordinator、RDEPUBReaderRuntime、RDEPUBReaderLocationCoordinator、RDEPUBReaderController+DataSource问题背景:大书按需分页模式下,从较早章节远距跳转到未解析章节时,虽然首跳章节可对齐,但后续翻页仍可能回落到错误章节窗口。
1. 文档目标
本文给出“大书远距目录跳转与后台补全”的完整优化方案,目标不是只修一次目录跳转,而是统一以下三件事:
- 远距目录跳转时,前台必须优先保证目标章节可读。
- 跳转后的连续翻页必须保持章节窗口连续,不被旧的后台页图覆盖。
- 后台补全必须围绕当前阅读区段优先补洞,而不是只按全书顺序推进。
本文基于当前仓库代码整理,不以旧文档描述为准。
2. 当前实现概览
当前大书模式由两条并行路径组成:
- 前台按需窗口
- 后台全书元数据补全
对应代码主入口:
- 前台快速打开与局部窗口建立: RDEPUBReaderPaginationCoordinator.swift
- 后台元数据补全: RDEPUBReaderPaginationCoordinator.swift
- 目录跳转前的目标章节准备: RDEPUBReaderRuntime.swift
- 阅读位置恢复: RDEPUBReaderLocationCoordinator.swift
- 翻页后的状态同步: RDEPUBReaderController+DataSource.swift
当前设计的优点:
- 首开速度快,用户不必等待全书解析完成。
- 单章内容和轻量页图是解耦的,内存开销较低。
- 目录跳转前可以临时重建目标章节附近的小窗口,已经能保证“首跳章节正确”。
当前设计的核心缺口:
- 后台补全仍然以“全书 buildable spine 顺序推进”为主,没有围绕当前阅读区段动态重排。
pendingFullPageMap是单一全局候选页图,不区分“当前阅读段”和“远处阅读段”。- 局部窗口和后台全局页图之间缺少稳定的切换协议,容易在跳转后下一次翻页时重新落回旧窗口。
3. 当前问题复盘
以“当前解析到第 10 章,用户从目录跳到第 1000 章”为例。
3.1 当前实际行为
- 前台当前持有的是“第 10 章附近”的局部
bookPageMap。 - 用户点击目录项后,系统会先通过
ensureOnDemandNavigationTargetAvailable(for:)临时重建“第 1000 章附近”的局部窗口。 - 因此首跳可以落到正确章节。
- 但后台的
pendingFullPageMap可能仍然主要覆盖第 10 章附近,或者仅覆盖一部分全书章节。 - 用户开始翻页时,如果前台再次接纳这份不适合当前区段的全局候选页图,就可能出现:
- 当前页仍在 1000 章附近
- 但后续页码解析和扩窗语义重新基于旧区段
- 导致下一章或后几页回到错误章节
3.2 问题本质
不是“目录跳转没有解析对”,而是:
- 前台阅读权威窗口缺少短时锁定机制。
- 后台补全结果是单一全局页图,接管粒度过粗。
- 后台任务优先级不跟随阅读位置变化而重排。
- 多次快速跳转、内存压力和异常任务恢复的边界条件尚未定义清楚。
4. 设计目标
优化方案需要同时满足以下目标。
4.1 功能目标
- 从任意章节远距跳转到未解析章节时,首跳章节必须正确。
- 跳转后的连续翻页必须沿目标章节窗口连续推进。
- 后台补全完成后,最终仍要收敛到完整全书页图。
- 多次快速跳转、边界章节跳转和后台异常恢复都必须可预测。
4.2 性能目标
- 不回退到“跳一次目录就全书同步解析”。
- 继续复用
RDEPUBChapterSummaryDiskCache和单章同步加载能力。 - 将前台阻塞和热区补全时延量化。
建议验收阈值:
- 远距目录跳转前台阻塞:
- P50
< 120ms - P95
< 250ms - 单次最长
< 400ms
- P50
- 跳转后首屏可读时间:
- P50
< 500ms - P95
< 1200ms
- P50
- 跳转后
hot zone80% 覆盖时间:< 3s
- 冷区公平性:
- 每
30s至少有一次 cold zone 进展
- 每
4.3 架构目标
- 当前阅读窗口与后台全书补全分层。
- 页图接管必须有明确准入条件。
- 后台解析顺序能够围绕用户当前位置动态重排。
- 后台 segment 和页图升级必须具备内存预算和降级策略。
5. 总体方案
建议将现有“大书按需分页”演进为三层模型:
Active Reading WindowSegmented Background CoverageFull Book Convergence
5.1 Active Reading Window
这是前台唯一权威来源。
职责:
- 决定当前页码、当前章节、后续翻页所依赖的真实窗口。
- 由当前阅读位置驱动扩窗。
- 在短时间内拒绝不安全的全局候选页图覆盖。
可继续复用现有 bookPageMap + chapterRuntimeStore,但语义上需要明确:
- 当前
context.bookPageMap不再被视为“全书候选结果”,而是“当前权威阅读窗口”。 - 所有翻页、目录跳转、当前页位置解析都只信任它。
5.2 Segmented Background Coverage
后台不再只维护一个“越来越完整的全书候选图”,而是维护多段覆盖信息。
建议新增概念:
struct RDEPUBBackgroundCoverageSegment {
let lowerSpineIndex: Int
let upperSpineIndex: Int
let pageMap: RDEPUBBookPageMap
let resolvedSpineIndices: Set<Int>
let generatedAt: CFAbsoluteTime
let renderSignature: String
let estimatedMemoryBytes: Int
}
职责:
- 表示某一段章节范围已经具备稳定的页码覆盖。
- 可以被前台按需合并或替换。
- 不必要求一开始就成为完整全书页图。
5.3 Full Book Convergence
最终后台仍会生成一份全书完整覆盖,但它只作为最终收敛目标,不再是每次前台导航时都想接管的唯一对象。
建议将现有:
pendingFullPageMap
演进为:
pendingCoverageSegmentspendingCompletePageMap
其中:
pendingCoverageSegments用于当前阅读区段附近的安全升级。pendingCompletePageMap只在完整覆盖达成时才应用。
5.4 renderSignature 定义
文中涉及的 renderSignature 用于判断两个 segment 或完整页图是否属于同一分页语义版本。
建议构成如下:
// renderSignature 由以下因素拼接后再 hash:
// 1. 排版参数:字体、字号、行高倍数、段间距、列数、列间距、内容 inset
// 2. 视口参数:pageSize、safeAreaInsets、单双页模式、fixed/reflowable 展示参数
// 3. 算法版本:分页算法版本号、chapter summary schemaVersion
// 4. 阅读器关键策略:widow/orphan 避让开关、文本渲染引擎选择
约束:
renderSignature相同,才允许 segment 合并或后台页图接管前台。renderSignature不同,一律视为不同分页版本:- 不合并
- 不复用旧页图进行前台接管
- 仅允许单章摘要作为重新构建输入
6. 关键策略
6.1 策略一:目录远跳后建立 Jump Session
目标
目录从第 10 章跳到第 1000 章后,前台必须进入一个短期“跳转会话”,在此期间当前阅读窗口拥有最高优先级。
建议新增模型
struct RDEPUBJumpSession {
let anchorSpineIndex: Int
let createdAt: CFAbsoluteTime
let protectedSpineIndices: Set<Int>
let sequenceNumber: Int
let expiresAt: CFAbsoluteTime
let reason: Reason
enum Reason {
case tableOfContentsJump
case bookmarkJump
case searchJump
}
}
行为规则
- 目录跳转成功后创建
JumpSession。 protectedSpineIndices至少覆盖:- 目标章节
- 当前窗口前后相邻章节
- 在
JumpSession结束前:- 不允许任何不覆盖
protectedSpineIndices的后台页图接管前台。
- 不允许任何不覆盖
- 每次新的远距跳转都会替换旧的
JumpSession,不允许多个 Session 并存。
生命周期定义
JumpSession 结束条件建议精确定义为以下四类:
coverage-complete- 候选页图已完整覆盖
protectedSpineIndices - 且当前页在新页图中可无损重定位
- 候选页图已完整覆盖
navigated-away- 用户连续沿同一方向翻页超过
jumpSessionExitPageThreshold - 且当前
spineIndex已不在protectedSpineIndices
- 用户连续沿同一方向翻页超过
timeout- 跳转后超过
jumpSessionTimeout - 且最近
idle持续时间大于jumpSessionIdleGracePeriod
- 跳转后超过
superseded- 用户再次执行远距跳转
- 新 Session 直接替换旧 Session
默认建议值:
jumpSessionExitPageThreshold = 6jumpSessionTimeout = 20sjumpSessionIdleGracePeriod = 1.5sprotectedNeighborRadius = 1
说明:
- “连续翻页”按同一方向的成功页变更次数计数,不按手势次数计数。
- 若用户在保护区内前后来回翻页,则方向计数重置,Session 不结束。
- 若用户离开保护区后又回到保护区,不恢复旧 Session,而是按当前阅读位置重新判断。
配置化建议
建议在 RDEPUBReaderConfiguration 中新增:
struct RDEPUBJumpSessionPolicy {
let exitPageThreshold: Int
let timeout: TimeInterval
let idleGracePeriod: TimeInterval
let protectedNeighborRadius: Int
}
修改位置
RDEPUBReaderRuntimeRDEPUBReaderLocationCoordinatorRDEPUBReaderController+DataSource
6.2 策略二:后台补全改为“围绕当前阅读区段优先补洞”
当前问题
现有 paginateMetadataOnly(...) 解析顺序本质上由:
allBuildableIndicesuncachedSpineIndices
决定,仍偏向全书静态顺序,而不是围绕当前阅读区段动态调整。
新策略
后台补全改为三段优先级:
hot zonewarm zonecold zone
定义建议:
hot zone:当前章节附近,例如currentSpine ± hotRadiuswarm zone:最近一次远距跳转区段,例如jumpAnchor ± warmRadiuscold zone:其余全书未补全章节
解析顺序改为:
- 先补
hot zone - 再补
warm zone - 最后扫
cold zone
Zone 参数来源与配置化
不建议把 32、128 写死。建议引入策略对象:
struct RDEPUBBackgroundPriorityPolicy {
let hotRadius: Int
let warmRadius: Int
let maxWarmJumpAnchors: Int
let coldLaneShare: Double
}
推荐默认值:
- 小书:
totalBuildableChapters < 300hotRadius = 12warmRadius = 32
- 中书:
300...1200hotRadius = 24warmRadius = 96
- 超大书:
> 1200hotRadius = 32warmRadius = 160
公式建议:
hotRadius = min(max(12, Int(sqrt(Double(totalBuildableChapters)))), 48)warmRadius = min(max(hotRadius * 3, 32), 192)
说明:
hotRadius关注当前连续翻页稳定性,应较小且聚焦。warmRadius关注远距跳转后的邻近补洞,可明显大于hotRadius。coldLaneShare用于避免后台永远只补热区,建议默认0.15。
coldLaneShare 调度规则
coldLaneShare 建议按“任务数量比例”而不是“CPU 时间比例”实现,便于控制和验证。
建议规则:
- 每一轮调度按批次生成 work items。
- 设本轮总计划任务数为
batchSize,则:coldCount = max(1, Int(round(Double(batchSize) * coldLaneShare)))- 剩余任务由
hot/warm按优先级占用
- 冷区采用轮询推进:
- 维护
coldCursor - 每轮从
coldCursor开始扫描下一个未解析冷区章节 - 批次结束后更新
coldCursor
- 维护
- 若当前没有可用 cold item:
- 额度让渡给
warmSecondary - 不阻塞热区执行
- 额度让渡给
这样可以保证:
- 热区始终优先
- 冷区不会永久饿死
- 调度行为可通过日志和测试稳定验证
Zone 重叠与多次跳转规则
若多次跳转导致多个 warm zone 重叠,建议规则如下:
hot zone永远优先级最高。warm zone只保留最近maxWarmJumpAnchors个跳转锚点,默认2。- 若多个 warm zone 重叠:
- 先按最近跳转时间排序
- 再按与当前
spineIndex距离排序
- 某章节若同时属于
hot与warm,只按hot处理一次。
修改建议
把:
uncachedSpineIndices = allBuildableIndices.filter { ... }
改造成:
let prioritizedSpineIndices = makeMetadataPriorityOrder(
allBuildableIndices: allBuildableIndices,
currentSpineIndex: currentVisibleSpineIndex,
warmJumpAnchors: recentJumpAnchors,
cachedSpineIndices: cachedSpineIndices,
policy: backgroundPriorityPolicy
)
新增方法建议:
makeMetadataPriorityOrder(...)currentVisibleSpineIndexForBackgroundTasks()
6.3 策略三:多次快速跳转与后台任务重排
目标
解决用户在后台补全过程中快速多次跳转时的状态冲突问题。
行为规则
- 只允许一个
active JumpSession。 - 每次新的远距跳转都会:
- 生成新的
sequenceNumber - 替换旧
JumpSession - 更新当前
jumpAnchor
- 生成新的
- 后台任务不要求“硬取消”已经进入单章构建中的工作单元,但要求:
- 未开始的任务必须支持重排
- 已完成但低优先级的结果只进入
backgroundCoverageStore,不得直接接管前台
warm zone默认只保留最近 2 次远距跳转锚点:- 最近一次为
primary warm anchor - 上一次为
secondary warm anchor - 更早锚点降级为
cold
- 最近一次为
队列策略
建议将后台元数据任务拆为可重排 work item:
struct RDEPUBMetadataParseWorkItem {
let spineIndex: Int
let generation: Int
let priorityBand: PriorityBand
enum PriorityBand {
case hot
case warmPrimary
case warmSecondary
case cold
}
}
规则:
- 新跳转产生新
generation。 - 调度器优先消费当前 generation 的
hot/warm。 - 旧 generation 未执行的冷任务可直接丢弃并重建。
- 旧 generation 已完成的结果仍可入库,但只能作为缓存命中来源,不能直接提升为前台页图。
失败重试策略
后台单章元数据任务失败后,建议保留有限重试能力:
- 最大重试次数:
maxRetryCount = 3
- 重试间隔:
- 指数退避:
0.5s -> 2s -> 8s
- 指数退避:
- 重试优先级:
hot失败后保持在hotwarm失败后保留原 bandcold失败后仍在cold
- 超过最大重试次数后:
- 标记为
deferredFailure - 不阻断其他章节解析
- 等用户再次进入相关区段时允许手动提升并重新尝试
- 标记为
6.4 策略四:页图接管从“整图替换”改为“分段升级”
当前问题
当前前台页图升级主要依赖:
refreshBookPageMapInPlace(_:)applyPendingFullPageMapIfNeeded()
这意味着升级动作仍偏向“整图替换”。
新策略
前台权威窗口只接受两类升级:
- 当前窗口连续扩张
- 当前窗口所在区段被更完整的覆盖段替换
不接受:
- 与当前窗口不连续的全局候选图直接替换
- 只覆盖当前章但不覆盖前后相邻章的页图接管
建议新增准入条件
对任意候选页图或 segment,接管前至少满足以下条件:
- 覆盖当前
currentSpineIndex - 覆盖
currentSpineIndex - 1与currentSpineIndex + 1,若这些章节是 buildable - 覆盖当前
JumpSession.protectedSpineIndices,若 Session 存在 - 当前页所在章节的本地页码边界与前台窗口一致,或可安全重定位
边界章节规则
对于第一页或最后一章,接管准入条件中的相邻章节检查需要收口:
- 若
currentSpineIndex == 0- 只要求覆盖
currentSpineIndex与currentSpineIndex + 1
- 只要求覆盖
- 若
currentSpineIndex == lastBuildableSpineIndex- 只要求覆盖
currentSpineIndex与currentSpineIndex - 1
- 只要求覆盖
- 若前后相邻章节存在但不是 buildable text spine
- 不将其纳入强制接管条件
- 若当前
JumpSession.protectedSpineIndices本身已覆盖更严格范围- 以
protectedSpineIndices为准
- 以
接管体验约束
- 页图接管默认不做显式动画,避免视觉跳闪。
- 若接管导致当前页号变化但内容位置不变,不展示 toast。
- 若接管后当前页需要重定位,优先无动画
transitionToPage。
修改位置
RDEPUBReaderRuntime.applyPendingFullPageMapIfNeeded()
6.5 策略五:局部窗口扩展必须以当前窗口为基准
当前问题
目录跳转时虽然可以临时重建第 1000 章附近窗口,但后续扩窗仍可能受到旧状态影响。
新策略
扩窗必须以“当前权威窗口”作为唯一基准,规则如下:
- 若用户向后翻页,优先向后扩展窗口。
- 若用户向前翻页,优先向前扩展窗口。
- 扩窗时保留当前窗口已有章节,不重新回退到旧窗口。
建议实现
为 bookPageMap 增加窗口语义:
struct RDEPUBActiveWindowDescriptor {
let lowerSpineIndex: Int
let upperSpineIndex: Int
let anchorSpineIndex: Int
}
在扩窗时:
- 根据当前方向选择新增章节集合。
- 只 append / prepend 缺失章节。
- 重建后的新 map 必须保留当前窗口已有顺序与绝对页号连续性。
6.6 策略六:后台解析结果与前台窗口彻底解耦
建议明确两类状态:
activeWindowPageMapbackgroundCoverageStore
其中:
activeWindowPageMap只服务前台阅读backgroundCoverageStore只积累后台成果
前台永远不直接消费“后台刚生成的任意结果”,而是通过一个仲裁器判断是否可以吸收。
建议新增协调器:
final class RDEPUBPageMapReconciliationCoordinator
职责:
- 判断某个后台 segment 是否可以并入当前窗口
- 判断完整页图是否满足接管条件
- 输出“保持当前窗口 / 扩窗 / 分段替换 / 全量替换”的决策
Segment 合并策略
当两个 segment 重叠时:
- 若
renderSignature相同:- 对重叠章节优先保留更新时间更近的覆盖
- 若
renderSignature不同:- 不合并,视为不同版本,旧版本淘汰
- 若合并后章节数超过
maxChaptersPerSegment:- 按当前阅读位置切成两个 segment
6.7 策略七:Segment 内存管理与降级策略
当前风险
引入 backgroundCoverageStore 后,若没有明确内存策略,大书可能累积过多 segment。
建议约束
- segment 数量上限:
- 默认
maxResidentSegments = 8
- 默认
- 单 segment 覆盖上限:
- 默认不超过
256个章节
- 默认不超过
- 总覆盖内存预算:
- 默认
8MB以内,仅针对页图与摘要聚合对象,不含单章NSAttributedString
- 默认
淘汰策略
按以下顺序淘汰:
- 不覆盖
activeWindow - 不覆盖
active JumpSession - 最久未命中
- 与当前阅读位置距离最远
建议新增:
struct RDEPUBBackgroundCoverageStorePolicy {
let maxResidentSegments: Int
let maxChaptersPerSegment: Int
let memoryBudgetBytes: Int
}
降级策略
在内存警告或预算超限时:
- 立即清空最远冷区 segment
- 保留:
activeWindowJumpSession保护区- 最近一次 warm segment
- 若仍超限:
- 停止创建新 segment
- 后台只继续写单章摘要缓存,不再持有内存聚合页图
estimatedMemoryBytes 估算方式
estimatedMemoryBytes 不要求做到对象级精确统计,但必须有稳定近似公式,建议基于页图结构体规模估算:
// 估算思路:
// bytes ≈ baseSegmentOverhead
// + entryCount * avgEntryCost
// + resolvedSpineCount * avgSetEntryCost
建议默认参数:
baseSegmentOverhead = 256BavgEntryCost = 96BavgSetEntryCost = 16B
即:
estimatedMemoryBytes =
256 +
pageMap.entries.count * 96 +
resolvedSpineIndices.count * 16
说明:
- 这里只估算
pageMap与 segment 元数据,不包含单章NSAttributedString。 - 该估算用于淘汰排序和预算闸门,不用于精确 profiling。
- 若后续 profiling 显示偏差明显,可统一调整常量,不影响外部接口。
与现有缓存的关系
磁盘层仍以单章摘要为准:
RDEPUBChapterSummaryDiskCache不变- segment 只作为内存聚合态
- 任何 segment 都必须可由单章摘要重新构建
7. 目标架构
flowchart LR
A["目录跳转 / 书签跳转 / 搜索跳转"] --> B["Jump Session"]
B --> C["Active Reading Window"]
C --> D["当前翻页 / 位置恢复 / 章节准备"]
E["后台元数据解析"] --> F["Background Coverage Store"]
F --> G["PageMap Reconciliation Coordinator"]
C --> G
B --> G
G --> H["允许扩窗或分段升级"]
H --> C
F --> I["完整全书页图"]
I --> G
这个架构下:
- 当前阅读窗口始终是前台权威。
- 后台解析只提供“可被采纳的覆盖能力”。
- 页图接管由显式仲裁决定,而不是隐式替换。
8. 分阶段实施计划
建议分三期落地,避免一次性大改。
Phase 1:稳定性优先
目标
先彻底消除“跳转对了,但后续翻页串章”。
范围
- 引入
JumpSession - 将
applyPendingFullPageMapIfNeeded()的接管条件升级为:- 覆盖当前保护区
- 覆盖当前相邻章节
extendPartialBookPageMapIfNeeded(...)只基于当前窗口连续扩张
不做
- 不引入完整 segment store
- 不修改磁盘缓存格式
验收
- 从第 10 章跳到第 1000 章,连续翻 20 页不串章。
- 跳到未解析章节后,前后翻页都维持在目标区段。
- 后台完整解析完成前,当前阅读窗口不被旧区段页图覆盖。
- 快速连续跳转时,旧 Session 被新 Session 替换,后续翻页只跟随最新跳转区段。
Phase 2:补全优先级重排
目标
让后台补全优先为当前阅读区段服务。
范围
paginateMetadataOnly(...)引入 hot/warm/cold 优先级- 支持阅读位置变化时动态重排后续任务
- 记录最近一次远距跳转锚点作为 warm zone
验收
- 目录跳到第 1000 章后,1000 附近章节在后台优先补齐。
- 继续翻页时,补全速度明显优于全书静态顺序。
- 多次快速跳转后,后台 work item 可以完成 generation 重排。
Phase 3:分段覆盖与最终收敛
目标
把后台全书补全从“单一全局候选图”升级为“分段覆盖 + 最终全图收敛”。
范围
- 新增
backgroundCoverageStore - 新增
RDEPUBPageMapReconciliationCoordinator - 引入 segment 级页图升级
验收
- 前台窗口升级不再依赖整图替换。
- 大书多次远距跳转后,后台补全仍能稳定收敛。
- 内存警告下可安全降级到“仅保留当前窗口 + 单章摘要缓存”。
9. 需要修改的核心文件
优先级从高到低如下:
Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderRuntime.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderPaginationCoordinator.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderLocationCoordinator.swiftSources/RDReaderView/EPUBUI/RDEPUBReaderController+DataSource.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBReaderContext.swift
建议新增文件:
Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBJumpSession.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBBackgroundCoverageStore.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBPageMapReconciliationCoordinator.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBBackgroundPriorityPolicy.swiftSources/RDReaderView/EPUBUI/ReaderController/RDEPUBMetadataParseWorkItem.swift
10. 风险与规避
风险 1:状态模型变复杂
如果一次性同时引入 Jump Session、Segment Store、Reconciliation Coordinator,短期理解成本会上升。
规避:
- 先做 Phase 1
- Phase 2、3 再逐步抽离结构
风险 2:页码重定位抖动
如果后台页图接管条件不严,当前页可能发生视觉跳动。
规避:
- 只有覆盖当前保护区才允许接管
- 接管时先保存当前位置,再重新解析页码
- 若新页码与当前页偏差超过阈值,延迟接管
风险 3:后台优先级重排引发任务饥饿
如果一直只优先当前阅读区,远处章节可能长期不补全。
规避:
- 设置 hot/warm/cold 权重,而不是永久只做 hot
- 每轮补全保留固定比例给 cold zone
风险 4:缓存一致性
若 segment 覆盖和完整页图并存,缓存语义容易混淆。
规避:
- 磁盘缓存仍保持“单章摘要”不变
- segment 与 full map 只作为内存态聚合结果
风险 5:多次跳转导致状态抖动
若每次跳转都完全重建后台优先级,可能导致系统一直处于重排态。
规避:
- 只保留最近 2 次 warm anchor
- 已进入执行的单章任务不强制中断
- 重排只影响未执行任务和接管优先级
风险 6:内存预算判断失真
若 segment 内存估算过粗,可能造成过晚淘汰。
规避:
- 对 segment 记录近似字节数
- 在内存警告时无条件执行强制降级
- 将单章
NSAttributedString与 segment 聚合对象分开统计
11. 验收用例
至少补以下 UI 用例。
Case 1:远距目录跳转后连续向后翻页
- 首开大书进入第 10 章附近
- 目录跳到第 1000 章
- 连续向后翻 20 页
- 期望:章节持续落在 1000 章附近连续推进
Case 2:远距目录跳转后先向前再向后翻页
- 从第 10 章跳到第 1000 章
- 向前翻 5 页,再向后翻 10 页
- 期望:窗口连续,不回落到第 10 章区段
Case 3:跳转后后台完整补全完成
- 跳到远距章节
- 停留并等待后台补全完成
- 再继续翻页
- 期望:页图升级后位置连续,无跳章
Case 4:多次远距跳转
- 第 10 章跳到第 1000 章
- 再跳到第 300 章
- 再跳到第 1500 章
- 期望:每次都以新的阅读区段为权威窗口
Case 5:跳转到第一章与最后一章
- 从中间章节跳到第一章
- 再从第一章跳到最后一章
- 期望:边界章节不会因缺少相邻章节而错误结束 Session 或拒绝接管
Case 6:快速连续跳转
- 10 → 1000 → 300 → 1500,间隔小于 2 秒
- 期望:
- 只有最后一次跳转对应的 Session 生效
- 后续翻页围绕 1500 章附近展开
Case 7:后台任务失败或超时
- 人为制造部分章节 build 失败
- 期望:
- 前台当前窗口仍可继续翻页
- 失败章节保留重试机会
- 不因为单章失败导致全局页图回退
Case 8:内存告警
- 在大书多次跳转后触发内存警告
- 期望:
- 冷区 segment 被淘汰
- 当前窗口与 JumpSession 保护区保留
- 后续仍可继续阅读
Case 9:保护区内来回翻页
- 跳到第 1000 章后,在保护区内前后翻页 20 次
- 期望:
- Jump Session 不因方向切换误结束
- 页图不被不完整候选图接管
Case 10:热区补全时效
- 跳转到远距章节
- 记录
hot zone章节覆盖时间 - 期望:
- 80% 覆盖时间满足性能阈值
12. 补充实现细节
12.1 makeMetadataPriorityOrder(...) 排序算法
建议排序键按以下顺序构造:
priorityBandhotwarmPrimarywarmSecondarycold
distanceToCurrentSpinedistanceToNewestJumpAnchorspineIndex
可表达为:
items.sorted {
($0.bandRank, $0.distanceToCurrent, $0.distanceToNewestJump, $0.spineIndex) <
($1.bandRank, $1.distanceToCurrent, $1.distanceToNewestJump, $1.spineIndex)
}
cold 区内部建议轮询式推进,而不是每次都从书头开始。
12.2 Segment 合并策略
当两个 segment 重叠时:
- 若
renderSignature相同:- 对重叠章节优先保留更新时间更近的覆盖
- 若
renderSignature不同:- 不合并,视为不同版本,旧版本淘汰
- 若合并后章节数超过
maxChaptersPerSegment:- 按当前阅读位置切成两个 segment
12.3 用户感知体验
- 页图接管默认不做显式动画,避免“页码重新解释”造成视觉跳闪。
- 若后台补全正在推进,可选地暴露调试态指标,不建议默认面向用户展示进度 UI。
- 若发生降级或后台重排,不打断前台阅读。
13. 推荐实施结论
建议按以下顺序执行:
- 先做 Phase 1,稳定远距跳转后的连续翻页。
- 再做 Phase 2,让后台补全顺序围绕当前阅读区段优先推进。
- 最后做 Phase 3,把单一
pendingFullPageMap演进成“分段覆盖 + 最终全图收敛”。
最关键的设计原则只有一句话:
前台当前阅读窗口必须始终拥有解释权,后台补全只能在安全条件满足后逐步接管,不能反向覆盖当前阅读语义。