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>
308 lines
11 KiB
Swift
308 lines
11 KiB
Swift
import Foundation
|
||
|
||
/// 后台覆盖段:表示某一段章节范围的稳定页码覆盖
|
||
struct RDEPUBBackgroundCoverageSegment {
|
||
/// 段的下界 spineIndex
|
||
let lowerSpineIndex: Int
|
||
/// 段的上界 spineIndex
|
||
let upperSpineIndex: Int
|
||
/// 页图数据
|
||
let pageMap: RDEPUBBookPageMap
|
||
/// 已解析的 spineIndex 集合
|
||
let resolvedSpineIndices: Set<Int>
|
||
/// 创建时间
|
||
let generatedAt: CFAbsoluteTime
|
||
/// 渲染签名
|
||
let renderSignature: String
|
||
/// 预估内存占用(字节)
|
||
let estimatedMemoryBytes: Int
|
||
|
||
/// 是否包含指定 spineIndex
|
||
func contains(spineIndex: Int) -> Bool {
|
||
spineIndex >= lowerSpineIndex && spineIndex <= upperSpineIndex
|
||
}
|
||
|
||
/// 与指定 spineIndex 的距离
|
||
func distance(to spineIndex: Int) -> Int {
|
||
if contains(spineIndex: spineIndex) { return 0 }
|
||
return min(abs(spineIndex - lowerSpineIndex), abs(spineIndex - upperSpineIndex))
|
||
}
|
||
}
|
||
|
||
/// 覆盖存储策略
|
||
struct RDEPUBBackgroundCoverageStorePolicy {
|
||
/// 最大常驻段数
|
||
let maxResidentSegments: Int
|
||
/// 单段最大章节数
|
||
let maxChaptersPerSegment: Int
|
||
/// 内存预算(字节)
|
||
let memoryBudgetBytes: Int
|
||
|
||
/// 默认策略
|
||
static let `default` = RDEPUBBackgroundCoverageStorePolicy(
|
||
maxResidentSegments: 8,
|
||
maxChaptersPerSegment: 256,
|
||
memoryBudgetBytes: 8 * 1024 * 1024 // 8MB
|
||
)
|
||
}
|
||
|
||
/// 后台覆盖存储管理器
|
||
final class RDEPUBBackgroundCoverageStore {
|
||
private unowned let context: RDEPUBReaderContext
|
||
|
||
/// 存储策略
|
||
private let policy: RDEPUBBackgroundCoverageStorePolicy
|
||
|
||
/// 存储的段列表
|
||
private var segments: [RDEPUBBackgroundCoverageSegment] = []
|
||
|
||
/// 当前总内存占用
|
||
private var currentMemoryBytes: Int = 0
|
||
|
||
/// 最后访问时间(用于 LRU 淘汰)
|
||
private var lastAccessTime: [Int: CFAbsoluteTime] = [:]
|
||
|
||
init(context: RDEPUBReaderContext, policy: RDEPUBBackgroundCoverageStorePolicy = .default) {
|
||
self.context = context
|
||
self.policy = policy
|
||
}
|
||
|
||
/// 添加段
|
||
func addSegment(_ segment: RDEPUBBackgroundCoverageSegment) {
|
||
// 检查是否需要淘汰
|
||
evictIfNeeded(forNewSegment: segment)
|
||
|
||
// 检查是否与现有段重叠,合并或替换
|
||
var merged = false
|
||
for (index, existing) in segments.enumerated() {
|
||
if canMerge(existing, segment) {
|
||
if let mergedSegment = mergeSegments(existing, segment) {
|
||
segments[index] = mergedSegment
|
||
currentMemoryBytes = currentMemoryBytes - existing.estimatedMemoryBytes + mergedSegment.estimatedMemoryBytes
|
||
merged = true
|
||
RDEPUBBackgroundTrace.log(
|
||
"CoverageStore",
|
||
"merged segments: \(existing.lowerSpineIndex)-\(existing.upperSpineIndex) + \(segment.lowerSpineIndex)-\(segment.upperSpineIndex)"
|
||
)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if !merged {
|
||
segments.append(segment)
|
||
currentMemoryBytes += segment.estimatedMemoryBytes
|
||
}
|
||
|
||
// 更新访问时间
|
||
lastAccessTime[segment.lowerSpineIndex] = CFAbsoluteTimeGetCurrent()
|
||
|
||
RDEPUBBackgroundTrace.log(
|
||
"CoverageStore",
|
||
"added segment \(segment.lowerSpineIndex)-\(segment.upperSpineIndex) total=\(segments.count) memory=\(currentMemoryBytes)B"
|
||
)
|
||
}
|
||
|
||
/// 查找覆盖指定 spineIndex 的段
|
||
func findSegment(containing spineIndex: Int) -> RDEPUBBackgroundCoverageSegment? {
|
||
let segment = segments.first { $0.contains(spineIndex: spineIndex) }
|
||
if let segment {
|
||
lastAccessTime[segment.lowerSpineIndex] = CFAbsoluteTimeGetCurrent()
|
||
}
|
||
return segment
|
||
}
|
||
|
||
/// 查找覆盖指定 spineIndex 集合的段
|
||
func findSegment(covering spineIndices: Set<Int>) -> RDEPUBBackgroundCoverageSegment? {
|
||
let segment = segments.first { segment in
|
||
spineIndices.allSatisfy { segment.contains(spineIndex: $0) }
|
||
}
|
||
if let segment {
|
||
lastAccessTime[segment.lowerSpineIndex] = CFAbsoluteTimeGetCurrent()
|
||
}
|
||
return segment
|
||
}
|
||
|
||
/// 获取所有段
|
||
func allSegments() -> [RDEPUBBackgroundCoverageSegment] {
|
||
segments
|
||
}
|
||
|
||
/// 清除所有段
|
||
func clearAll() {
|
||
segments.removeAll()
|
||
currentMemoryBytes = 0
|
||
lastAccessTime.removeAll()
|
||
}
|
||
|
||
/// 清除冷区段(不覆盖当前阅读位置和保护区的段)
|
||
func clearColdSegments(
|
||
activeWindowSpineIndices: Set<Int>,
|
||
protectedSpineIndices: Set<Int>
|
||
) {
|
||
let keepIndices = activeWindowSpineIndices.union(protectedSpineIndices)
|
||
segments.removeAll { segment in
|
||
let isCold = !segment.resolvedSpineIndices.contains(where: { keepIndices.contains($0) })
|
||
if isCold {
|
||
currentMemoryBytes -= segment.estimatedMemoryBytes
|
||
lastAccessTime.removeValue(forKey: segment.lowerSpineIndex)
|
||
}
|
||
return isCold
|
||
}
|
||
}
|
||
|
||
/// 处理内存警告
|
||
func handleMemoryWarning(
|
||
activeWindowSpineIndices: Set<Int>,
|
||
protectedSpineIndices: Set<Int>
|
||
) {
|
||
RDEPUBBackgroundTrace.log(
|
||
"CoverageStore",
|
||
"memory warning: clearing cold segments, current=\(currentMemoryBytes)B"
|
||
)
|
||
|
||
// 第一步:清除冷区段
|
||
clearColdSegments(
|
||
activeWindowSpineIndices: activeWindowSpineIndices,
|
||
protectedSpineIndices: protectedSpineIndices
|
||
)
|
||
|
||
// 如果仍然超限,清除更多段
|
||
if currentMemoryBytes > policy.memoryBudgetBytes {
|
||
// 按距离排序,清除最远的段
|
||
let sorted = segments.sorted { lhs, rhs in
|
||
let lhsDistance = lhs.resolvedSpineIndices.map { idx in
|
||
activeWindowSpineIndices.map { abs(idx - $0) }.min() ?? Int.max
|
||
}.min() ?? Int.max
|
||
let rhsDistance = rhs.resolvedSpineIndices.map { idx in
|
||
activeWindowSpineIndices.map { abs(idx - $0) }.min() ?? Int.max
|
||
}.min() ?? Int.max
|
||
return lhsDistance > rhsDistance
|
||
}
|
||
|
||
for segment in sorted {
|
||
if currentMemoryBytes <= policy.memoryBudgetBytes { break }
|
||
currentMemoryBytes -= segment.estimatedMemoryBytes
|
||
lastAccessTime.removeValue(forKey: segment.lowerSpineIndex)
|
||
segments.removeAll { $0.lowerSpineIndex == segment.lowerSpineIndex }
|
||
}
|
||
}
|
||
|
||
RDEPUBBackgroundTrace.log(
|
||
"CoverageStore",
|
||
"after cleanup: segments=\(segments.count) memory=\(currentMemoryBytes)B"
|
||
)
|
||
}
|
||
|
||
/// 检查是否需要淘汰
|
||
private func evictIfNeeded(forNewSegment newSegment: RDEPUBBackgroundCoverageSegment) {
|
||
// 检查段数限制
|
||
while segments.count >= policy.maxResidentSegments {
|
||
evictLeastRecentlyUsed()
|
||
}
|
||
|
||
// 检查内存限制
|
||
while currentMemoryBytes + newSegment.estimatedMemoryBytes > policy.memoryBudgetBytes {
|
||
evictLeastRecentlyUsed()
|
||
}
|
||
}
|
||
|
||
/// 淘汰最近最少使用的段
|
||
private func evictLeastRecentlyUsed() {
|
||
guard !segments.isEmpty else { return }
|
||
|
||
// 找到最久未访问的段
|
||
var oldestTime = CFAbsoluteTimeGetCurrent()
|
||
var oldestIndex = 0
|
||
for (index, segment) in segments.enumerated() {
|
||
let accessTime = lastAccessTime[segment.lowerSpineIndex] ?? 0
|
||
if accessTime < oldestTime {
|
||
oldestTime = accessTime
|
||
oldestIndex = index
|
||
}
|
||
}
|
||
|
||
let evicted = segments.remove(at: oldestIndex)
|
||
currentMemoryBytes -= evicted.estimatedMemoryBytes
|
||
lastAccessTime.removeValue(forKey: evicted.lowerSpineIndex)
|
||
|
||
RDEPUBBackgroundTrace.log(
|
||
"CoverageStore",
|
||
"evicted segment \(evicted.lowerSpineIndex)-\(evicted.upperSpineIndex)"
|
||
)
|
||
}
|
||
|
||
/// 检查两个段是否可以合并
|
||
private func canMerge(_ lhs: RDEPUBBackgroundCoverageSegment, _ rhs: RDEPUBBackgroundCoverageSegment) -> Bool {
|
||
// 渲染签名必须相同
|
||
guard lhs.renderSignature == rhs.renderSignature else { return false }
|
||
|
||
// 检查是否重叠或相邻
|
||
let overlap = lhs.upperSpineIndex >= rhs.lowerSpineIndex - 1 &&
|
||
rhs.upperSpineIndex >= lhs.lowerSpineIndex - 1
|
||
return overlap
|
||
}
|
||
|
||
/// 合并两个段
|
||
private func mergeSegments(_ lhs: RDEPUBBackgroundCoverageSegment, _ rhs: RDEPUBBackgroundCoverageSegment) -> RDEPUBBackgroundCoverageSegment? {
|
||
let newLower = min(lhs.lowerSpineIndex, rhs.lowerSpineIndex)
|
||
let newUpper = max(lhs.upperSpineIndex, rhs.upperSpineIndex)
|
||
let newChapterCount = newUpper - newLower + 1
|
||
|
||
// 检查合并后是否超过单段最大章节数
|
||
if newChapterCount > policy.maxChaptersPerSegment {
|
||
// 按当前阅读位置切分
|
||
return nil
|
||
}
|
||
|
||
// 合并 resolvedSpineIndices
|
||
let newResolved = lhs.resolvedSpineIndices.union(rhs.resolvedSpineIndices)
|
||
|
||
// 合并页图
|
||
let newerSegment = lhs.generatedAt <= rhs.generatedAt ? rhs : lhs
|
||
let olderSegment = lhs.generatedAt <= rhs.generatedAt ? lhs : rhs
|
||
let newPageMap = mergePageMaps(olderSegment.pageMap, newerSegment.pageMap)
|
||
|
||
return RDEPUBBackgroundCoverageSegment(
|
||
lowerSpineIndex: newLower,
|
||
upperSpineIndex: newUpper,
|
||
pageMap: newPageMap,
|
||
resolvedSpineIndices: newResolved,
|
||
generatedAt: max(lhs.generatedAt, rhs.generatedAt),
|
||
renderSignature: lhs.renderSignature,
|
||
estimatedMemoryBytes: estimateMemoryBytes(pageMap: newPageMap, resolvedCount: newResolved.count)
|
||
)
|
||
}
|
||
|
||
/// 合并两个页图
|
||
private func mergePageMaps(_ older: RDEPUBBookPageMap, _ newer: RDEPUBBookPageMap) -> RDEPUBBookPageMap {
|
||
var builder = RDEPUBBookPageMap.Builder()
|
||
var entriesBySpineIndex: [Int: RDEPUBBookPageMapEntry] = [:]
|
||
|
||
for entry in older.entries {
|
||
entriesBySpineIndex[entry.spineIndex] = entry
|
||
}
|
||
for entry in newer.entries {
|
||
entriesBySpineIndex[entry.spineIndex] = entry
|
||
}
|
||
|
||
for spineIndex in entriesBySpineIndex.keys.sorted() {
|
||
guard let entry = entriesBySpineIndex[spineIndex] else { continue }
|
||
builder.add(
|
||
spineIndex: entry.spineIndex,
|
||
href: entry.href,
|
||
title: entry.title,
|
||
pageCount: entry.pageCount,
|
||
fragmentOffsets: entry.fragmentOffsets
|
||
)
|
||
}
|
||
|
||
return builder.build()
|
||
}
|
||
|
||
/// 估算内存占用
|
||
private func estimateMemoryBytes(pageMap: RDEPUBBookPageMap, resolvedCount: Int) -> Int {
|
||
256 + pageMap.entries.count * 96 + resolvedCount * 16
|
||
}
|
||
}
|