feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化

- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
This commit is contained in:
shenlei
2026-06-22 20:26:34 +08:00
parent f50495ad91
commit c65c190b71
178 changed files with 11380 additions and 6728 deletions
@@ -1,65 +1,56 @@
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
memoryBudgetBytes: 8 * 1024 * 1024
)
}
///
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) {
@@ -67,12 +58,10 @@ final class RDEPUBBackgroundCoverageStore {
self.policy = policy
}
///
func addSegment(_ segment: RDEPUBBackgroundCoverageSegment) {
//
evictIfNeeded(forNewSegment: segment)
//
var merged = false
for (index, existing) in segments.enumerated() {
if canMerge(existing, segment) {
@@ -94,7 +83,6 @@ final class RDEPUBBackgroundCoverageStore {
currentMemoryBytes += segment.estimatedMemoryBytes
}
// 访
lastAccessTime[segment.lowerSpineIndex] = CFAbsoluteTimeGetCurrent()
RDEPUBBackgroundTrace.log(
@@ -103,7 +91,6 @@ final class RDEPUBBackgroundCoverageStore {
)
}
/// spineIndex
func findSegment(containing spineIndex: Int) -> RDEPUBBackgroundCoverageSegment? {
let segment = segments.first { $0.contains(spineIndex: spineIndex) }
if let segment {
@@ -112,7 +99,6 @@ final class RDEPUBBackgroundCoverageStore {
return segment
}
/// spineIndex
func findSegment(covering spineIndices: Set<Int>) -> RDEPUBBackgroundCoverageSegment? {
let segment = segments.first { segment in
spineIndices.allSatisfy { segment.contains(spineIndex: $0) }
@@ -123,19 +109,16 @@ final class RDEPUBBackgroundCoverageStore {
return segment
}
///
func allSegments() -> [RDEPUBBackgroundCoverageSegment] {
segments
}
///
func clearAll() {
segments.removeAll()
currentMemoryBytes = 0
lastAccessTime.removeAll()
}
///
func clearColdSegments(
activeWindowSpineIndices: Set<Int>,
protectedSpineIndices: Set<Int>
@@ -151,7 +134,6 @@ final class RDEPUBBackgroundCoverageStore {
}
}
///
func handleMemoryWarning(
activeWindowSpineIndices: Set<Int>,
protectedSpineIndices: Set<Int>
@@ -161,15 +143,13 @@ final class RDEPUBBackgroundCoverageStore {
"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
@@ -194,24 +174,20 @@ final class RDEPUBBackgroundCoverageStore {
)
}
///
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() {
@@ -232,33 +208,27 @@ final class RDEPUBBackgroundCoverageStore {
)
}
///
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)
@@ -274,7 +244,6 @@ final class RDEPUBBackgroundCoverageStore {
)
}
///
private func mergePageMaps(_ older: RDEPUBBookPageMap, _ newer: RDEPUBBookPageMap) -> RDEPUBBookPageMap {
var builder = RDEPUBBookPageMap.Builder()
var entriesBySpineIndex: [Int: RDEPUBBookPageMapEntry] = [:]
@@ -300,7 +269,6 @@ final class RDEPUBBackgroundCoverageStore {
return builder.build()
}
///
private func estimateMemoryBytes(pageMap: RDEPUBBookPageMap, resolvedCount: Int) -> Int {
256 + pageMap.entries.count * 96 + resolvedCount * 16
}