ReadViewSDK/Doc/UML_CLASS_DIAGRAMS.md
shenlei d15f20b097 feat: 交互协调器拆分、附件提示、暗色图片适配、选区放大镜及文档清理
- 拆分 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>
2026-06-24 17:47:24 +08:00

21 KiB
Raw Blame History

ReadViewSDK UML 类图

最后更新2026-06-09 图表格式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
        +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. 数据模型关系图

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. 后台解析并发模型

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. 缓存键与失效策略

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