import UIKit final class RDEPUBChapterRuntimeStore { // MARK: - 子缓存 /// 章节运行时主缓存(等价 WXRead chapterDataCache) private let chapterDataCache = RDEPUBChapterDataCache() /// 轻量分页结构缓存(等价 WXRead pageCountCache) private let pageCountCache = RDEPUBPageCountCache() /// 图片缓存(独立 NSCache,等价 WXRead imageCache) let imageCache = NSCache() /// 串行加载队列(等价 WXRead com.weread.chapterload) /// QoS .userInitiated:用户主动跳章/开书属于前台交互,需尽快完成 let chapterLoadQueue = DispatchQueue(label: "com.rdreader.chapterload", qos: .userInitiated) private let chapterLoadQueueKey = DispatchSpecificKey() // MARK: - 窗口状态 /// 当前章 spineIndex private(set) var currentSpineIndex: Int? /// 当前窗口内的 spineIndex 集合(以当前章为中心,按配置半径展开) private(set) var windowSpineIndices: [Int] = [] // MARK: - 请求通道(前台导航 vs 后台预取,语义独立,互不抢占) /// 前台导航目标(用户主动跳章:目录/书签/搜索/翻章) /// 仅保留最后一次目标,旧的排队请求可被取消 private var pendingNavigationTarget: Int? private let navigationLock = NSLock() /// 后台预取目标集合(±1 相邻章预取) /// 预取不抢占前台导航通道,预取完成后仅刷新快照,不触发跳章 private var pendingPrefetchTargets: Set = [] private let prefetchLock = NSLock() /// 是否有章节正在构建中 private(set) var isBuilding: Bool = false private let buildingLock = NSLock() // MARK: - 初始化 init() { imageCache.countLimit = 50 chapterLoadQueue.setSpecific(key: chapterLoadQueueKey, value: ()) } func assertNotOnChapterLoadQueue() { dispatchPrecondition(condition: .notOnQueue(chapterLoadQueue)) } // MARK: - 缓存查询(线程安全,通过 cache wrapper 的 lock 保护) func chapterData(for spineIndex: Int) -> RDEPUBRuntimeChapter? { return chapterDataCache[spineIndex] } func pageCount(for key: RDEPUBChapterCacheKey) -> RDEPUBRuntimePageCount? { return pageCountCache[key] } // MARK: - 缓存插入 func insertChapter(_ chapter: RDEPUBRuntimeChapter) { chapterDataCache[chapter.spineIndex] = chapter } func insertPageCount(_ pc: RDEPUBRuntimePageCount, for key: RDEPUBChapterCacheKey) { pageCountCache[key] = pc } // MARK: - 窗口管理 /// 设定当前章,自动计算按半径展开的窗口 func setCurrentChapter(spineIndex: Int, totalSpineCount: Int, windowRadius: Int = 1) { currentSpineIndex = spineIndex let radius = max(0, windowRadius) let lowerBound = max(0, spineIndex - radius) let upperBound = min(totalSpineCount - 1, spineIndex + radius) guard lowerBound <= upperBound else { windowSpineIndices = [spineIndex] return } windowSpineIndices = Array(lowerBound...upperBound) } /// 返回窗口外、应该淘汰的 spineIndex func evictableSpineIndices() -> [Int] { let windowSet = Set(windowSpineIndices) return chapterDataCache.storedSpineIndices.filter { !windowSet.contains($0) } } // MARK: - 淘汰 func evict(spineIndex: Int) { chapterDataCache.remove(spineIndex: spineIndex) pageCountCache.remove(forSpineIndex: spineIndex) } func evictAllExceptCurrent() { guard let current = currentSpineIndex else { chapterDataCache.removeAll() pageCountCache.removeAll() return } let currentChapter = chapterDataCache[current] chapterDataCache.removeAll() if let ch = currentChapter { chapterDataCache[current] = ch } // WXRead 语义:内存警告时 pageCountCache 全量清空 pageCountCache.removeAll() } // MARK: - 内存警告 func handleMemoryWarning() { evictAllExceptCurrent() imageCache.removeAllObjects() } // MARK: - 前台导航请求管理(§14.4 取消语义) /// 注册前台导航目标(用户主动跳章时调用) /// 仅保留最后一次目标,旧的排队请求可被取消 func setNavigationTarget(spineIndex: Int) { navigationLock.lock() pendingNavigationTarget = spineIndex navigationLock.unlock() } /// 消费前台导航目标(章节构建完成后调用,检查是否有更新的目标) func consumeNavigationTarget() -> Int? { navigationLock.lock() let target = pendingNavigationTarget pendingNavigationTarget = nil navigationLock.unlock() return target } // MARK: - 后台预取请求管理 /// 注册后台预取目标(±1 相邻章预取时调用) /// 预取不抢占前台导航通道 func addPrefetchTarget(_ spineIndex: Int) { prefetchLock.lock() pendingPrefetchTargets.insert(spineIndex) prefetchLock.unlock() } /// 标记预取目标已完成 func removePrefetchTarget(_ spineIndex: Int) { prefetchLock.lock() pendingPrefetchTargets.remove(spineIndex) prefetchLock.unlock() } /// 清空所有预取目标(切章时调用,旧预取结果不再需要) func clearPrefetchTargets() { prefetchLock.lock() pendingPrefetchTargets.removeAll() prefetchLock.unlock() } /// 检查是否有待处理的预取目标 func hasPrefetchTarget(_ spineIndex: Int) -> Bool { prefetchLock.lock() let has = pendingPrefetchTargets.contains(spineIndex) prefetchLock.unlock() return has } func markBuilding(_ building: Bool) { buildingLock.lock() isBuilding = building buildingLock.unlock() } // MARK: - P1: 排版参数变化后整体失效(§8.5) func invalidateAllForSettingsChange() { chapterDataCache.removeAll() pageCountCache.removeAll() imageCache.removeAllObjects() } }