ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBBackgroundPriorityPolicy.swift
shenlei 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
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
}