refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
import UIKit
|
||||
|
||||
final class RDEPUBChapterRuntimeStore {
|
||||
|
||||
private let chapterDataCache = RDEPUBChapterDataCache()
|
||||
|
||||
private let pageCountCache = RDEPUBPageCountCache()
|
||||
|
||||
let imageCache = NSCache<NSString, UIImage>()
|
||||
|
||||
let chapterLoadQueue = DispatchQueue(label: "com.RDEpubReader.chapterload", qos: .userInitiated)
|
||||
|
||||
private let chapterLoadQueueKey = DispatchSpecificKey<Void>()
|
||||
|
||||
// M-02: currentSpineIndex and windowSpineIndices are accessed only from the main thread
|
||||
// (verified by audit of all 7 access points). They are not protected by locks unlike
|
||||
// navigationLock/prefetchLock/buildingLock/cfiMapLock, but this is safe as long as
|
||||
// access remains main-thread-only. Do NOT access from chapterLoadQueue.
|
||||
private(set) var currentSpineIndex: Int?
|
||||
|
||||
private(set) var windowSpineIndices: [Int] = []
|
||||
|
||||
private var pendingNavigationTarget: Int?
|
||||
|
||||
private let navigationLock = NSLock()
|
||||
|
||||
private var pendingPrefetchTargets: Set<Int> = []
|
||||
|
||||
private let prefetchLock = NSLock()
|
||||
|
||||
private(set) var isBuilding: Bool = false
|
||||
|
||||
private let buildingLock = NSLock()
|
||||
|
||||
private var buildingCFIMapSpineIndices: Set<Int> = []
|
||||
|
||||
private let cfiMapLock = NSLock()
|
||||
|
||||
private var pendingChapterLoadSpineIndices: Set<Int> = []
|
||||
|
||||
private let pendingChapterLoadLock = NSLock()
|
||||
|
||||
init() {
|
||||
|
||||
imageCache.countLimit = 50
|
||||
imageCache.totalCostLimit = 104_857_600 // 100 MB
|
||||
|
||||
chapterLoadQueue.setSpecific(key: chapterLoadQueueKey, value: ())
|
||||
}
|
||||
|
||||
func assertNotOnChapterLoadQueue() {
|
||||
dispatchPrecondition(condition: .notOnQueue(chapterLoadQueue))
|
||||
}
|
||||
|
||||
func chapterData(for spineIndex: Int) -> RDEPUBRuntimeChapter? {
|
||||
return chapterDataCache[spineIndex]
|
||||
}
|
||||
|
||||
func pageCount(for key: RDEPUBChapterCacheKey) -> RDEPUBRuntimePageCount? {
|
||||
return pageCountCache[key]
|
||||
}
|
||||
|
||||
func insertChapter(_ chapter: RDEPUBRuntimeChapter) {
|
||||
chapterDataCache[chapter.spineIndex] = chapter
|
||||
RDEPUBMemoryProbe.log("chapterLoaded spine=\(chapter.spineIndex) pages=\(chapter.pages.count)")
|
||||
}
|
||||
|
||||
func insertPageCount(_ pc: RDEPUBRuntimePageCount, for key: RDEPUBChapterCacheKey) {
|
||||
pageCountCache[key] = pc
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func evictableSpineIndices() -> [Int] {
|
||||
let windowSet = Set(windowSpineIndices)
|
||||
return chapterDataCache.storedSpineIndices.filter { !windowSet.contains($0) }
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pageCountCache.removeAll()
|
||||
}
|
||||
|
||||
func handleMemoryWarning() {
|
||||
evictAllExceptCurrent()
|
||||
imageCache.removeAllObjects()
|
||||
}
|
||||
|
||||
func setNavigationTarget(spineIndex: Int) {
|
||||
navigationLock.lock()
|
||||
pendingNavigationTarget = spineIndex
|
||||
navigationLock.unlock()
|
||||
}
|
||||
|
||||
func consumeNavigationTarget() -> Int? {
|
||||
navigationLock.lock()
|
||||
let target = pendingNavigationTarget
|
||||
pendingNavigationTarget = nil
|
||||
navigationLock.unlock()
|
||||
return target
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func beginPendingChapterLoad(for spineIndex: Int) -> Bool {
|
||||
pendingChapterLoadLock.lock()
|
||||
defer { pendingChapterLoadLock.unlock() }
|
||||
return pendingChapterLoadSpineIndices.insert(spineIndex).inserted
|
||||
}
|
||||
|
||||
func endPendingChapterLoad(for spineIndex: Int) {
|
||||
pendingChapterLoadLock.lock()
|
||||
pendingChapterLoadSpineIndices.remove(spineIndex)
|
||||
pendingChapterLoadLock.unlock()
|
||||
}
|
||||
|
||||
func hasPendingChapterLoad(for spineIndex: Int) -> Bool {
|
||||
pendingChapterLoadLock.lock()
|
||||
let hasPendingLoad = pendingChapterLoadSpineIndices.contains(spineIndex)
|
||||
pendingChapterLoadLock.unlock()
|
||||
return hasPendingLoad
|
||||
}
|
||||
|
||||
func beginBuildingCFIMap(for spineIndex: Int) -> Bool {
|
||||
cfiMapLock.lock()
|
||||
defer { cfiMapLock.unlock() }
|
||||
let inserted = buildingCFIMapSpineIndices.insert(spineIndex).inserted
|
||||
return inserted
|
||||
}
|
||||
|
||||
func endBuildingCFIMap(for spineIndex: Int) {
|
||||
cfiMapLock.lock()
|
||||
buildingCFIMapSpineIndices.remove(spineIndex)
|
||||
cfiMapLock.unlock()
|
||||
}
|
||||
|
||||
func invalidateAllLayoutDependentContent() {
|
||||
RDEPUBMemoryProbe.log("layoutDependentContentInvalidateAll")
|
||||
chapterDataCache.removeAll()
|
||||
pageCountCache.removeAll()
|
||||
imageCache.removeAllObjects()
|
||||
cfiMapLock.lock()
|
||||
buildingCFIMapSpineIndices.removeAll()
|
||||
cfiMapLock.unlock()
|
||||
pendingChapterLoadLock.lock()
|
||||
pendingChapterLoadSpineIndices.removeAll()
|
||||
pendingChapterLoadLock.unlock()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user