Files
ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBBackgroundPriorityPolicy.swift
T
shenleiandClaude Opus 4.7 c64460988a feat: 实现大书远距目录跳转与后台补全优化方案
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>
2026-06-15 16:28:11 +08:00

194 lines
5.7 KiB
Swift

import Foundation
/// 后台补全优先级策略
///
/// 控制后台元数据解析的优先级排序,确保当前阅读区段优先补全
struct RDEPUBBackgroundPriorityPolicy {
/// 热区半径:当前章节附近的范围
let hotRadius: Int
/// 温区半径:远距跳转锚点附近的范围
let warmRadius: Int
/// 最大温区跳转锚点数量
let maxWarmJumpAnchors: Int
/// 冷区份额:每轮补全中冷区任务的比例
let coldLaneShare: Double
/// 默认策略
static let `default` = RDEPUBBackgroundPriorityPolicy(
hotRadius: 24,
warmRadius: 96,
maxWarmJumpAnchors: 2,
coldLaneShare: 0.15
)
/// 根据总章节数动态计算策略
static func adaptive(totalBuildableChapters: Int) -> RDEPUBBackgroundPriorityPolicy {
let hotRadius = min(max(12, Int(sqrt(Double(totalBuildableChapters)))), 48)
let warmRadius = min(max(hotRadius * 3, 32), 192)
return RDEPUBBackgroundPriorityPolicy(
hotRadius: hotRadius,
warmRadius: warmRadius,
maxWarmJumpAnchors: 2,
coldLaneShare: 0.15
)
}
}
/// 优先级带
enum RDEPUBPriorityBand: Int, Comparable {
case hot = 0
case warmPrimary = 1
case warmSecondary = 2
case cold = 3
static func < (lhs: RDEPUBPriorityBand, rhs: RDEPUBPriorityBand) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
/// 温区跳转锚点
struct RDEPUBWarmJumpAnchor {
let spineIndex: Int
let timestamp: CFAbsoluteTime
let sequenceNumber: Int
}
/// 元数据解析工作项
struct RDEPUBMetadataParseWorkItem {
let spineIndex: Int
let generation: Int
let priorityBand: RDEPUBPriorityBand
/// 排序键
var sortKey: (bandRank: Int, distanceToCurrent: Int, distanceToNewestJump: Int, spineIndex: Int) {
(priorityBand.rawValue, 0, 0, spineIndex)
}
}
/// 后台补全优先级管理器
final class RDEPUBBackgroundPriorityManager {
private unowned let context: RDEPUBReaderContext
/// 当前策略
private(set) var policy: RDEPUBBackgroundPriorityPolicy
/// 温区跳转锚点列表(按时间倒序)
private var warmAnchors: [RDEPUBWarmJumpAnchor] = []
/// 当前 generation
private(set) var currentGeneration: Int = 0
/// 冷区游标
private var coldCursor: Int = 0
init(context: RDEPUBReaderContext) {
self.context = context
self.policy = .default
}
/// 更新策略
func updatePolicy(_ newPolicy: RDEPUBBackgroundPriorityPolicy) {
policy = newPolicy
}
/// 添加温区跳转锚点
func addWarmAnchor(spineIndex: Int) {
let anchor = RDEPUBWarmJumpAnchor(
spineIndex: spineIndex,
timestamp: CFAbsoluteTimeGetCurrent(),
sequenceNumber: currentGeneration
)
warmAnchors.insert(anchor, at: 0)
// 保留最近 N 个锚点
if warmAnchors.count > policy.maxWarmJumpAnchors {
warmAnchors = Array(warmAnchors.prefix(policy.maxWarmJumpAnchors))
}
currentGeneration += 1
coldCursor = 0 // 重置冷区游标
RDEPUBBackgroundTrace.log(
"PriorityManager",
"addWarmAnchor spine=\(spineIndex) generation=\(currentGeneration) anchors=\(warmAnchors.count)"
)
}
/// 生成优先级排序的 spineIndex 列表
func makeMetadataPriorityOrder(
allBuildableIndices: [Int],
currentSpineIndex: Int?,
cachedSpineIndices: Set<Int>
) -> [Int] {
let uncachedIndices = allBuildableIndices.filter { !cachedSpineIndices.contains($0) }
guard !uncachedIndices.isEmpty else { return [] }
// 为每个 spineIndex 计算优先级带
let items = uncachedIndices.map { spineIndex -> (spineIndex: Int, band: RDEPUBPriorityBand) in
let band = classifySpineIndex(
spineIndex: spineIndex,
currentSpineIndex: currentSpineIndex
)
return (spineIndex, band)
}
// 排序
let sorted = items.sorted { lhs, rhs in
// 先按优先级带排序
if lhs.band != rhs.band {
return lhs.band < rhs.band
}
// 同一带内按距离排序
let lhsDistanceToCurrent = currentSpineIndex.map { abs(lhs.spineIndex - $0) } ?? Int.max
let rhsDistanceToCurrent = currentSpineIndex.map { abs(rhs.spineIndex - $0) } ?? Int.max
if lhsDistanceToCurrent != rhsDistanceToCurrent {
return lhsDistanceToCurrent < rhsDistanceToCurrent
}
// 距离相同时按 spineIndex 排序
return lhs.spineIndex < rhs.spineIndex
}
return sorted.map { $0.spineIndex }
}
/// 分类 spineIndex 到优先级带
private func classifySpineIndex(
spineIndex: Int,
currentSpineIndex: Int?
) -> RDEPUBPriorityBand {
// 检查是否在热区
if let current = currentSpineIndex {
let distance = abs(spineIndex - current)
if distance <= policy.hotRadius {
return .hot
}
}
// 检查是否在温区
for (index, anchor) in warmAnchors.enumerated() {
let distance = abs(spineIndex - anchor.spineIndex)
if distance <= policy.warmRadius {
return index == 0 ? .warmPrimary : .warmSecondary
}
}
// 冷区
return .cold
}
/// 获取当前温区锚点(用于后台任务调度)
func currentWarmAnchors() -> [RDEPUBWarmJumpAnchor] {
warmAnchors
}
/// 重置状态
func reset() {
warmAnchors.removeAll()
currentGeneration = 0
coldCursor = 0
}
}