- 拆分 ContentDelegates/TextContentView 为独立协调器(InteractionCoordinator、LocationResolution、ExternalLinks、AttachmentTooltip) - 新增 RDEPUBAttachmentTooltipView/OverlayView 附件气泡提示 - 新增 RDEPUBDarkImageAdjuster 暗色模式图片亮度适配 - 新增 RDEPUBSelectionLoupeView 选区放大镜 - 新增 MetadataParseWorker/CancellationController 元数据解析取消机制 - 重构 PresentationRuntime/PaginationCoordinator 精简职责 - 优化 ChapterLoader/WarmupOrchestrator 异步章节加载 - CFI 模块微调与 NoteModels 更新 - 清理冗余文档,更新架构/UML/业务逻辑文档 Co-Authored-By: Claude <noreply@anthropic.com>
735 lines
21 KiB
Markdown
735 lines
21 KiB
Markdown
# ReadViewSDK UML 类图
|
||
|
||
> 最后更新:2026-06-09
|
||
> 图表格式:Mermaid(可在 GitHub / VS Code / Mermaid Live Editor 中渲染)
|
||
|
||
---
|
||
|
||
## 1. 整体模块关系
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph EPUBCore
|
||
Parser[RDEPUBParser]
|
||
Pub[RDEPUBPublication]
|
||
Resolver[RDEPUBResourceResolver]
|
||
Session[RDEPUBReadingSession]
|
||
WebView[RDEPUBWebView]
|
||
Paginator[RDEPUBPaginator]
|
||
SearchEngine[RDEPUBHTMLSearchEngine]
|
||
end
|
||
|
||
subgraph EPUBTextRendering
|
||
Builder[RDEPUBTextBookBuilder]
|
||
Renderer[RDEPUBDTCoreTextRenderer]
|
||
Pipeline[RDEPUBTextTypesetterPipeline]
|
||
Counter[RDEPUBChapterPageCounter]
|
||
FrameFactory[RDEPUBCoreTextPageFrameFactory]
|
||
BreakPolicy[RDEPUBPageBreakPolicy]
|
||
BookCache[RDEPUBTextBookCache]
|
||
ChapterData[RDEPUBChapterData]
|
||
IndexTable[RDEPUBTextIndexTable]
|
||
end
|
||
|
||
subgraph RDReaderView
|
||
ReaderView[RDReaderView]
|
||
FlowLayout[RDReaderFlowLayout]
|
||
Preload[RDReaderPreloadController]
|
||
Spread[RDReaderSpreadResolver]
|
||
TapRegion[RDReaderTapRegionHandler]
|
||
end
|
||
|
||
subgraph EPUBUI
|
||
Controller[RDEPUBReaderController]
|
||
Context[RDEPUBReaderContext]
|
||
Runtime[RDEPUBReaderRuntime]
|
||
PaginationCoord[RDEPUBReaderPaginationCoordinator]
|
||
Loader[RDEPUBChapterLoader]
|
||
RuntimeStore[RDEPUBChapterRuntimeStore]
|
||
DiskCache[RDEPUBChapterSummaryDiskCache]
|
||
PageMap[RDEPUBBookPageMap]
|
||
Config[RDEPUBReaderConfiguration]
|
||
end
|
||
|
||
Parser --> Pub
|
||
Pub --> Resolver
|
||
Session --> Pub
|
||
WebView --> Pub
|
||
WebView --> Resolver
|
||
Paginator --> Parser
|
||
|
||
Builder --> Renderer
|
||
Builder --> Pipeline
|
||
Builder --> Counter
|
||
Counter --> FrameFactory
|
||
FrameFactory --> BreakPolicy
|
||
Builder --> BookCache
|
||
ChapterData --> IndexTable
|
||
|
||
Controller --> Context
|
||
Context --> Runtime
|
||
Runtime --> Loader
|
||
Runtime --> RuntimeStore
|
||
Runtime --> DiskCache
|
||
PaginationCoord --> Context
|
||
PaginationCoord --> PageMap
|
||
Loader --> RuntimeStore
|
||
Loader --> DiskCache
|
||
|
||
Controller --> ReaderView
|
||
Controller --> Session
|
||
Controller --> Builder
|
||
ReaderView --> FlowLayout
|
||
ReaderView --> Preload
|
||
ReaderView --> Spread
|
||
ReaderView --> TapRegion
|
||
```
|
||
|
||
---
|
||
|
||
## 2. EPUBCore 核心类图
|
||
|
||
```mermaid
|
||
classDiagram
|
||
class RDEPUBParser {
|
||
+metadata: RDEPUBMetadata
|
||
+manifest: [String: RDEPUBManifestItem]
|
||
+spine: [RDEPUBSpineItem]
|
||
+tableOfContents: [EPUBTableOfContentsItem]
|
||
+extractionRootURL: URL
|
||
+opfURL: URL
|
||
+opfDirectoryURL: URL
|
||
+parse(epubURL: URL) throws
|
||
+makePublication() RDEPUBPublication
|
||
+htmlString(forRelativePath:) String?
|
||
+htmlString(forSpineIndex:) String?
|
||
+coverImage() UIImage?
|
||
+resourceURL(forRelativePath:) URL
|
||
+fileURL(forRelativePath:) URL?
|
||
}
|
||
|
||
class RDEPUBPublication {
|
||
+parser: RDEPUBParser
|
||
+resourceResolver: RDEPUBResourceResolver
|
||
+metadata: RDEPUBMetadata
|
||
+manifest: [RDEPUBManifestItem]
|
||
+spine: [RDEPUBSpineItem]
|
||
+tableOfContents: [EPUBTableOfContentsItem]
|
||
+layout: RDEPUBLayout
|
||
+readingProfile: RDEPUBReadingProfile
|
||
+readingProgression: RDEPUBReadingProgression
|
||
+bookIdentifier: String?
|
||
+fixedLayoutSpreadEnabled(preferences:viewportSize:) Bool
|
||
+makeFixedSpreads(preferences:viewportSize:) [EPUBFixedSpread]
|
||
}
|
||
|
||
class RDEPUBResourceResolver {
|
||
+parser: RDEPUBParser
|
||
+fileURL(forRelativePath:) URL?
|
||
+resourceURL(forRelativePath:) URL
|
||
+normalizedHref(_:relativeToSpineIndex:) String?
|
||
+normalizedLocation(_:relativeToSpineIndex:bookIdentifier:) RDEPUBLocation?
|
||
+spineIndex(forNormalizedHref:) Int?
|
||
+spineIndex(for:) Int?
|
||
}
|
||
|
||
class RDEPUBReadingSession {
|
||
+publication: RDEPUBPublication
|
||
+navigatorState: RDEPUBNavigatorState
|
||
+activePages: [EPUBPage]
|
||
+activeChapters: [EPUBChapterInfo]
|
||
+currentViewport: RDEPUBViewport?
|
||
+currentReadingContext: RDEPUBReadingContext?
|
||
+transition(to: RDEPUBNavigatorState)
|
||
+setActiveSnapshot(_:)
|
||
+queueNavigation(to:relativeToSpineIndex:bookIdentifier:) Int
|
||
+updateReadingContext(...)
|
||
+makePaginationSnapshot(pageCounts:preferences:layoutContext:)
|
||
}
|
||
|
||
class RDEPUBWebView {
|
||
+publication: RDEPUBPublication?
|
||
+currentRenderRequest: RDEPUBRenderRequest?
|
||
+webView: WKWebView
|
||
+delegate: RDEPUBWebViewDelegate?
|
||
+load(publication:request:)
|
||
+reset()
|
||
}
|
||
|
||
class RDEPUBNavigatorState {
|
||
<<enum>>
|
||
initializing
|
||
loading
|
||
idle
|
||
jumping
|
||
moving
|
||
repaginating
|
||
+isStableForSnapshotApplication: Bool
|
||
}
|
||
|
||
RDEPUBParser --> RDEPUBPublication : creates
|
||
RDEPUBPublication --> RDEPUBResourceResolver : owns
|
||
RDEPUBReadingSession --> RDEPUBPublication : wraps
|
||
RDEPUBReadingSession --> RDEPUBNavigatorState : manages
|
||
RDEPUBWebView --> RDEPUBPublication : uses
|
||
RDEPUBWebView --> RDEPUBResourceResolver : uses
|
||
```
|
||
|
||
---
|
||
|
||
## 3. EPUBTextRendering 类图
|
||
|
||
```mermaid
|
||
classDiagram
|
||
class RDEPUBTextBookBuilder {
|
||
+renderer: RDEPUBTextRenderer
|
||
+cache: RDEPUBTextBookCache?
|
||
+layoutConfig: RDEPUBTextLayoutConfig
|
||
+renderPipeline: RDEPUBChapterRenderPipeline
|
||
+paginationPipeline: RDEPUBChapterPaginationPipeline
|
||
+tailNormalizer: RDEPUBChapterTailNormalizer
|
||
+build(parser:publication:pageSize:style:) throws RDEPUBTextBook
|
||
+buildChapter(parser:publication:spineIndex:pageSize:style:) throws RDEPUBTextChapterBuildResult?
|
||
}
|
||
|
||
class RDEPUBTextRenderer {
|
||
<<protocol>>
|
||
+renderChapter(request:) throws RDEPUBRenderedChapterContent
|
||
}
|
||
|
||
class RDEPUBDTCoreTextRenderer {
|
||
+renderChapter(request:) throws RDEPUBRenderedChapterContent
|
||
+renderChapter(html:baseURL:style:) throws RDEPUBRenderedChapterContent
|
||
}
|
||
|
||
class RDEPUBTextTypesetterPipeline {
|
||
+makeRequest(from: RDEPUBTypesettingInput) RDEPUBTypesettingOutput
|
||
}
|
||
|
||
class RDEPUBChapterPageCounter {
|
||
+factory: RDEPUBCoreTextPageFrameFactory
|
||
+layoutFrames(fragmentOffsets:) [RDEPUBTextLayoutFrame]
|
||
}
|
||
|
||
class RDEPUBCoreTextPageFrameFactory {
|
||
+attributedString: NSAttributedString
|
||
+pageSize: CGSize
|
||
+config: RDEPUBTextLayoutConfig
|
||
+makeFrames(...) [RDEPUBTextLayoutFrame]
|
||
}
|
||
|
||
class RDEPUBPageBreakPolicy {
|
||
+lineIsInAvoidPageBreakInsideBlock(_:) Bool
|
||
+lineIsInKeepWithNextBlock(_:) Bool
|
||
+adjustedRange(from:totalLength:lineRanges:factory:) RDEPUBPageBreakDecision
|
||
}
|
||
|
||
class RDEPUBTextBook {
|
||
+chapters: [RDEPUBTextChapter]
|
||
+pages: [RDEPUBTextPage]
|
||
+indexTable: RDEPUBTextIndexTable
|
||
+positionConverter: RDEPUBTextPositionConverter
|
||
+chapterData(for href:) RDEPUBChapterData?
|
||
+chapterData(forPageNumber:) RDEPUBChapterData?
|
||
}
|
||
|
||
class RDEPUBTextChapter {
|
||
+chapterIndex: Int
|
||
+spineIndex: Int
|
||
+href: String
|
||
+title: String
|
||
+attributedContent: NSAttributedString
|
||
+fragmentOffsets: [String: Int]
|
||
+pages: [RDEPUBTextPage]
|
||
}
|
||
|
||
class RDEPUBTextPage {
|
||
+absolutePageIndex: Int
|
||
+chapterIndex: Int
|
||
+spineIndex: Int
|
||
+contentRange: NSRange
|
||
+metadata: RDEPUBTextPageMetadata
|
||
}
|
||
|
||
class RDEPUBTextBookCache {
|
||
+schemaVersion: Int
|
||
+load(key:) [String: RDEPUBTextChapterPaginationCache]?
|
||
+save(_:key:)
|
||
+invalidateAll()
|
||
}
|
||
|
||
class RDEPUBChapterData {
|
||
+chapterIndex: Int
|
||
+spineIndex: Int
|
||
+href: String
|
||
+title: String
|
||
+pageCount: Int
|
||
+page(containing:) RDEPUBTextPage?
|
||
+pageNumber(containing:) Int?
|
||
+absoluteRange(for location:) ClosedRange<Int>?
|
||
}
|
||
|
||
class RDEPUBTextIndexTable {
|
||
+chapterStartOffsets: [Int]
|
||
+totalCharacterCount: Int
|
||
+anchor(forAbsoluteIndex:in:) RDEPUBTextAnchor
|
||
+pageNumber(for:in:) Int
|
||
+globalIndex(for:) Int
|
||
}
|
||
|
||
RDEPUBTextBookBuilder --> RDEPUBTextRenderer : uses
|
||
RDEPUBDTCoreTextRenderer ..|> RDEPUBTextRenderer
|
||
RDEPUBTextBookBuilder --> RDEPUBTextTypesetterPipeline : uses
|
||
RDEPUBTextBookBuilder --> RDEPUBChapterPageCounter : uses
|
||
RDEPUBChapterPageCounter --> RDEPUBCoreTextPageFrameFactory : uses
|
||
RDEPUBCoreTextPageFrameFactory --> RDEPUBPageBreakPolicy : uses
|
||
RDEPUBTextBookBuilder --> RDEPUBTextBookCache : uses
|
||
RDEPUBTextBook --> RDEPUBTextChapter : contains
|
||
RDEPUBTextChapter --> RDEPUBTextPage : contains
|
||
RDEPUBTextBook --> RDEPUBTextIndexTable : computes
|
||
RDEPUBChapterData --> RDEPUBTextChapter : wraps
|
||
RDEPUBChapterData --> RDEPUBTextIndexTable : uses
|
||
```
|
||
|
||
---
|
||
|
||
## 4. ReaderView 类图
|
||
|
||
```mermaid
|
||
classDiagram
|
||
class RDReaderView {
|
||
+currentPage: Int
|
||
+currentDisplayType: DisplayType
|
||
+pageProvider: RDReaderPageProvider?
|
||
+delegate: RDReaderDelegate?
|
||
+landscapeDualPageEnabled: Bool
|
||
+pageDirection: PageDirection
|
||
+coverPageIndex: Int?
|
||
+pagesPerScreen: Int
|
||
+transitionToPage(pageNum:animated:)
|
||
+reloadData()
|
||
+switchReaderDisplayType(_:)
|
||
}
|
||
|
||
class DisplayType {
|
||
<<enum>>
|
||
pageCurl
|
||
horizontalScroll
|
||
verticalScroll
|
||
}
|
||
|
||
class RDReaderPageProvider {
|
||
<<protocol>>
|
||
+numberOfPages(in:) Int
|
||
+readerView(_:viewForPageAt:reusableView:) UIView
|
||
+pageIdentifier(in:index:) String?
|
||
}
|
||
|
||
class RDReaderFlowLayout {
|
||
+displayType: DisplayType
|
||
+isLandscapeDualPage: Bool
|
||
+coverPageIndex: Int?
|
||
+pagesPerScreen: Int
|
||
+currentPage: Int
|
||
}
|
||
|
||
class RDReaderPreloadController {
|
||
+radius: Int
|
||
+pageViewForDisplay(pageNum:environment:contentViewProvider:) UIView?
|
||
+takePreloadedView(for:) UIView?
|
||
+prime(around:preferredForward:parentView:environment:contentViewProvider:)
|
||
+invalidate(environment:)
|
||
}
|
||
|
||
class RDReaderSpreadResolver {
|
||
+isFullScreenPage(...) Bool
|
||
+dualPagePair(for:totalPages:coverPageIndex:) (Int, Int?)
|
||
+nextPage(from:totalPages:pagesPerScreen:coverPageIndex:forward:) Int?
|
||
}
|
||
|
||
class RDReaderTapRegionHandler {
|
||
+resolveTapEvent(point:viewFrame:isToolViewVisible:) TapEvent
|
||
}
|
||
|
||
class TapEvent {
|
||
<<enum>>
|
||
none
|
||
left
|
||
center
|
||
right
|
||
}
|
||
|
||
class RDReaderPagingController {
|
||
+isTransitioning: Bool
|
||
+didBuildUI: Bool
|
||
+pendingTransitionRequest: PageTransitionRequest?
|
||
+shouldQueuePageTransition(_:currentDisplayType:) Bool
|
||
+finishPageCurlTransition() PageTransitionRequest?
|
||
}
|
||
|
||
class RDReaderContentCell {
|
||
+containerView: UIView?
|
||
}
|
||
|
||
class RDReaderPageChildViewController {
|
||
+contentView: UIView?
|
||
+pageNum: Int
|
||
}
|
||
|
||
RDReaderView --> DisplayType : uses
|
||
RDReaderView --> RDReaderPageProvider : delegates
|
||
RDReaderView --> RDReaderFlowLayout : owns
|
||
RDReaderView --> RDReaderPreloadController : owns
|
||
RDReaderView --> RDReaderSpreadResolver : owns
|
||
RDReaderView --> RDReaderTapRegionHandler : owns
|
||
RDReaderView --> RDReaderPagingController : owns
|
||
RDReaderView --> RDReaderContentCell : creates
|
||
RDReaderView --> RDReaderPageChildViewController : creates
|
||
RDReaderTapRegionHandler --> TapEvent : produces
|
||
```
|
||
|
||
---
|
||
|
||
## 5. EPUBUI 控制器与运行时类图
|
||
|
||
```mermaid
|
||
classDiagram
|
||
class RDEPUBReaderController {
|
||
+delegate: RDEPUBReaderDelegate?
|
||
+configuration: RDEPUBReaderConfiguration
|
||
+currentLocation: RDEPUBLocation?
|
||
+currentPageNumber: Int?
|
||
+currentSelection: RDEPUBSelection?
|
||
+highlights: [RDEPUBHighlight]
|
||
+bookmarks: [RDEPUBBookmark]
|
||
+tableOfContents: [EPUBTableOfContentsItem]
|
||
+readerView: RDReaderView
|
||
+readerContext: RDEPUBReaderContext
|
||
+runtime: RDEPUBReaderRuntime?
|
||
}
|
||
|
||
class RDEPUBReaderContext {
|
||
+parser: RDEPUBParser?
|
||
+publication: RDEPUBPublication?
|
||
+readingSession: RDEPUBReadingSession?
|
||
+textBook: RDEPUBTextBook?
|
||
+bookPageMap: RDEPUBBookPageMap?
|
||
+configuration: RDEPUBReaderConfiguration
|
||
+persistence: RDEPUBReaderPersistence?
|
||
+currentRenderSignature() String
|
||
+chapterCacheKey(forSpineIndex:) RDEPUBChapterCacheKey
|
||
+chapterCacheKey(forSpineIndex:precomputedContentHash:renderSignature:) RDEPUBChapterCacheKey
|
||
+makeTextBookBuilder(layoutConfig:) RDEPUBTextBookBuilder
|
||
}
|
||
|
||
class RDEPUBReaderRuntime {
|
||
+chapterLoader: RDEPUBChapterLoader
|
||
+chapterRuntimeStore: RDEPUBChapterRuntimeStore
|
||
+summaryDiskCache: RDEPUBChapterSummaryDiskCache
|
||
+pageResolver: RDEPUBPageResolver
|
||
+loadCoordinator: RDEPUBReaderLoadCoordinator
|
||
+paginationCoordinator: RDEPUBReaderPaginationCoordinator
|
||
+locationCoordinator: RDEPUBReaderLocationCoordinator
|
||
+searchCoordinator: RDEPUBReaderSearchCoordinator
|
||
+chromeCoordinator: RDEPUBReaderChromeCoordinator
|
||
+annotationCoordinator: RDEPUBReaderAnnotationCoordinator
|
||
+viewportMonitor: RDEPUBReaderViewportMonitor
|
||
+applyBookPageMap(_:restoreLocation:)
|
||
+refreshBookPageMapInPlace(_:)
|
||
}
|
||
|
||
class RDEPUBReaderPaginationCoordinator {
|
||
-context: RDEPUBReaderContext
|
||
+paginatePublication(restoreLocation:)
|
||
+repaginatePreservingCurrentLocation()
|
||
}
|
||
|
||
class RDEPUBMetadataParseWorker {
|
||
-context: RDEPUBReaderContext
|
||
-cancellationController: RDEPUBMetadataParseCancellationController
|
||
+start(token:restoreLocation:)
|
||
}
|
||
|
||
class RDEPUBMetadataParseCancellationController {
|
||
+token: UUID
|
||
+attach(queue:)
|
||
+cancel()
|
||
+isCancelled: Bool
|
||
}
|
||
|
||
class RDEPUBReaderLoadCoordinator {
|
||
-context: RDEPUBReaderContext
|
||
+startInitialLoadIfNeeded()
|
||
-loadPublication()
|
||
-applyParsedPublication()
|
||
}
|
||
|
||
class RDEPUBReaderSearchCoordinator {
|
||
-context: RDEPUBReaderContext
|
||
+search(keyword:)
|
||
+goToNextMatch()
|
||
+goToPreviousMatch()
|
||
+clearSearch()
|
||
}
|
||
|
||
class RDEPUBReaderAnnotationCoordinator {
|
||
-context: RDEPUBReaderContext
|
||
+addHighlight(from:)
|
||
+removeHighlight(id:)
|
||
+toggleBookmark(at:)
|
||
+handleSelectionMenuAction(_:)
|
||
}
|
||
|
||
class RDEPUBChapterLoader {
|
||
-context: RDEPUBReaderContext
|
||
+loadChapter(spineIndex:store:) async throws RDEPUBRuntimeChapter
|
||
+loadChapterSynchronouslyForMigration(spineIndex:store:) throws RDEPUBRuntimeChapter
|
||
}
|
||
|
||
class RDEPUBChapterRuntimeStore {
|
||
+chapterDataCache: [Int: RDEPUBRuntimeChapter]
|
||
+pageCountCache: RDEPUBPageCountCache
|
||
+imageCache: NSCache
|
||
+setCurrentChapter(spineIndex:totalSpineCount:windowRadius:)
|
||
+chapterData(for:) RDEPUBRuntimeChapter?
|
||
+pageCount(for:) RDEPUBRuntimePageCount?
|
||
}
|
||
|
||
class RDEPUBChapterSummaryDiskCache {
|
||
+cacheDirectory: URL
|
||
+read(for:) RDEPUBChapterSummary?
|
||
+readAll(keys:) ([Int: RDEPUBChapterSummary], RDEPUBBookPageMap.Builder)
|
||
+write(summary:for:)
|
||
+flushPendingWrites()
|
||
+isCacheComplete(keys:) Bool
|
||
}
|
||
|
||
class RDEPUBBookPageMap {
|
||
+totalChapters: Int
|
||
+totalPages: Int
|
||
+chapters: [EPUBChapterInfo]
|
||
+pages: [EPUBPage]
|
||
}
|
||
|
||
class RDEPUBChapterCacheKey {
|
||
+bookID: String
|
||
+spineIndex: Int
|
||
+renderSignature: String
|
||
+chapterContentHash: String
|
||
}
|
||
|
||
class RDEPUBReaderConfiguration {
|
||
+fontSize: CGFloat
|
||
+fontChoice: FontChoice
|
||
+lineHeightMultiple: CGFloat
|
||
+numberOfColumns: Int
|
||
+theme: ReaderTheme
|
||
+displayType: DisplayType
|
||
+onDemandChapterWindowSize: Int
|
||
+metadataParsingConcurrency: Int
|
||
}
|
||
|
||
RDEPUBReaderController --> RDEPUBReaderContext : owns
|
||
RDEPUBReaderController --> RDEPUBReaderRuntime : owns
|
||
RDEPUBReaderController --> RDEPUBReaderPaginationCoordinator : owns
|
||
RDEPUBReaderContext --> RDEPUBReaderConfiguration : holds
|
||
RDEPUBReaderRuntime --> RDEPUBChapterLoader : owns
|
||
RDEPUBReaderRuntime --> RDEPUBChapterRuntimeStore : owns
|
||
RDEPUBReaderRuntime --> RDEPUBChapterSummaryDiskCache : owns
|
||
RDEPUBReaderRuntime --> RDEPUBReaderLoadCoordinator : owns
|
||
RDEPUBReaderRuntime --> RDEPUBReaderSearchCoordinator : owns
|
||
RDEPUBReaderRuntime --> RDEPUBReaderAnnotationCoordinator : owns
|
||
RDEPUBReaderPaginationCoordinator --> RDEPUBReaderContext : uses
|
||
RDEPUBChapterLoader --> RDEPUBReaderContext : uses
|
||
RDEPUBChapterLoader --> RDEPUBChapterRuntimeStore : uses
|
||
RDEPUBChapterLoader --> RDEPUBChapterSummaryDiskCache : uses
|
||
RDEPUBChapterSummaryDiskCache --> RDEPUBChapterCacheKey : uses
|
||
RDEPUBBookPageMap --> EPUBChapterInfo : contains
|
||
RDEPUBBookPageMap --> EPUBPage : contains
|
||
```
|
||
|
||
---
|
||
|
||
## 6. 数据模型关系图
|
||
|
||
```mermaid
|
||
classDiagram
|
||
class RDEPUBLocation {
|
||
+bookIdentifier: String?
|
||
+href: String
|
||
+progression: Double
|
||
+lastProgression: Double?
|
||
+fragment: String?
|
||
+rangeAnchor: RDEPUBTextRangeAnchor?
|
||
}
|
||
|
||
class RDEPUBTextAnchor {
|
||
+fileIndex: Int
|
||
+row: Int
|
||
+column: Int
|
||
+chapterOffset: Int
|
||
+fragmentID: String?
|
||
+spineIndex: Int
|
||
}
|
||
|
||
class RDEPUBTextRangeAnchor {
|
||
+start: RDEPUBTextAnchor
|
||
+end: RDEPUBTextAnchor
|
||
+nsRange: NSRange
|
||
}
|
||
|
||
class RDEPUBSelection {
|
||
+bookIdentifier: String?
|
||
+location: RDEPUBLocation
|
||
+text: String
|
||
+rangeInfo: String?
|
||
+createdAt: Date
|
||
}
|
||
|
||
class RDEPUBHighlight {
|
||
+id: String
|
||
+bookIdentifier: String?
|
||
+location: RDEPUBLocation
|
||
+text: String
|
||
+style: RDEPUBHighlightStyle
|
||
+color: String
|
||
+note: String?
|
||
+createdAt: Date
|
||
+uiColor: UIColor
|
||
}
|
||
|
||
class RDEPUBBookmark {
|
||
+id: String
|
||
+bookIdentifier: String?
|
||
+location: RDEPUBLocation
|
||
+chapterTitle: String?
|
||
+note: String?
|
||
+createdAt: Date
|
||
}
|
||
|
||
class RDEPUBAnnotation {
|
||
+id: String
|
||
+bookIdentifier: String?
|
||
+kind: RDEPUBAnnotationKind
|
||
+location: RDEPUBLocation
|
||
+text: String?
|
||
+rangeInfo: String?
|
||
+chapterTitle: String?
|
||
+createdAt: Date
|
||
+bookmark: RDEPUBBookmark?
|
||
+highlight: RDEPUBHighlight?
|
||
}
|
||
|
||
class EPUBPage {
|
||
+spineIndex: Int
|
||
+chapterIndex: Int
|
||
+pageIndexInChapter: Int
|
||
+totalPagesInChapter: Int
|
||
+chapterTitle: String
|
||
+fixedSpread: EPUBFixedSpread?
|
||
}
|
||
|
||
class EPUBChapterInfo {
|
||
+spineIndex: Int
|
||
+title: String
|
||
+pageCount: Int
|
||
}
|
||
|
||
class RDEPUBViewport {
|
||
+resources: [RDEPUBViewportResource]
|
||
+visiblePageNumber: Int
|
||
+chapterIndex: Int?
|
||
+isFixedLayout: Bool
|
||
}
|
||
|
||
class RDEPUBReadingContext {
|
||
+location: RDEPUBLocation
|
||
+viewport: RDEPUBViewport
|
||
+pageNumber: Int
|
||
+chapterIndex: Int?
|
||
}
|
||
|
||
RDEPUBLocation --> RDEPUBTextRangeAnchor : optional
|
||
RDEPUBTextRangeAnchor --> RDEPUBTextAnchor : start/end
|
||
RDEPUBSelection --> RDEPUBLocation : references
|
||
RDEPUBHighlight --> RDEPUBLocation : references
|
||
RDEPUBBookmark --> RDEPUBLocation : references
|
||
RDEPUBAnnotation --> RDEPUBLocation : references
|
||
RDEPUBReadingContext --> RDEPUBLocation : holds
|
||
RDEPUBReadingContext --> RDEPUBViewport : holds
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 后台解析并发模型
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Main as Main Thread
|
||
participant BG as Background Queue
|
||
participant OQ as OperationQueue (N workers)
|
||
participant Disk as Disk Cache
|
||
|
||
Main->>BG: RDEPUBMetadataParseWorker.start(token)
|
||
BG->>BG: 预计算 contentHash (串行)
|
||
BG->>Disk: readAll(catalog) 批量读取缓存
|
||
BG->>Main: refreshBookPageMapInPlace (缓存部分)
|
||
BG->>BG: waitForReadingInteractionToSettle (0.8s)
|
||
|
||
loop 每个未缓存章节
|
||
BG->>OQ: addOperation(spineIndex)
|
||
OQ->>OQ: buildChapter (渲染+分页)
|
||
OQ->>OQ: chapterCacheKey (复用预计算hash)
|
||
OQ->>Disk: write(summary) 异步写盘
|
||
OQ->>OQ: resultLock: 累加结果
|
||
alt 每32章或最后一章
|
||
OQ->>Main: refreshBookPageMapInPlace
|
||
end
|
||
end
|
||
|
||
OQ->>BG: waitUntilAllOperationsAreFinished
|
||
BG->>Disk: flushPendingWrites
|
||
BG->>Main: refreshBookPageMapInPlace (最终)
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 缓存键与失效策略
|
||
|
||
```mermaid
|
||
graph LR
|
||
subgraph CacheKey["RDEPUBChapterCacheKey"]
|
||
A[bookID]
|
||
B[spineIndex]
|
||
C[renderSignature]
|
||
D[chapterContentHash]
|
||
end
|
||
|
||
subgraph RenderSignature["renderSignature 组成"]
|
||
E[fontName]
|
||
F[fontSize]
|
||
G[lineHeightMultiple]
|
||
H[lineSpacing]
|
||
I[layoutConfig.cacheSignature]
|
||
J[schemaVersion]
|
||
end
|
||
|
||
subgraph CacheSignature["layoutConfig.cacheSignature"]
|
||
K[frameWidth]
|
||
L[frameHeight]
|
||
M[edgeInsets]
|
||
N[numberOfColumns]
|
||
O[columnGap]
|
||
P[avoidOrphans/Widows]
|
||
Q[hyphenation]
|
||
R[imageMaxHeightRatio]
|
||
end
|
||
|
||
C --> E & F & G & H & I & J
|
||
I --> K & L & M & N & O & P & Q & R
|
||
|
||
style CacheKey fill:#e1f5fe
|
||
style RenderSignature fill:#fff3e0
|
||
style CacheSignature fill:#e8f5e9
|
||
```
|