import Foundation /// 后台覆盖段:表示某一段章节范围的稳定页码覆盖 struct RDEPUBBackgroundCoverageSegment { /// 段的下界 spineIndex let lowerSpineIndex: Int /// 段的上界 spineIndex let upperSpineIndex: Int /// 页图数据 let pageMap: RDEPUBBookPageMap /// 已解析的 spineIndex 集合 let resolvedSpineIndices: Set /// 创建时间 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) -> 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, protectedSpineIndices: Set ) { 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, protectedSpineIndices: Set ) { 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 } }