- Add RDEPUBReaderSearchBarView with animated show/hide, keyword navigation, and match counting integrated into the reader controller - Restructure docs: replace scattered design docs with consolidated BUSINESS_LOGIC.md and UML_CLASS_DIAGRAMS.md; update ARCHITECTURE.md - Add SearchTests and FanrenParseTimeTest; enhance LargeBookOnDemandTests - Add scripts/run_ui_regression.sh and summarize_ui_results.py for automated UI test execution and reporting Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
20 KiB
20 KiB
ReadViewSDK UML 类图
最后更新:2026-06-04 图表格式:Mermaid(可在 GitHub / VS Code / Mermaid Live Editor 中渲染)
1. 整体模块关系
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 核心类图
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 类图
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 类图
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 控制器与运行时类图
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
+viewportMonitor: RDEPUBViewportMonitor
+applyBookPageMap(_:restoreLocation:)
+refreshBookPageMapInPlace(_:)
}
class RDEPUBReaderPaginationCoordinator {
-context: RDEPUBReaderContext
+paginatePublication(restoreLocation:)
+paginateMetadataOnly(token:restoreLocation:)
+repaginatePreservingCurrentLocation()
-restoreBookPageMapIfPossible(publication:) RDEPUBBookPageMap?
-buildPageMap(from:summaries:) RDEPUBBookPageMap
}
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
RDEPUBReaderPaginationCoordinator --> RDEPUBReaderContext : uses
RDEPUBChapterLoader --> RDEPUBReaderContext : uses
RDEPUBChapterLoader --> RDEPUBChapterRuntimeStore : uses
RDEPUBChapterLoader --> RDEPUBChapterSummaryDiskCache : uses
RDEPUBChapterSummaryDiskCache --> RDEPUBChapterCacheKey : uses
RDEPUBBookPageMap --> EPUBChapterInfo : contains
RDEPUBBookPageMap --> EPUBPage : contains
6. 数据模型关系图
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
+kind: RDEPUBAnnotationKind
+location: RDEPUBLocation
+text: String?
+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. 后台解析并发模型
sequenceDiagram
participant Main as Main Thread
participant BG as Background Queue
participant OQ as OperationQueue (N workers)
participant Disk as Disk Cache
Main->>BG: paginateMetadataOnly(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. 缓存键与失效策略
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