feat: 架构整改 — Context拆分、Runtime拆分、异步章节加载、UI测试覆盖
Phase 1: Context 拆分 - 新增 RDEPUBReaderState/RDEPUBReaderEnvironment/RDEPUBReaderServices - RDEPUBReaderContext 改为过渡门面,代理到 State/Environment/Services Phase 2: Runtime 拆分 - 新增 RDEPUBPresentationRuntime 处理分页状态管理 - 新增 RDEPUBChapterWarmupOrchestrator 处理章节预热与加载编排 - RDEPUBReaderRuntime 从 1277 行收缩,公共 API 转发到新 facade Phase 0.5: 性能优化 - prepareOnDemandChapter 支持异步模式(allowSynchronousLoad: false) - extendPartialBookPageMapIfNeeded 改为 DispatchGroup 并发加载 - RDEPUBChapterOffsetMap.cfiMap 加 NSLock 保护数据竞争 - CFI Map 构建延迟到后台队列(scheduleDeferredCFIMapBuildIfNeeded) - RDEPUBTextPageRenderView 引入静态位图缓存 - RDEPUBTextContentView 新增 loadingSpinner 占位页 Phase 3: 状态机 - 新增 RDEPUBNavigationStateMachine(含 DEBUG 合法转换校验) - 新增 RDEPUBPaginationState 记录分页来源 Review 修复 - makeSummary 重复方法合并 - ensureNavigationTargetAvailable 同步路径加注释标记 UI 测试 - 新增 AsyncChapterLoadingTests(20 个测试,覆盖全部架构整改场景) - 跨章节翻页、延迟 CFI、状态机、内存警告、预加载、位置恢复等
This commit is contained in:
@@ -10,54 +10,108 @@ final class RDEPUBReaderContext {
|
||||
|
||||
weak var readerView: RDReaderView?
|
||||
|
||||
var dependencies: RDEPUBReaderDependencies = .live
|
||||
let state: RDEPUBReaderState
|
||||
|
||||
let environment: RDEPUBReaderEnvironment
|
||||
|
||||
let services: RDEPUBReaderServices
|
||||
|
||||
var dependencies: RDEPUBReaderDependencies {
|
||||
get { services.dependencies }
|
||||
set {
|
||||
services.dependencies = newValue
|
||||
environment.displayEnvironment = newValue.environment
|
||||
}
|
||||
}
|
||||
|
||||
var runtime: RDEPUBReaderRuntime? {
|
||||
controller?.runtime
|
||||
}
|
||||
|
||||
var parser: RDEPUBParser?
|
||||
|
||||
var publication: RDEPUBPublication?
|
||||
|
||||
var readingSession: RDEPUBReadingSession?
|
||||
|
||||
var textBook: RDEPUBTextBook?
|
||||
|
||||
var bookPageMap: RDEPUBBookPageMap?
|
||||
|
||||
var activeBookmarks: [RDEPUBBookmark] = []
|
||||
|
||||
var activeHighlights: [RDEPUBHighlight] = []
|
||||
|
||||
var currentBookIdentifier: String?
|
||||
|
||||
var paginationToken = UUID()
|
||||
|
||||
var paginator: RDEPUBPaginator?
|
||||
|
||||
var searchState: RDEPUBSearchState?
|
||||
|
||||
var pendingFullPageMap: RDEPUBBookPageMap?
|
||||
|
||||
var lastTextPaginationPageSize: CGSize?
|
||||
|
||||
var lastMetadataParseWallClockMs: Int = 0
|
||||
|
||||
var lastMetadataParseConcurrency: Int = 0
|
||||
|
||||
var currentSelection: RDEPUBSelection? {
|
||||
get { selectionState.selection }
|
||||
set {
|
||||
if let newValue, !newValue.isEmpty {
|
||||
selectionState = .selected(newValue)
|
||||
} else {
|
||||
selectionState = .idle
|
||||
}
|
||||
}
|
||||
var parser: RDEPUBParser? {
|
||||
get { state.parser }
|
||||
set { state.parser = newValue }
|
||||
}
|
||||
|
||||
var selectionState: RDEPUBSelectionState = .idle
|
||||
var publication: RDEPUBPublication? {
|
||||
get { state.publication }
|
||||
set { state.publication = newValue }
|
||||
}
|
||||
|
||||
var readingSession: RDEPUBReadingSession? {
|
||||
get { state.readingSession }
|
||||
set { state.readingSession = newValue }
|
||||
}
|
||||
|
||||
var textBook: RDEPUBTextBook? {
|
||||
get { state.textBook }
|
||||
set { state.textBook = newValue }
|
||||
}
|
||||
|
||||
var bookPageMap: RDEPUBBookPageMap? {
|
||||
get { state.bookPageMap }
|
||||
set { state.bookPageMap = newValue }
|
||||
}
|
||||
|
||||
var activeBookmarks: [RDEPUBBookmark] {
|
||||
get { state.activeBookmarks }
|
||||
set { state.activeBookmarks = newValue }
|
||||
}
|
||||
|
||||
var activeHighlights: [RDEPUBHighlight] {
|
||||
get { state.activeHighlights }
|
||||
set { state.activeHighlights = newValue }
|
||||
}
|
||||
|
||||
var currentBookIdentifier: String? {
|
||||
get { state.currentBookIdentifier }
|
||||
set { state.currentBookIdentifier = newValue }
|
||||
}
|
||||
|
||||
var paginationToken: UUID {
|
||||
get { state.paginationToken }
|
||||
set { state.paginationToken = newValue }
|
||||
}
|
||||
|
||||
var paginator: RDEPUBPaginator? {
|
||||
get { state.paginator }
|
||||
set { state.paginator = newValue }
|
||||
}
|
||||
|
||||
var searchState: RDEPUBSearchState? {
|
||||
get { state.searchState }
|
||||
set { state.searchState = newValue }
|
||||
}
|
||||
|
||||
var pendingFullPageMap: RDEPUBBookPageMap? {
|
||||
get { state.pendingFullPageMap }
|
||||
set { state.pendingFullPageMap = newValue }
|
||||
}
|
||||
|
||||
var lastTextPaginationPageSize: CGSize? {
|
||||
get { state.lastTextPaginationPageSize }
|
||||
set { state.lastTextPaginationPageSize = newValue }
|
||||
}
|
||||
|
||||
var lastMetadataParseWallClockMs: Int {
|
||||
get { state.lastMetadataParseWallClockMs }
|
||||
set { state.lastMetadataParseWallClockMs = newValue }
|
||||
}
|
||||
|
||||
var lastMetadataParseConcurrency: Int {
|
||||
get { state.lastMetadataParseConcurrency }
|
||||
set { state.lastMetadataParseConcurrency = newValue }
|
||||
}
|
||||
|
||||
var currentSelection: RDEPUBSelection? {
|
||||
get { state.currentSelection }
|
||||
set { state.currentSelection = newValue }
|
||||
}
|
||||
|
||||
var selectionState: RDEPUBSelectionState {
|
||||
get { state.selectionState }
|
||||
set { state.selectionState = newValue }
|
||||
}
|
||||
|
||||
var configuration: RDEPUBReaderConfiguration = .default
|
||||
|
||||
@@ -65,32 +119,43 @@ final class RDEPUBReaderContext {
|
||||
|
||||
var epubURL: URL = URL(string: "about:blank")!
|
||||
|
||||
var isRepaginating: Bool = false
|
||||
var isRepaginating: Bool {
|
||||
get { state.isRepaginating }
|
||||
set { state.isRepaginating = newValue }
|
||||
}
|
||||
|
||||
var didStartInitialLoad: Bool = false
|
||||
var didStartInitialLoad: Bool {
|
||||
get { state.didStartInitialLoad }
|
||||
set { state.didStartInitialLoad = newValue }
|
||||
}
|
||||
|
||||
var isExternalTextBook: Bool = false
|
||||
var isExternalTextBook: Bool {
|
||||
get { state.isExternalTextBook }
|
||||
set { state.isExternalTextBook = newValue }
|
||||
}
|
||||
|
||||
var textFileURL: URL?
|
||||
var textFileURL: URL? {
|
||||
get { state.textFileURL }
|
||||
set { state.textFileURL = newValue }
|
||||
}
|
||||
|
||||
var textBookCache = RDEPUBTextBookCache()
|
||||
var textBookCache: RDEPUBTextBookCache { state.textBookCache }
|
||||
|
||||
init(controller: RDEPUBReaderController) {
|
||||
self.controller = controller
|
||||
self.readerView = controller.readerView
|
||||
let state = RDEPUBReaderState()
|
||||
self.state = state
|
||||
self.environment = RDEPUBReaderEnvironment(
|
||||
controller: controller,
|
||||
readerView: controller.readerView,
|
||||
displayEnvironment: RDEPUBUIScreenEnvironment()
|
||||
)
|
||||
self.services = RDEPUBReaderServices(dependencies: .live)
|
||||
}
|
||||
|
||||
func currentLayoutContext() -> RDEPUBNavigatorLayoutContext {
|
||||
let containerSize = readerView?.bounds.size ?? .zero
|
||||
let viewSize = controller?.view.bounds.size ?? containerSize
|
||||
let resolvedSize = containerSize == .zero ? viewSize : containerSize
|
||||
return RDEPUBNavigatorLayoutContext(
|
||||
containerSize: resolvedSize,
|
||||
pagesPerScreen: readerView?.pagesPerScreen ?? 1,
|
||||
safeAreaInsets: controller?.view.safeAreaInsets ?? .zero,
|
||||
userInterfaceIdiom: controller?.traitCollection.userInterfaceIdiom ?? .phone,
|
||||
reflowableContentInsets: configuration.reflowableContentInsets
|
||||
)
|
||||
environment.currentLayoutContext(configuration: configuration)
|
||||
}
|
||||
|
||||
func currentPreferences() -> RDEPUBPreferences {
|
||||
@@ -122,82 +187,60 @@ final class RDEPUBReaderContext {
|
||||
return mainThreadSize
|
||||
}
|
||||
}
|
||||
return dependencies.environment.fallbackViewportSize
|
||||
return environment.fallbackViewportSize
|
||||
}
|
||||
|
||||
func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
||||
let font = configuration.fontChoice.font(ofSize: configuration.fontSize)
|
||||
let lineSpacing = max(font.lineHeight * (configuration.lineHeightMultiple - 1), 4)
|
||||
return RDEPUBTextRenderStyle(
|
||||
font: font,
|
||||
lineSpacing: lineSpacing,
|
||||
textColor: configuration.theme.contentTextColor,
|
||||
backgroundColor: configuration.theme.contentBackgroundColor
|
||||
)
|
||||
environment.currentTextRenderStyle(configuration: configuration)
|
||||
}
|
||||
|
||||
func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig {
|
||||
return RDEPUBTextLayoutConfig(
|
||||
frameWidth: max(pageSize.width, 1),
|
||||
frameHeight: max(pageSize.height, 1),
|
||||
edgeInsets: configuration.reflowableContentInsets,
|
||||
numberOfColumns: configuration.numberOfColumns,
|
||||
columnGap: configuration.columnGap,
|
||||
|
||||
avoidOrphans: false,
|
||||
avoidWidows: false,
|
||||
avoidPageBreakInsideEnabled: true,
|
||||
hyphenation: true,
|
||||
imageMaxHeightRatio: 0.85,
|
||||
fallbackViewportSize: dependencies.environment.fallbackViewportSize
|
||||
)
|
||||
environment.currentTextLayoutConfig(configuration: configuration, pageSize: pageSize)
|
||||
}
|
||||
|
||||
func resolvedTextRenderer() -> RDEPUBTextRenderer {
|
||||
dependencies.makeTextRenderer(configuration.textRenderingEngine)
|
||||
services.resolvedTextRenderer(configuration: configuration)
|
||||
}
|
||||
|
||||
var activePages: [EPUBPage] {
|
||||
readingSession?.activePages ?? []
|
||||
state.activePages
|
||||
}
|
||||
|
||||
var activeChapters: [EPUBChapterInfo] {
|
||||
readingSession?.activeChapters ?? []
|
||||
state.activeChapters
|
||||
}
|
||||
|
||||
var currentBrightness: CGFloat {
|
||||
get { dependencies.environment.currentBrightness }
|
||||
set { dependencies.environment.currentBrightness = newValue }
|
||||
get { environment.currentBrightness }
|
||||
set { environment.currentBrightness = newValue }
|
||||
}
|
||||
|
||||
func replaceActiveSnapshot(_ snapshot: RDEPUBReadingSession.PaginationSnapshot) {
|
||||
readingSession?.setActiveSnapshot(snapshot)
|
||||
state.replaceActiveSnapshot(snapshot)
|
||||
}
|
||||
|
||||
func clearActiveSnapshot() {
|
||||
readingSession?.resetRuntimeState()
|
||||
state.clearActiveSnapshot()
|
||||
}
|
||||
|
||||
func makeParser() -> RDEPUBParser {
|
||||
dependencies.makeParser()
|
||||
services.makeParser()
|
||||
}
|
||||
|
||||
func makePaginator() -> RDEPUBPaginator {
|
||||
dependencies.makePaginator()
|
||||
services.makePaginator()
|
||||
}
|
||||
|
||||
func makeTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDEPUBTextBookBuilder {
|
||||
dependencies.makeTextBookBuilder(resolvedTextRenderer(), textBookCache, layoutConfig)
|
||||
services.makeTextBookBuilder(
|
||||
configuration: configuration,
|
||||
cache: textBookCache,
|
||||
layoutConfig: layoutConfig
|
||||
)
|
||||
}
|
||||
|
||||
func makeChapterSummaryDiskCache() -> RDEPUBChapterSummaryDiskCache {
|
||||
let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
|
||||
?? FileManager.default.temporaryDirectory
|
||||
let bookID = (currentBookIdentifier ?? "default").sha256Hex
|
||||
let directory = cachesDirectory
|
||||
.appendingPathComponent("RDEPUBChapterSummaryCache", isDirectory: true)
|
||||
.appendingPathComponent(bookID, isDirectory: true)
|
||||
return RDEPUBChapterSummaryDiskCache(cacheDirectory: directory)
|
||||
services.makeChapterSummaryDiskCache(bookIdentifier: currentBookIdentifier)
|
||||
}
|
||||
|
||||
func chapterCacheKey(forSpineIndex spineIndex: Int) -> RDEPUBChapterCacheKey {
|
||||
@@ -272,7 +315,10 @@ final class RDEPUBReaderContext {
|
||||
}
|
||||
|
||||
func makePlainTextBookBuilder(layoutConfig: RDEPUBTextLayoutConfig) -> RDPlainTextBookBuilder {
|
||||
dependencies.makePlainTextBookBuilder(resolvedTextRenderer(), layoutConfig)
|
||||
services.makePlainTextBookBuilder(
|
||||
configuration: configuration,
|
||||
layoutConfig: layoutConfig
|
||||
)
|
||||
}
|
||||
|
||||
func currentVisibleLocation() -> RDEPUBLocation? {
|
||||
|
||||
Reference in New Issue
Block a user