Files
ReadViewSDK/Sources/RDReaderView/EPUBUI/ReaderController/RDEPUBBackgroundCoverageStore.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

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