Parcourir la source

feat: 性能之优化

HapeLee il y a 1 mois
Parent
commit
d0f0f4a397
22 fichiers modifiés avec 575 ajouts et 814 suppressions
  1. 11 2
      app/src/debug/java/io/legado/app/debug/DebugScenarioActivity.kt
  2. 558 231
      app/src/main/java/io/legado/app/feature/reader/ReaderCanvasSurface.kt
  3. 0 17
      app/src/main/java/io/legado/app/help/PaintPool.kt
  4. 5 7
      app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt
  5. 0 3
      app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigScreen.kt
  6. 1 51
      app/src/main/java/io/legado/app/utils/ViewExtensions.kt
  7. 0 36
      app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt
  8. 0 27
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt
  9. 0 47
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt
  10. 0 76
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt
  11. 0 44
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt
  12. 0 31
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt
  13. 0 72
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt
  14. 0 64
      app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt
  15. 0 21
      app/src/main/java/io/legado/app/utils/canvasrecorder/pools/CanvasPool.kt
  16. 0 10
      app/src/main/java/io/legado/app/utils/canvasrecorder/pools/PicturePool.kt
  17. 0 18
      app/src/main/java/io/legado/app/utils/canvasrecorder/pools/RenderNodePool.kt
  18. 0 19
      app/src/main/java/io/legado/app/utils/objectpool/BaseObjectPool.kt
  19. 0 9
      app/src/main/java/io/legado/app/utils/objectpool/BaseSafeObjectPool.kt
  20. 0 11
      app/src/main/java/io/legado/app/utils/objectpool/ObjectPool.kt
  21. 0 3
      app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolExtensions.kt
  22. 0 15
      app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolLocked.kt

+ 11 - 2
app/src/debug/java/io/legado/app/debug/DebugScenarioActivity.kt

@@ -90,9 +90,17 @@ class DebugScenarioActivity : AppCompatActivity() {
         require(fixture.chapters.isNotEmpty()) { "fixture must contain chapters" }
         require(fixture.book.startChapter in fixture.chapters.indices) { "invalid start chapter" }
 
-        val pageAnim = when (fixture.book.pageMode) {
+        // 翻页模式可由 intent 覆写:性能/动画场景需要在同一个夹具上切换 cover/slide/fade/…,
+        // 否则只能靠阅读设置里的下拉菜单手工切,脚本无法稳定复现。
+        val pageMode = intent.getStringExtra(EXTRA_PAGE_MODE) ?: fixture.book.pageMode
+        val pageAnim = when (pageMode) {
+            "cover" -> PageAnim.coverPageAnim
+            "slide" -> PageAnim.slidePageAnim
             "simulation" -> PageAnim.simulationPageAnim
-            else -> error("unsupported page mode: ${fixture.book.pageMode}")
+            "scroll" -> PageAnim.scrollPageAnim
+            "fade" -> PageAnim.fadePageAnim
+            "none" -> PageAnim.noAnim
+            else -> error("unsupported page mode: $pageMode")
         }
         val chapters = fixture.chapters.mapIndexed { index, chapter ->
             require(chapter.repeat in 1..1000) { "invalid repeat for chapter $index" }
@@ -230,6 +238,7 @@ class DebugScenarioActivity : AppCompatActivity() {
         const val EXTRA_FIXTURE = "fixture"
         const val EXTRA_SESSION = "session"
         const val EXTRA_ENTRY = "entry"
+        const val EXTRA_PAGE_MODE = "pageMode"
         const val EXTRA_PREFERENCES_B64 = "preferencesB64"
         const val LOG_TAG = "LegadoDebug"
         const val DEBUG_ORIGIN = "legado-debug://fixture-source"

+ 558 - 231
app/src/main/java/io/legado/app/feature/reader/ReaderCanvasSurface.kt

@@ -49,6 +49,7 @@ import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.ClipOp
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.StrokeCap
@@ -59,6 +60,7 @@ import androidx.compose.ui.graphics.drawscope.clipRect
 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.graphics.drawscope.withTransform
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.graphics.layer.drawLayer
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.graphics.rememberGraphicsLayer
@@ -144,6 +146,7 @@ import io.legado.app.feature.reader.core.transition.ReaderScrollPolicy
 import io.legado.app.feature.reader.core.transition.ReaderScrollResult
 import io.legado.app.feature.reader.core.transition.ReaderTransitionDecision
 import io.legado.app.feature.reader.core.transition.ReaderTransitionMode
+import io.legado.app.feature.reader.core.transition.ReaderTransitionTransforms
 import io.legado.app.feature.reader.core.transition.ReaderTurnDirection
 import io.legado.app.feature.reader.core.transition.ReaderViewportLayerPolicy
 import io.legado.app.feature.reader.core.transition.transforms
@@ -255,6 +258,19 @@ fun ReaderCanvasSurface(
     val scrollDecay = rememberSplineBasedDecay<Float>()
     var displayOffset by remember { mutableFloatStateOf(0f) }
     var transition by remember { mutableStateOf(ReaderPageTransition()) }
+    // 换页分支的判定只依赖低频状态:`transition` 每帧都会因 `offsetPx` 变化而失效,
+    // 在组合期读它会让整个画布每帧重组。`turnDirection` 只在一次换页开始/结束(或模式切换)
+    // 时改变;位移、透明度、折页几何一律在绘制期/layer 期读取。
+    var turnDirection by remember { mutableStateOf<ReaderTurnDirection?>(null) }
+
+    /**
+     * [transition] 的唯一写入口,顺带维护低频的 [turnDirection]。
+     * 不要把 `transition` 读到组合期,否则拖拽的每一帧都会重组整个画布。
+     */
+    fun applyTransition(next: ReaderPageTransition) {
+        transition = next
+        turnDirection = next.direction?.takeIf { next.dragging }
+    }
     var pageMotionJob by remember { mutableStateOf<Job?>(null) }
     // The first curl frame starts at a corner. Animate it into the safe fold position so
     // the entering page is revealed instead of popping in when horizontal capture begins.
@@ -309,7 +325,7 @@ fun ReaderCanvasSurface(
             pendingTurn = null
             pendingTurnOrigin = null
             displayOffset = 0f
-            transition = ReaderPageTransition()
+            applyTransition(ReaderPageTransition())
             bookmarkReturnJob?.cancel()
             curlRevealJob?.cancel()
             bookmarkOffset = 0f
@@ -327,7 +343,16 @@ fun ReaderCanvasSurface(
     }
     var curlTouchY by remember { mutableFloatStateOf(1f) }
     var curlTouchX by remember { mutableFloatStateOf(0f) }
-    var curlCornerY by remember { mutableStateOf(0f) }
+    var curlCornerY by remember { mutableFloatStateOf(0f) }
+    // 快照层与位图池都在会话级持有、跨回合复用,对照旧 View `CanvasRecorderFactory` 的对象池与
+    // `curBitmap/prevBitmap/nextBitmap`:不能再像以前那样每个回合 `rememberGraphicsLayer()` 新建。
+    val pageSnapshots = remember { PageSnapshotBitmapPool() }
+    val baseSnapshotLayer = rememberGraphicsLayer()
+    val revealSnapshotLayer = rememberGraphicsLayer()
+    val autoPageSnapshotLayer = rememberGraphicsLayer()
+    DisposableEffect(Unit) {
+        onDispose { pageSnapshots.clear() }
+    }
     // 滚动偏移只在 graphicsLayer 块(layer 属性期)读取:拖拽/fling 帧只更新层变换、
     // 零重组零重绘(对照 shutiao 的 contentOffset 语义)。
     val scrollOffsetState = remember { mutableFloatStateOf(0f) }
@@ -384,7 +409,8 @@ fun ReaderCanvasSurface(
             .distinctBy { element -> element.source to element.bounds }
             .forEach { element -> launch { prefetchSemaphore.withPermit { loadImage(element) } } }
     }
-    val transforms = transition.copy(offsetPx = displayOffset).transforms(transitionMode)
+    // `transforms` 只在非滚动/非折页分支使用,且读了每帧变化的 `transition`/`displayOffset`;
+    // 在这里(组合期)求值会让滚动惯性、封面滑动、仿真折页的每一帧都重组整个画布。
     fun pageViewportLayout(window: ReaderPageWindow = currentPageWindow()): ReaderPageViewportLayout =
         if (transitionMode == ReaderTransitionMode.SCROLL) {
             ReaderPageViewportLayout.scroll(window, scrollOffset)
@@ -472,7 +498,7 @@ fun ReaderCanvasSurface(
         if (transitionMode == ReaderTransitionMode.NONE) {
             completePendingTurn()
             displayOffset = 0f
-            transition = ReaderPageTransition()
+            applyTransition(ReaderPageTransition())
             return
         }
         val targetCurlX = transition.direction?.let {
@@ -497,7 +523,7 @@ fun ReaderCanvasSurface(
             displayOffset = decision.targetOffsetPx
             completePendingTurn()
             displayOffset = 0f
-            transition = ReaderPageTransition()
+            applyTransition(ReaderPageTransition())
             return
         }
         val startOffset = displayOffset
@@ -529,7 +555,7 @@ fun ReaderCanvasSurface(
             }
             completePendingTurn()
             displayOffset = 0f
-            transition = ReaderPageTransition()
+            applyTransition(ReaderPageTransition())
         }
     }
     fun tapPageTurn(direction: ReaderTurnDirection) {
@@ -553,7 +579,7 @@ fun ReaderCanvasSurface(
             )
         }
         val target = if (direction == ReaderTurnDirection.PREVIOUS) width else -width
-        transition = ReaderPageTransition(direction, 0f, width, dragging = true)
+        applyTransition(ReaderPageTransition(direction, 0f, width, dragging = true))
         displayOffset = 0f
         settlePageTurn(ReaderTransitionDecision(target, commit = true))
     }
@@ -712,7 +738,7 @@ fun ReaderCanvasSurface(
             }
             clearSelectionForPageChange(ReaderPageChangeOrigin.PROGRAMMATIC)
             displayOffset = 0f
-            transition = ReaderPageTransition()
+            applyTransition(ReaderPageTransition())
             dispatchTapAction(
                 if (direction == ReaderTurnDirection.PREVIOUS) {
                     ReaderTapAction.PREVIOUS_PAGE
@@ -762,7 +788,7 @@ fun ReaderCanvasSurface(
             pendingTurn = null
             pendingTurnOrigin = null
             displayOffset = 0f
-            transition = ReaderPageTransition()
+            applyTransition(ReaderPageTransition())
         }
     }
     LaunchedEffect(current.layoutRevision) {
@@ -924,7 +950,7 @@ fun ReaderCanvasSurface(
                 // 手势必须改用返回的窗口命中,否则长按会选中已不在屏幕上的旧页。
                 val turnedWindow = completePendingTurn()
                 displayOffset = 0f
-                transition = ReaderPageTransition()
+                applyTransition(ReaderPageTransition())
                 bookmarkReturnJob?.cancel()
                 bookmarkOffset = 0f
                 bookmarkArmed = false
@@ -1231,17 +1257,19 @@ fun ReaderCanvasSurface(
                             }
                         }
                         if (horizontalTurn && transitionMode != ReaderTransitionMode.SCROLL) {
-                            transition = horizontalDrag?.transition(
-                                total.x, size.width.toFloat(),
-                                ReaderPageNavigator.canTurnPrevious(
-                                    latestPages,
-                                    latestHasPreviousChapter(),
-                                ),
-                                ReaderPageNavigator.canTurnNext(
-                                    latestPages,
-                                    latestHasNextChapter(),
-                                ),
-                            ) ?: ReaderPageTransition(pageExtentPx = size.width.toFloat())
+                            applyTransition(
+                                horizontalDrag?.transition(
+                                    total.x, size.width.toFloat(),
+                                    ReaderPageNavigator.canTurnPrevious(
+                                        latestPages,
+                                        latestHasPreviousChapter(),
+                                    ),
+                                    ReaderPageNavigator.canTurnNext(
+                                        latestPages,
+                                        latestHasNextChapter(),
+                                    ),
+                                ) ?: ReaderPageTransition(pageExtentPx = size.width.toFloat())
+                            )
                             transition.direction?.takeIf { transitionMode == ReaderTransitionMode.SIMULATION }
                                 ?.let {
                                     curlTouchX = ReaderCurlTouchPolicy.dragX(
@@ -1304,7 +1332,7 @@ fun ReaderCanvasSurface(
                             settlePageTurn(ReaderTransitionDecision(0f, commit = false))
                         } else {
                             displayOffset = 0f
-                            transition = ReaderPageTransition()
+                            applyTransition(ReaderPageTransition())
                         }
                     }
                 }
@@ -1472,39 +1500,60 @@ fun ReaderCanvasSurface(
                     loadImage = loadImage,
                 )
             }
-        } else if (transitionMode == ReaderTransitionMode.SIMULATION && transition.dragging) {
+        } else if (transitionMode == ReaderTransitionMode.SIMULATION && turnDirection != null) {
             SimulationPageStack(
-                pages,
-                transition.direction!!,
-                displayOffset,
-                ReaderCurlTouchPolicy.revealX(
-                    transition.direction!!,
-                    curlTouchX,
-                    current.widthPx.toFloat(),
-                    curlRevealProgress
-                ),
-                curlTouchY,
-                curlCornerY,
-                backgroundColor,
-                pageBackgroundImage,
-                backgroundImageAlpha,
-                selectionColor,
-                textAccentColor,
-                textSelection,
-                selectionPreviewStyle,
-                cachedImage,
-                loadImage
+                pages = pages,
+                direction = turnDirection!!,
+                pageExtentPx = current.widthPx.toFloat(),
+                pageOffsetPx = { displayOffset },
+                touchX = { curlTouchX },
+                touchY = { curlTouchY },
+                cornerY = { curlCornerY },
+                revealProgress = { curlRevealProgress },
+                snapshots = pageSnapshots,
+                baseLayer = baseSnapshotLayer,
+                revealLayer = revealSnapshotLayer,
+                background = backgroundColor,
+                backgroundImage = pageBackgroundImage,
+                backgroundImageAlpha = backgroundImageAlpha,
+                selection = selectionColor,
+                readAloud = textAccentColor,
+                activeSelection = textSelection,
+                selectionPreviewStyle = selectionPreviewStyle,
+                cachedImage = cachedImage,
+                loadImage = loadImage
             )
         } else {
+            // Cover/Slide/Fade/无动画:本回合画哪几页只由低频的 [turnDirection] 决定,
+            // 位移与透明度在 layer 块(layer 属性期)现读,因此拖拽与收尾帧**零重组**。
+            // 页内容由 `graphicsLayer` 自带的 RenderNode 缓存,只有页内容变化才会重录——
+            // 等价于旧 View 的 Cover/Slide/Fade delegate:手势开始 `setBitmap()` 截一次,
+            // 之后每帧只 `withTranslation` / `withClip` / `setAlpha` 画 recorder。
+            val turnPreview = ReaderPageTransition(
+                direction = turnDirection,
+                offsetPx = 0f,
+                pageExtentPx = 1f,
+                dragging = turnDirection != null,
+            ).transforms(transitionMode)
             @Composable
-            fun PageLayer(page: ReaderPage, transform: ReaderPageTransform, offsetY: Float = 0f) {
+            fun PageLayer(page: ReaderPage, role: PagedLayerRole, offsetY: () -> Float) {
                 ReaderPageCanvas(
                     page, backgroundColor, pageBackgroundImage, backgroundImageAlpha, selectionColor, textAccentColor,
                     Modifier
                         .fillMaxSize()
                         .graphicsLayer {
+                            val transform = transition
+                                .copy(offsetPx = displayOffset)
+                                .transforms(transitionMode)
+                                .forRole(role)
+                            if (transform == null) {
+                                // 与本回合方向不一致(理论上被上面的预览挡住):显式隐藏,
+                                // 避免残留上一次的位移把邻页留在屏幕上。
+                                alpha = 0f
+                                return@graphicsLayer
+                            }
                             translationX = transform.translationX
-                            translationY = offsetY + transform.translationY
+                            translationY = offsetY() + transform.translationY
                             alpha = transform.alpha
                         },
                     textSelection,
@@ -1513,19 +1562,28 @@ fun ReaderCanvasSurface(
                     loadImage,
                 )
             }
-            if (transforms.currentOnTop) {
-                transforms.next?.let { t -> pages.next?.let { PageLayer(it, t) } }
+            // 只借 `transforms()` 判断"本回合哪些页参与绘制"与绘制顺序;位移/透明度不用它的值,
+            // 因为那些值必须留到 layer 期现读。
+            val currentOnTop = turnPreview.currentOnTop
+            val drawsNext = turnPreview.next != null
+            val drawsPrevious = turnPreview.previous != null
+            if (currentOnTop && drawsNext) {
+                pages.next?.let { page -> PageLayer(page, PagedLayerRole.NEXT) { 0f } }
+            }
+            PageLayer(current, PagedLayerRole.CURRENT) { bookmarkOffset }
+            if (drawsPrevious) {
+                pages.previous?.let { page -> PageLayer(page, PagedLayerRole.PREVIOUS) { 0f } }
             }
-            PageLayer(current, transforms.current, bookmarkOffset)
-            transforms.previous?.let { t -> pages.previous?.let { PageLayer(it, t) } }
-            if (!transforms.currentOnTop) {
-                transforms.next?.let { t -> pages.next?.let { PageLayer(it, t) } }
+            if (!currentOnTop && drawsNext) {
+                pages.next?.let { page -> PageLayer(page, PagedLayerRole.NEXT) { 0f } }
             }
         }
-        if (transitionMode == ReaderTransitionMode.COVER && transition.direction != null && displayOffset != 0f) {
+        if (transitionMode == ReaderTransitionMode.COVER && turnDirection != null) {
             Canvas(Modifier.fillMaxSize()) {
                 val direction = transition.direction ?: return@Canvas
-                val edge = ReaderCoverShadowPolicy.edgePx(direction, displayOffset, size.width)
+                val offsetPx = displayOffset
+                if (offsetPx == 0f) return@Canvas
+                val edge = ReaderCoverShadowPolicy.edgePx(direction, offsetPx, size.width)
                 val shadowWidth = ReaderCoverShadowPolicy.widthDp * density
                 val dark = Color(ReaderCoverShadowPolicy.colorArgb)
                 drawRect(
@@ -1539,13 +1597,13 @@ fun ReaderCanvasSurface(
                 )
             }
         }
-        if (transitionMode != ReaderTransitionMode.SCROLL && autoPageActive && autoRevealPx > 0f) {
+        if (transitionMode != ReaderTransitionMode.SCROLL && autoPageActive) {
+            // 自动翻页的"下一页揭示",对照旧 `AutoPager.onDraw`:
+            // `canvasRecorder.recordIfNeeded(readView.nextPage)` 只录一次,之后每帧只有
+            // `withClip` + `draw` + 1px 指示线。这里把下一页录进快照层,`autoRevealPx`
+            // 只在绘制期读,因此整段自动阅读期间零重组、零重录(页码变化时才重录一次)。
             pages.next?.let { page ->
-                Box(Modifier
-                    .fillMaxSize()
-                    .drawWithContent {
-                        clipRect(bottom = autoRevealPx.coerceAtMost(size.height)) { this@drawWithContent.drawContent() }
-                    }) {
+                PageSnapshotLayer(autoPageSnapshotLayer) {
                     ReaderPageCanvas(
                         page,
                         backgroundColor,
@@ -1560,16 +1618,23 @@ fun ReaderCanvasSurface(
                         loadImage
                     )
                 }
-            }
-            Canvas(Modifier.fillMaxSize()) {
-                drawRect(
-                    color = autoPageIndicatorColor,
-                    topLeft = Offset(
-                        0f,
-                        ReaderAutoPagePolicy.indicatorTopPx(autoRevealPx, size.height),
-                    ),
-                    size = Size(size.width, 1f),
-                )
+                Canvas(Modifier.fillMaxSize()) {
+                    val reveal = autoRevealPx
+                    if (reveal <= 0f) return@Canvas
+                    clipRect(bottom = reveal.coerceAtMost(size.height)) {
+                        drawLayer(
+                            autoPageSnapshotLayer
+                        )
+                    }
+                    drawRect(
+                        color = autoPageIndicatorColor,
+                        topLeft = Offset(
+                            0f,
+                            ReaderAutoPagePolicy.indicatorTopPx(reveal, size.height),
+                        ),
+                        size = Size(size.width, 1f),
+                    )
+                }
             }
         }
         if (ReaderViewportLayerPolicy.usesFixedPageChrome(transitionMode)) {
@@ -1938,14 +2003,29 @@ private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawScrollPageConte
     visibleDecorationCache.overlayRules.forEach { it.draw(native) }
 }
 
+/**
+ * 仿真折页:两页内容各录制一次,拖拽/收尾的每一帧只做裁剪与合成。
+ *
+ * 旧 View 的 `SimulationPageDelegate` 在 `setDirection()` 里对两页各做一次 `screenshot`,
+ * 之后每帧只 `drawBitmap` + `clipPath` + 复用成员的 GradientDrawable。迁移到 Compose 后
+ * 一度变成每帧重新录制整页(`GraphicsLayer.record` 直接放在 `drawWithContent` 里),
+ * 文字、行内图片、背景图每帧重画一遍并多次全屏合成,弱机必然掉帧。这里恢复旧语义。
+ *
+ * 折页触点与几何以 lambda 形式传入,只在绘制期读取:组合期读它们会让整个画布每帧重组。
+ */
 @Composable
 private fun SimulationPageStack(
     pages: ReaderPageWindow,
     direction: ReaderTurnDirection,
-    pageOffsetPx: Float,
-    touchX: Float,
-    touchY: Float,
-    cornerY: Float,
+    pageExtentPx: Float,
+    pageOffsetPx: () -> Float,
+    touchX: () -> Float,
+    touchY: () -> Float,
+    cornerY: () -> Float,
+    revealProgress: () -> Float,
+    snapshots: PageSnapshotBitmapPool,
+    baseLayer: GraphicsLayer,
+    revealLayer: GraphicsLayer,
     background: Color,
     backgroundImage: Drawable?,
     backgroundImageAlpha: Float,
@@ -1959,117 +2039,121 @@ private fun SimulationPageStack(
     val basePage = if (direction == ReaderTurnDirection.NEXT) pages.current else pages.previous
     val revealPage = if (direction == ReaderTurnDirection.NEXT) pages.next else pages.current
     if (basePage == null || revealPage == null) return
+    val renderPaths = remember { ReaderCurlRenderPaths() }
     BoxWithConstraints(Modifier.fillMaxSize()) {
         val density = LocalDensity.current
         val width = with(density) { maxWidth.toPx() }
         val height = with(density) { maxHeight.toPx() }
-        val frame = remember(width, height, touchX, touchY, cornerY) {
-            PageCurlGeometry.calculate(
-                width,
-                height,
-                touchX,
-                touchY.coerceIn(.1f, height - .1f),
-                lockedCorner = CurlPoint(width, cornerY),
+        val widthPx = width.roundToInt().coerceAtLeast(1)
+        val heightPx = height.roundToInt().coerceAtLeast(1)
+        val baseKey = remember(
+            basePage, background, backgroundImage, backgroundImageAlpha,
+            selection, readAloud, activeSelection, selectionPreviewStyle, widthPx, heightPx,
+        ) {
+            pageSnapshotKey(
+                basePage, background, backgroundImage, backgroundImageAlpha,
+                selection, readAloud, activeSelection, selectionPreviewStyle, widthPx, heightPx,
             )
         }
-        if (frame == null) {
-            // A degenerate Bezier frame used to draw only the base page, which made the
-            // entering page disappear for an entire drag frame. Keep the destination visible
-            // with a cheap horizontal fallback until the next valid curl frame arrives.
-            ReaderPageCanvas(
-                revealPage,
-                background,
-                backgroundImage,
-                backgroundImageAlpha,
-                selection,
-                readAloud,
-                Modifier.fillMaxSize(),
-                activeSelection,
-                selectionPreviewStyle,
-                cachedImage,
-                loadImage
+        val revealKey = remember(
+            revealPage, background, backgroundImage, backgroundImageAlpha,
+            selection, readAloud, activeSelection, selectionPreviewStyle, widthPx, heightPx,
+        ) {
+            pageSnapshotKey(
+                revealPage, background, backgroundImage, backgroundImageAlpha,
+                selection, readAloud, activeSelection, selectionPreviewStyle, widthPx, heightPx,
             )
-            val baseTranslation = when (direction) {
-                ReaderTurnDirection.NEXT -> pageOffsetPx
-                ReaderTurnDirection.PREVIOUS -> pageOffsetPx - width
+        }
+        // 命中池子就用位图,连快照层都不用挂(该回合零页面重录);未命中先走图层路径,
+        // 随后补拍 —— 一帧拍一页,把旧 View `setBitmap()` 那种"手势开始连拍两页"的卡顿摊开。
+        var baseBitmap by remember(baseKey) { mutableStateOf(snapshots.get(baseKey)) }
+        var revealBitmap by remember(revealKey) { mutableStateOf(snapshots.get(revealKey)) }
+        val baseSettled = basePage.isSnapshotSettled(cachedImage)
+        val revealSettled = revealPage.isSnapshotSettled(cachedImage)
+        LaunchedEffect(baseKey, revealKey, baseSettled, revealSettled) {
+            withFrameNanos { }
+            if (baseBitmap == null) {
+                snapshots.capture(baseLayer, baseKey, baseSettled)?.let { baseBitmap = it }
+            }
+            withFrameNanos { }
+            if (revealBitmap == null) {
+                snapshots.capture(revealLayer, revealKey, revealSettled)?.let { revealBitmap = it }
             }
-            ReaderPageCanvas(
-                basePage,
-                background,
-                backgroundImage,
-                backgroundImageAlpha,
-                selection,
-                readAloud,
-                Modifier
-                    .fillMaxSize()
-                    .graphicsLayer { translationX = baseTranslation },
-                activeSelection,
-                selectionPreviewStyle,
-                cachedImage,
-                loadImage,
-            )
-            return@BoxWithConstraints
-        }
-        val paths = remember(frame, width, height) { frame.renderPaths(width, height) }
-        val path0 = paths.front
-        val pathNext = paths.reveal
-        val pathBack = paths.back
-        val baseLayer = rememberGraphicsLayer()
-        Box(Modifier
-            .fillMaxSize()
-            .drawWithContent {
-                baseLayer.record { this@drawWithContent.drawContent() }
-                clipPath(path0, ClipOp.Difference) { drawLayer(baseLayer) }
-            }) {
-            ReaderPageCanvas(
-                basePage,
-                background,
-                backgroundImage,
-                backgroundImageAlpha,
-                selection,
-                readAloud,
-                Modifier.fillMaxSize(),
-                activeSelection,
-                selectionPreviewStyle,
-                cachedImage,
-                loadImage
-            )
         }
-        Box(Modifier
-            .fillMaxSize()
-            .drawWithContent {
-                clipPath(path0) {
-                    clipPath(pathNext) {
-                        this@drawWithContent.drawContent()
-                        drawCurlBackShadow(frame)
-                    }
-                }
-            }) {
-            ReaderPageCanvas(
-                revealPage,
-                background,
-                backgroundImage,
-                backgroundImageAlpha,
-                selection,
-                readAloud,
-                Modifier.fillMaxSize(),
-                activeSelection,
-                selectionPreviewStyle,
-                cachedImage,
-                loadImage
-            )
+        if (baseBitmap == null) {
+            PageSnapshotLayer(baseLayer) {
+                ReaderPageCanvas(
+                    basePage,
+                    background,
+                    backgroundImage,
+                    backgroundImageAlpha,
+                    selection,
+                    readAloud,
+                    Modifier.fillMaxSize(),
+                    activeSelection,
+                    selectionPreviewStyle,
+                    cachedImage,
+                    loadImage
+                )
+            }
+        }
+        if (revealBitmap == null) {
+            PageSnapshotLayer(revealLayer) {
+                ReaderPageCanvas(
+                    revealPage,
+                    background,
+                    backgroundImage,
+                    backgroundImageAlpha,
+                    selection,
+                    readAloud,
+                    Modifier.fillMaxSize(),
+                    activeSelection,
+                    selectionPreviewStyle,
+                    cachedImage,
+                    loadImage
+                )
+            }
         }
         Canvas(Modifier.fillMaxSize()) {
+            val frame = PageCurlGeometry.calculate(
+                width,
+                height,
+                ReaderCurlTouchPolicy.revealX(direction, touchX(), pageExtentPx, revealProgress()),
+                touchY().coerceIn(.1f, height - .1f),
+                lockedCorner = CurlPoint(width, cornerY()),
+            )
+            if (frame == null) {
+                // A degenerate Bezier frame used to draw only the base page, which made the
+                // entering page disappear for an entire drag frame. Keep the destination visible
+                // with a cheap horizontal fallback until the next valid curl frame arrives.
+                drawPageSnapshot(revealBitmap, revealLayer, widthPx, heightPx)
+                val baseTranslation = when (direction) {
+                    ReaderTurnDirection.NEXT -> pageOffsetPx()
+                    ReaderTurnDirection.PREVIOUS -> pageOffsetPx() - width
+                }
+                withTransform({ translate(baseTranslation, 0f) }) {
+                    drawPageSnapshot(baseBitmap, baseLayer, widthPx, heightPx)
+                }
+                return@Canvas
+            }
+            val paths = renderPaths.update(frame, width, height)
+            val path0 = paths.front
+            val pathNext = paths.reveal
+            val pathBack = paths.back
+            // 翻起页正面(挖掉折起的角)
+            clipPath(path0, ClipOp.Difference) {
+                drawPageSnapshot(baseBitmap, baseLayer, widthPx, heightPx)
+            }
+            // 折页下方露出的下一页
+            clipPath(path0) {
+                clipPath(pathNext) {
+                    drawPageSnapshot(revealBitmap, revealLayer, widthPx, heightPx)
+                    drawCurlBackShadow(frame)
+                }
+            }
+            // 翻起页背面(镜像的当前页)+ 折缝阴影
             clipPath(path0) { clipPath(pathBack) {
                 drawRect(background)
-                val matrix = Matrix().apply {
-                    values[Matrix.ScaleX] = frame.mirror.scaleX
-                    values[Matrix.SkewX] = frame.mirror.skewX
-                    values[Matrix.SkewY] = frame.mirror.skewY
-                    values[Matrix.ScaleY] = frame.mirror.scaleY
-                    values[Matrix.TranslateX] = frame.mirror.translateX
-                    values[Matrix.TranslateY] = frame.mirror.translateY
-                }
                 // Canvas.drawBitmap() in the View implementation naturally kept sampling
                 // within the screenshot's bounds. A transformed GraphicsLayer otherwise
                 // samples beyond its recorded page surface as opaque black on some devices,
@@ -2077,10 +2161,10 @@ private fun SimulationPageStack(
                 // coordinates first so uncovered back-page pixels retain the mean background
                 // color drawn above, while the complete background image remains mirrored.
                 withTransform({
-                    transform(matrix)
+                    transform(paths.mirror)
                     clipRect(0f, 0f, size.width, size.height)
                 }) {
-                    drawLayer(baseLayer)
+                    drawPageSnapshot(baseBitmap, baseLayer, widthPx, heightPx)
                 }
                 drawCurlFolderShadow(frame)
             } }
@@ -2089,75 +2173,318 @@ private fun SimulationPageStack(
     }
 }
 
+/**
+ * 一页位图快照的身份。任何会改变该页像素的输入都必须进来,否则会命中过期位图:
+ * 页 id/revision/layoutRevision(图片刷新会改 revision)、背景色/背景图及其透明度、
+ * 选区与朗读高亮色、活动选区、选区预览样式、视口尺寸。
+ */
+private data class PageSnapshotKey(
+    val pageId: ReaderPageId,
+    val revision: Long,
+    val layoutRevision: Long,
+    val backgroundArgb: Int,
+    val backgroundImageIdentity: Int,
+    val backgroundImageAlpha: Float,
+    val selectionArgb: Int,
+    val readAloudArgb: Int,
+    val activeSelection: ReaderSelection?,
+    val previewStyle: TextProcessStyle?,
+    val widthPx: Int,
+    val heightPx: Int,
+)
+
+private fun pageSnapshotKey(
+    page: ReaderPage,
+    background: Color,
+    backgroundImage: Drawable?,
+    backgroundImageAlpha: Float,
+    selection: Color,
+    readAloud: Color,
+    activeSelection: ReaderSelection?,
+    selectionPreviewStyle: TextProcessStyle?,
+    widthPx: Int,
+    heightPx: Int,
+): PageSnapshotKey = PageSnapshotKey(
+    pageId = page.id,
+    revision = page.revision,
+    layoutRevision = page.layoutRevision,
+    backgroundArgb = background.toArgb(),
+    backgroundImageIdentity = System.identityHashCode(backgroundImage),
+    backgroundImageAlpha = backgroundImageAlpha,
+    selectionArgb = selection.toArgb(),
+    readAloudArgb = readAloud.toArgb(),
+    activeSelection = activeSelection,
+    previewStyle = selectionPreviewStyle,
+    widthPx = widthPx,
+    heightPx = heightPx,
+)
+
+/**
+ * 会话级页面位图快照池,对照旧 View 的 `curBitmap / prevBitmap / nextBitmap`。
+ *
+ * 旧实现在 `setBitmap()` 里复用同一张 Bitmap 的内存;Compose 做不到——`GraphicsLayer.toImageBitmap()`
+ * 每次都新建 Bitmap,`GraphicsLayer.draw` 是 internal,`Canvas.drawRenderNode` 又只能作用于硬件画布——
+ * 所以这里退一步:按页身份缓存最近几页的位图。收益有两处:
+ * ① 命中时该回合**完全不用重录页面**(快照层都不用挂载),每帧只做 blit;
+ * ② 取消后再拖、或连续翻页时,只需补拍新进入窗口的那一页(原来每回合都要重算两页)。
+ */
+private class PageSnapshotBitmapPool {
+    private val entries = LinkedHashMap<PageSnapshotKey, ImageBitmap>(4, .75f, true)
+
+    fun get(key: PageSnapshotKey): ImageBitmap? = entries[key]
+
+    fun put(key: PageSnapshotKey, bitmap: ImageBitmap) {
+        if (bitmap.width <= 0 || bitmap.height <= 0) return
+        entries[key] = bitmap
+        while (entries.size > MAX_ENTRIES) {
+            val oldest = entries.entries.iterator()
+            if (!oldest.hasNext()) break
+            oldest.next()
+            oldest.remove()
+        }
+    }
+
+    /**
+     * 未命中且本页异步像素源都已就绪([isSnapshotSettled])时才栅格化进池子。
+     * 未就绪时返回 null,调用方继续用图层路径——否则会把"图片还没加载"的占位图固化进池子,
+     * 之后每次折页都拿它当快照。
+     */
+    suspend fun capture(
+        layer: GraphicsLayer,
+        key: PageSnapshotKey,
+        settled: Boolean
+    ): ImageBitmap? {
+        if (!settled) return null
+        entries[key]?.let { return it }
+        val bitmap = runCatching { layer.toImageBitmap() }.getOrNull() ?: return null
+        if (bitmap.width <= 0 || bitmap.height <= 0) return null
+        put(key, bitmap)
+        return bitmap
+    }
+
+    fun clear() = entries.clear()
+
+    private companion object {
+        /** 三张全屏 ARGB 位图 ≈ 旧 View 的 cur/prev/next 三张快照;再多就是白占内存。 */
+        const val MAX_ENTRIES = 3
+    }
+}
+
+/**
+ * 该页的异步像素源是否都已就绪:内联图、文字背景图、书签角标。未就绪时不拍位图快照,
+ * 否则会把"加载中"的占位内容固化进池子,之后每次折页都拿它当快照。
+ */
+private fun ReaderPage.isSnapshotSettled(
+    cachedImage: (ReaderElement.Image) -> Bitmap?,
+): Boolean {
+    if (elements.any { it is ReaderElement.Image && cachedImage(it) == null }) return false
+    if (textBackgroundRuns().any { ReaderTextBackgroundLoader.cached(it.image.source) == null }) {
+        return false
+    }
+    val badge = decoration.bookmarkBadge ?: return true
+    return ReaderBookmarkBadgeRenderer.cachedResult(badge) != null
+}
+
+/** 有位图就用位图(一次 blit),否则退回快照层(display list 重放)。 */
+private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawPageSnapshot(
+    image: ImageBitmap?,
+    layer: GraphicsLayer,
+    widthPx: Int,
+    heightPx: Int,
+) {
+    if (image != null) {
+        drawImage(
+            image = image,
+            dstOffset = IntOffset.Zero,
+            dstSize = IntSize(widthPx, heightPx),
+        )
+    } else {
+        drawLayer(layer)
+    }
+}
+
+/**
+ * 把 [content] 录制进 [layer],本身不向屏幕画任何东西;合成由上层画布的 `drawLayer` 负责。
+ *
+ * `graphicsLayer` 让本节点拥有独立的 RenderNode:兄弟节点(每帧都失效的折页合成画布)
+ * 重绘时不会重新执行这里的绘制块,因此 `record` 只在本页内容真正变化(换页、图片加载完成、
+ * 选区变化……)时发生——等价于旧 View 的 `setBitmap()` 每次手势只截一次图。
+ */
+@Composable
+private fun PageSnapshotLayer(
+    layer: GraphicsLayer,
+    content: @Composable () -> Unit,
+) {
+    Box(
+        Modifier
+            .fillMaxSize()
+            // 空块是刻意的:`graphicsLayer` 只要传入非空 block 就会为本节点申请独立
+            // RenderNode(OwnedLayer),`RenderNodeLayer.updateDisplayList` 仅在自身脏时
+            // 才执行下面的绘制块。删掉它,`record` 会退化成每帧重录整页。
+            .graphicsLayer { }
+            .drawWithContent {
+                layer.record { this@drawWithContent.drawContent() }
+            }
+    ) {
+        content()
+    }
+}
+
+/** 分页模式下参与合成的一页在 [ReaderTransitionTransforms] 里的角色。 */
+private enum class PagedLayerRole { CURRENT, PREVIOUS, NEXT }
+
+private fun ReaderTransitionTransforms.forRole(role: PagedLayerRole): ReaderPageTransform? =
+    when (role) {
+        PagedLayerRole.CURRENT -> current
+        PagedLayerRole.PREVIOUS -> previous
+        PagedLayerRole.NEXT -> next
+    }
+
 private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawCurlFrontShadows(
     frame: PageCurlFrame,
     paths: ReaderCurlRenderPaths,
 ) {
     val curlPath = paths.front
     val reverse = frame.corner.x == 0f && frame.corner.y == size.height || frame.corner.x == size.width && frame.corner.y == 0f
-    clipPath(curlPath, ClipOp.Difference) { clipPath(paths.frontShadowHorizontal) {
-        val left = if (reverse) frame.control1.x else frame.control1.x - 25f
-        val right = if (reverse) frame.control1.x + 25f else frame.control1.x + 1f
-        val rotation = (atan2((frame.touch.x - frame.control1.x).toDouble(), (frame.control1.y - frame.touch.y).toDouble()) * 180.0 / PI).toFloat()
-        withTransform({ rotate(rotation, Offset(frame.control1.x, frame.control1.y)) }) {
-            drawRect(Brush.horizontalGradient(if (reverse) listOf(Color(ReaderCurlVisualPolicy.frontShadowDarkArgb), Color.Transparent) else listOf(Color.Transparent, Color(ReaderCurlVisualPolicy.frontShadowDarkArgb)), left, right), Offset(left, frame.control1.y - hypot(size.width.toDouble(), size.height.toDouble()).toFloat()), Size(right - left, hypot(size.width.toDouble(), size.height.toDouble()).toFloat()))
+    // 两条正面阴影共用同一个"折页之外"的裁剪:合成一次裁剪遮罩,别为每条阴影各来一次。
+    clipPath(curlPath, ClipOp.Difference) {
+        clipPath(paths.frontShadowHorizontal) {
+            val left = if (reverse) frame.control1.x else frame.control1.x - 25f
+            val right = if (reverse) frame.control1.x + 25f else frame.control1.x + 1f
+            val rotation = (atan2(
+                (frame.touch.x - frame.control1.x).toDouble(),
+                (frame.control1.y - frame.touch.y).toDouble()
+            ) * 180.0 / PI).toFloat()
+            withTransform({ rotate(rotation, Offset(frame.control1.x, frame.control1.y)) }) {
+                drawRect(
+                    Brush.horizontalGradient(
+                        if (reverse) listOf(
+                            Color(ReaderCurlVisualPolicy.frontShadowDarkArgb),
+                            Color.Transparent
+                        ) else listOf(
+                            Color.Transparent,
+                            Color(ReaderCurlVisualPolicy.frontShadowDarkArgb)
+                        ),
+                        left,
+                        right
+                    ),
+                    Offset(
+                        left,
+                        frame.control1.y - hypot(
+                            size.width.toDouble(),
+                            size.height.toDouble()
+                        ).toFloat()
+                    ),
+                    Size(
+                        right - left,
+                        hypot(size.width.toDouble(), size.height.toDouble()).toFloat()
+                    )
+                )
+            }
         }
-    } }
-    clipPath(curlPath, ClipOp.Difference) { clipPath(paths.frontShadowVertical) {
-        val top = if (reverse) frame.control2.y else frame.control2.y - 25f
-        val bottom = if (reverse) frame.control2.y + 25f else frame.control2.y + 1f
-        val rotation = (atan2((frame.control2.y - frame.touch.y).toDouble(), (frame.control2.x - frame.touch.x).toDouble()) * 180.0 / PI).toFloat()
-        val diagonal = hypot(size.width.toDouble(), size.height.toDouble()).toFloat()
-        val adjustedY = if (frame.control2.y < 0f) frame.control2.y - size.height else frame.control2.y
-        val hmg = hypot(frame.control2.x.toDouble(), adjustedY.toDouble()).toFloat()
-        val left = if (hmg > diagonal) frame.control2.x - 25f - hmg else frame.control2.x - diagonal
-        val right = if (hmg > diagonal) frame.control2.x + diagonal - hmg else frame.control2.x
-        withTransform({ rotate(rotation, Offset(frame.control2.x, frame.control2.y)) }) {
-            drawRect(Brush.verticalGradient(if (reverse) listOf(Color(ReaderCurlVisualPolicy.frontShadowDarkArgb), Color.Transparent) else listOf(Color.Transparent, Color(ReaderCurlVisualPolicy.frontShadowDarkArgb)), top, bottom), Offset(left, top), Size(right - left, bottom - top))
+        clipPath(paths.frontShadowVertical) {
+            val top = if (reverse) frame.control2.y else frame.control2.y - 25f
+            val bottom = if (reverse) frame.control2.y + 25f else frame.control2.y + 1f
+            val rotation = (atan2(
+                (frame.control2.y - frame.touch.y).toDouble(),
+                (frame.control2.x - frame.touch.x).toDouble()
+            ) * 180.0 / PI).toFloat()
+            val diagonal = hypot(size.width.toDouble(), size.height.toDouble()).toFloat()
+            val adjustedY =
+                if (frame.control2.y < 0f) frame.control2.y - size.height else frame.control2.y
+            val hmg = hypot(frame.control2.x.toDouble(), adjustedY.toDouble()).toFloat()
+            val left =
+                if (hmg > diagonal) frame.control2.x - 25f - hmg else frame.control2.x - diagonal
+            val right = if (hmg > diagonal) frame.control2.x + diagonal - hmg else frame.control2.x
+            withTransform({ rotate(rotation, Offset(frame.control2.x, frame.control2.y)) }) {
+                drawRect(
+                    Brush.verticalGradient(
+                        if (reverse) listOf(
+                            Color(ReaderCurlVisualPolicy.frontShadowDarkArgb),
+                            Color.Transparent
+                        ) else listOf(
+                            Color.Transparent,
+                            Color(ReaderCurlVisualPolicy.frontShadowDarkArgb)
+                        ),
+                        top,
+                        bottom
+                    ), Offset(left, top), Size(right - left, bottom - top)
+                )
+            }
         }
-    } }
+    }
 }
 
-private data class ReaderCurlRenderPaths(
-    val front: Path,
-    val reveal: Path,
-    val back: Path,
-    val frontShadowHorizontal: Path,
-    val frontShadowVertical: Path,
-)
+/** 清空并重建一个复用的 [Path](`rewind()` 保留内部缓冲,不逐帧新建)。 */
+private inline fun Path.rebuild(block: Path.() -> Unit) {
+    rewind()
+    block()
+}
 
-private fun PageCurlFrame.renderPaths(width: Float, height: Float): ReaderCurlRenderPaths {
-    val reverse = corner.x == 0f && corner.y == height || corner.x == width && corner.y == 0f
-    val angle = if (reverse) {
-        PI / 4 - atan2((control1.y - touch.y).toDouble(), (touch.x - control1.x).toDouble())
-    } else {
-        PI / 4 - atan2((touch.y - control1.y).toDouble(), (touch.x - control1.x).toDouble())
-    }
-    val shadowX = (touch.x + 25f * 1.414f * cos(angle)).toFloat()
-    val shadowY = (touch.y + (if (reverse) 1 else -1) * 25f * 1.414f * sin(angle)).toFloat()
-    return ReaderCurlRenderPaths(
-        front = Path().apply {
-            moveTo(start1.x, start1.y); quadraticTo(control1.x, control1.y, end1.x, end1.y)
-            lineTo(touch.x, touch.y); lineTo(end2.x, end2.y)
-            quadraticTo(control2.x, control2.y, start2.x, start2.y); lineTo(corner.x, corner.y); close()
-        },
-        reveal = Path().apply {
-            moveTo(start1.x, start1.y); lineTo(vertex1.x, vertex1.y); lineTo(vertex2.x, vertex2.y)
-            lineTo(start2.x, start2.y); lineTo(corner.x, corner.y); close()
-        },
-        back = Path().apply {
-            moveTo(vertex2.x, vertex2.y); lineTo(vertex1.x, vertex1.y); lineTo(end1.x, end1.y)
-            lineTo(touch.x, touch.y); lineTo(end2.x, end2.y); close()
-        },
-        frontShadowHorizontal = Path().apply {
-            moveTo(shadowX, shadowY); lineTo(touch.x, touch.y); lineTo(control1.x, control1.y)
-            lineTo(start1.x, start1.y); close()
-        },
-        frontShadowVertical = Path().apply {
-            moveTo(shadowX, shadowY); lineTo(touch.x, touch.y); lineTo(control2.x, control2.y)
-            lineTo(start2.x, start2.y); close()
-        },
-    )
+/**
+ * 折页每帧重用的绘制载体,对照旧 `SimulationPageDelegate` 的成员 `mPath0/mPath1/mMatrix`:
+ * 折页是 60/120fps 热路径,逐帧新建 5 个 `Path` 只会给 GC 添压力。
+ */
+private class ReaderCurlRenderPaths {
+    val front = Path()
+    val reveal = Path()
+    val back = Path()
+    val frontShadowHorizontal = Path()
+    val frontShadowVertical = Path()
+    val mirror = Matrix()
+
+    fun update(frame: PageCurlFrame, width: Float, height: Float): ReaderCurlRenderPaths {
+        val reverse = frame.corner.x == 0f && frame.corner.y == height ||
+                frame.corner.x == width && frame.corner.y == 0f
+        val angle = if (reverse) {
+            PI / 4 - atan2(
+                (frame.control1.y - frame.touch.y).toDouble(),
+                (frame.touch.x - frame.control1.x).toDouble()
+            )
+        } else {
+            PI / 4 - atan2(
+                (frame.touch.y - frame.control1.y).toDouble(),
+                (frame.touch.x - frame.control1.x).toDouble()
+            )
+        }
+        val shadowX = (frame.touch.x + 25f * 1.414f * cos(angle)).toFloat()
+        val shadowY =
+            (frame.touch.y + (if (reverse) 1 else -1) * 25f * 1.414f * sin(angle)).toFloat()
+        front.rebuild {
+            moveTo(frame.start1.x, frame.start1.y)
+            quadraticTo(frame.control1.x, frame.control1.y, frame.end1.x, frame.end1.y)
+            lineTo(frame.touch.x, frame.touch.y); lineTo(frame.end2.x, frame.end2.y)
+            quadraticTo(frame.control2.x, frame.control2.y, frame.start2.x, frame.start2.y)
+            lineTo(frame.corner.x, frame.corner.y); close()
+        }
+        reveal.rebuild {
+            moveTo(frame.start1.x, frame.start1.y); lineTo(frame.vertex1.x, frame.vertex1.y)
+            lineTo(frame.vertex2.x, frame.vertex2.y)
+            lineTo(frame.start2.x, frame.start2.y); lineTo(frame.corner.x, frame.corner.y); close()
+        }
+        back.rebuild {
+            moveTo(frame.vertex2.x, frame.vertex2.y); lineTo(frame.vertex1.x, frame.vertex1.y)
+            lineTo(frame.end1.x, frame.end1.y)
+            lineTo(frame.touch.x, frame.touch.y); lineTo(frame.end2.x, frame.end2.y); close()
+        }
+        frontShadowHorizontal.rebuild {
+            moveTo(shadowX, shadowY); lineTo(frame.touch.x, frame.touch.y)
+            lineTo(frame.control1.x, frame.control1.y)
+            lineTo(frame.start1.x, frame.start1.y); close()
+        }
+        frontShadowVertical.rebuild {
+            moveTo(shadowX, shadowY); lineTo(frame.touch.x, frame.touch.y)
+            lineTo(frame.control2.x, frame.control2.y)
+            lineTo(frame.start2.x, frame.start2.y); close()
+        }
+        mirror.values[Matrix.ScaleX] = frame.mirror.scaleX
+        mirror.values[Matrix.SkewX] = frame.mirror.skewX
+        mirror.values[Matrix.SkewY] = frame.mirror.skewY
+        mirror.values[Matrix.ScaleY] = frame.mirror.scaleY
+        mirror.values[Matrix.TranslateX] = frame.mirror.translateX
+        mirror.values[Matrix.TranslateY] = frame.mirror.translateY
+        return this
+    }
 }
 
 private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawCurlBackShadow(frame: PageCurlFrame) {

+ 0 - 17
app/src/main/java/io/legado/app/help/PaintPool.kt

@@ -1,17 +0,0 @@
-package io.legado.app.help
-
-import android.graphics.Paint
-import io.legado.app.utils.objectpool.BaseSafeObjectPool
-
-object PaintPool : BaseSafeObjectPool<Paint>(8) {
-
-    private val emptyPaint = Paint()
-
-    override fun create(): Paint = Paint()
-
-    override fun recycle(target: Paint) {
-        target.set(emptyPaint)
-        super.recycle(target)
-    }
-
-}

+ 5 - 7
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -78,7 +78,6 @@ import io.legado.app.feature.reader.core.gesture.ReaderTapActionGrid
 import io.legado.app.feature.reader.core.model.readerBackgroundAlpha
 import io.legado.app.feature.reader.core.transition.ReaderPageTurnSpeed
 import io.legado.app.feature.reader.core.transition.ReaderTransitionMode
-import io.legado.app.feature.reader.core.transition.ReaderViewportLayerPolicy
 import io.legado.app.feature.reader.platform.ReaderPerfTrace
 import io.legado.app.help.IntentHelp
 import io.legado.app.model.ReadBook
@@ -729,14 +728,13 @@ fun ReadBookRouteScreen(
                     )
                 }
         ) {
-            // 滚动模式的背景由画布内的固定层绘制(画布还要当菜单 haze 的源),根层再画一遍
-            // 会让半透明背景图叠加两次、比设置值更浓,且与分页模式(页面自绘不透明底色挡住
-            // 根层,实际只画一次)观感不一致。画布可见时让出根层,其它状态仍由根层兜底。
+            // 背景由画布自己负责:滚动模式是画布内的固定层,分页模式是每页自绘的不透明底色
+            // 与背景图(`readerSurfaceColor` 不透明,根层这一遍会被整屏遮住)。画布可见时
+            // 一律让出根层,否则每个翻页帧都要重画一次未压缩的全屏背景位图;画布不可见
+            // (首个可读页之前、退出之后)仍由根层兜底。
             val readerCanvasVisible = hasReadablePage
             val readerTransitionMode = ReaderTransitionMode.fromPageAnim(controller.pageAnim)
-            if (!(ReaderViewportLayerPolicy.usesFixedBackground(readerTransitionMode) &&
-                        readerCanvasVisible)
-            ) {
+            if (!readerCanvasVisible) {
                 // 归因用:成对 marker 夹住子树,其间隔即该子树的组合耗时(都在
                 // `Compose:recompose` 之内)。子系统不用 tracing 时 marker 是空操作。
                 ReaderPerfTrace.marker("compose.background.begin")

+ 0 - 3
app/src/main/java/io/legado/app/ui/config/readConfig/ReadConfigScreen.kt

@@ -22,7 +22,6 @@ import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
-import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -388,7 +387,6 @@ fun ReadConfigScreen(
                     }
                 )
 
-                if (CanvasRecorderFactory.isSupport) {
                     SwitchSettingItem(
                         title = stringResource(R.string.enable_optimize_render),
                         checked = settings.optimizeRender,
@@ -396,7 +394,6 @@ fun ReadConfigScreen(
                             onIntent(ReadConfigIntent.OptimizeRenderChanged(it))
                         }
                     )
-                }
 
                 ClickableSettingItem(
                     title = stringResource(R.string.click_regional_config),

+ 1 - 51
app/src/main/java/io/legado/app/utils/ViewExtensions.kt

@@ -4,10 +4,6 @@ package io.legado.app.utils
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Picture
 import android.os.Build
 import android.text.Html
 import android.view.MotionEvent
@@ -29,9 +25,6 @@ import androidx.annotation.DrawableRes
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.view.menu.MenuPopupHelper
 import androidx.appcompat.widget.PopupMenu
-import androidx.core.graphics.createBitmap
-import androidx.core.graphics.record
-import androidx.core.graphics.withTranslation
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.get
@@ -40,11 +33,9 @@ import androidx.core.view.marginBottom
 import androidx.core.view.updateLayoutParams
 import androidx.recyclerview.widget.RecyclerView
 import androidx.viewpager.widget.ViewPager
-import io.legado.app.help.GlideImageGetter
 import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.help.GlideImageGetter
 import io.legado.app.lib.theme.TintHelper
-import io.legado.app.utils.canvasrecorder.CanvasRecorder
-import io.legado.app.utils.canvasrecorder.record
 import org.koin.core.context.GlobalContext
 import splitties.systemservices.inputMethodManager
 import splitties.views.bottomPadding
@@ -165,47 +156,6 @@ fun View.visible(visible: Boolean) {
     }
 }
 
-fun View.screenshot(bitmap: Bitmap? = null, canvas: Canvas? = null): Bitmap? {
-    return if (width > 0 && height > 0) {
-        val screenshot = if (bitmap != null && bitmap.width == width && bitmap.height == height) {
-            bitmap.eraseColor(Color.TRANSPARENT)
-            bitmap
-        } else {
-            bitmap?.recycle()
-            createBitmap(width, height)
-        }
-        val c = canvas ?: Canvas()
-        c.setBitmap(screenshot)
-        c.save()
-        c.translate(-scrollX.toFloat(), -scrollY.toFloat())
-        this.draw(c)
-        c.restore()
-        c.setBitmap(null)
-        screenshot.prepareToDraw()
-        screenshot
-    } else {
-        null
-    }
-}
-
-fun View.screenshot(picture: Picture) {
-    if (width > 0 && height > 0) {
-        picture.record(width, height) {
-            withTranslation(-scrollX.toFloat(), -scrollY.toFloat()) {
-                draw(this)
-            }
-        }
-    }
-}
-
-fun View.screenshot(canvasRecorder: CanvasRecorder) {
-    if (width > 0 && height > 0) {
-        canvasRecorder.record(width, height) {
-            draw(this)
-        }
-    }
-}
-
 fun View.setPaddingBottom(bottom: Int) {
     setPadding(paddingLeft, paddingTop, paddingRight, bottom)
 }

+ 0 - 36
app/src/main/java/io/legado/app/utils/canvasrecorder/BaseCanvasRecorder.kt

@@ -1,36 +0,0 @@
-package io.legado.app.utils.canvasrecorder
-
-import androidx.annotation.CallSuper
-
-abstract class BaseCanvasRecorder : CanvasRecorder {
-
-    @JvmField
-    protected var isDirty = true
-
-    override fun invalidate() {
-        isDirty = true
-    }
-
-    @CallSuper
-    override fun recycle() {
-        isDirty = true
-    }
-
-    @CallSuper
-    override fun endRecording() {
-        isDirty = false
-    }
-
-    override fun isDirty(): Boolean {
-        return isDirty
-    }
-
-    override fun isLocked(): Boolean {
-        return false
-    }
-
-    override fun needRecord(): Boolean {
-        return isDirty() && !isLocked()
-    }
-
-}

+ 0 - 27
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorder.kt

@@ -1,27 +0,0 @@
-package io.legado.app.utils.canvasrecorder
-
-import android.graphics.Canvas
-
-interface CanvasRecorder {
-
-    val width: Int
-
-    val height: Int
-
-    fun beginRecording(width: Int, height: Int): Canvas
-
-    fun endRecording()
-
-    fun draw(canvas: Canvas)
-
-    fun invalidate()
-
-    fun recycle()
-
-    fun isDirty(): Boolean
-
-    fun isLocked(): Boolean
-
-    fun needRecord(): Boolean
-
-}

+ 0 - 47
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi23Impl.kt

@@ -1,47 +0,0 @@
-package io.legado.app.utils.canvasrecorder
-
-import android.graphics.Canvas
-import android.graphics.Picture
-import io.legado.app.utils.canvasrecorder.pools.PicturePool
-import io.legado.app.utils.objectpool.synchronized
-
-class CanvasRecorderApi23Impl : BaseCanvasRecorder() {
-
-    private var picture: Picture? = null
-
-    override val width get() = picture?.width ?: -1
-    override val height get() = picture?.height ?: -1
-
-    private fun initPicture() {
-        if (picture == null) {
-            picture = picturePool.obtain()
-        }
-    }
-
-    override fun beginRecording(width: Int, height: Int): Canvas {
-        initPicture()
-        return picture!!.beginRecording(width, height)
-    }
-
-    override fun endRecording() {
-        picture!!.endRecording()
-        super.endRecording()
-    }
-
-    override fun draw(canvas: Canvas) {
-        if (picture == null) return
-        canvas.drawPicture(picture!!)
-    }
-
-    override fun recycle() {
-        super.recycle()
-        if (picture == null) return
-        picturePool.recycle(picture!!)
-        picture = null
-    }
-
-    companion object {
-        private val picturePool = PicturePool().synchronized()
-    }
-
-}

+ 0 - 76
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderApi29Impl.kt

@@ -1,76 +0,0 @@
-package io.legado.app.utils.canvasrecorder
-
-import android.graphics.Canvas
-import android.graphics.Picture
-import android.graphics.RenderNode
-import android.os.Build
-import androidx.annotation.RequiresApi
-import io.legado.app.utils.objectpool.synchronized
-import io.legado.app.utils.canvasrecorder.pools.PicturePool
-import io.legado.app.utils.canvasrecorder.pools.RenderNodePool
-
-@RequiresApi(Build.VERSION_CODES.Q)
-class CanvasRecorderApi29Impl : BaseCanvasRecorder() {
-
-    private var renderNode: RenderNode? = null
-    private var picture: Picture? = null
-
-    override val width get() = renderNode?.width ?: -1
-    override val height get() = renderNode?.height ?: -1
-
-    private fun init() {
-        if (renderNode == null) {
-            renderNode = renderNodePool.obtain()
-        }
-        if (picture == null) {
-            picture = picturePool.obtain()
-        }
-    }
-
-    private fun flushRenderNode() {
-        val rc = renderNode!!.beginRecording()
-        rc.drawPicture(picture!!)
-        renderNode!!.endRecording()
-    }
-
-    override fun beginRecording(width: Int, height: Int): Canvas {
-        init()
-        renderNode!!.setPosition(0, 0, width, height)
-        return picture!!.beginRecording(width, height)
-    }
-
-    override fun endRecording() {
-        picture!!.endRecording()
-        flushRenderNode()
-        super.endRecording()
-    }
-
-    override fun draw(canvas: Canvas) {
-        if (renderNode == null || picture == null) {
-            return
-        }
-        if (canvas.isHardwareAccelerated) {
-            if (!renderNode!!.hasDisplayList()) {
-                flushRenderNode()
-            }
-            canvas.drawRenderNode(renderNode!!)
-        } else {
-            canvas.drawPicture(picture!!)
-        }
-    }
-
-    override fun recycle() {
-        super.recycle()
-        if (renderNode == null || picture == null) return
-        renderNodePool.recycle(renderNode!!)
-        renderNode = null
-        picturePool.recycle(picture!!)
-        picture = null
-    }
-
-    companion object {
-        private val picturePool = PicturePool().synchronized()
-        private val renderNodePool = RenderNodePool().synchronized()
-    }
-
-}

+ 0 - 44
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderExtensions.kt

@@ -1,44 +0,0 @@
-package io.legado.app.utils.canvasrecorder
-
-import android.graphics.Canvas
-import android.view.View
-import androidx.core.graphics.withSave
-
-inline fun CanvasRecorder.recordIfNeeded(
-    width: Int,
-    height: Int,
-    block: Canvas.() -> Unit
-): Boolean {
-    if (!needRecord()) return false
-    record(width, height, block)
-    return true
-}
-
-fun CanvasRecorder.recordIfNeeded(view: View): Boolean {
-    if (!needRecord()) return false
-    record(view.width, view.height) {
-        view.draw(this)
-    }
-    return true
-}
-
-inline fun CanvasRecorder.record(width: Int, height: Int, block: Canvas.() -> Unit) {
-    val canvas = beginRecording(width, height)
-    try {
-        canvas.withSave {
-            block()
-        }
-    } finally {
-        endRecording()
-    }
-}
-
-inline fun CanvasRecorder.recordIfNeededThenDraw(
-    canvas: Canvas,
-    width: Int,
-    height: Int,
-    block: Canvas.() -> Unit
-) {
-    recordIfNeeded(width, height, block)
-    draw(canvas)
-}

+ 0 - 31
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderFactory.kt

@@ -1,31 +0,0 @@
-package io.legado.app.utils.canvasrecorder
-
-import android.os.Build
-import io.legado.app.domain.gateway.ReadSettingsGateway
-import org.koin.core.context.GlobalContext
-
-object CanvasRecorderFactory {
-
-    private val readSettingsGateway
-        get() = GlobalContext.get().get<ReadSettingsGateway>()
-
-    private val atLeastApi24 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
-    private val atLeastApi29 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
-    val isSupport = atLeastApi24
-
-    // issue 3868
-    fun create(locked: Boolean = false): CanvasRecorder {
-        val impl = when {
-            !readSettingsGateway.currentSettings.optimizeRender -> CanvasRecorderImpl()
-            atLeastApi29 -> CanvasRecorderApi29Impl()
-            atLeastApi24 -> CanvasRecorderApi23Impl()
-            else -> CanvasRecorderImpl()
-        }
-        return if (locked) {
-            CanvasRecorderLocked(impl)
-        } else {
-            impl
-        }
-    }
-
-}

+ 0 - 72
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderImpl.kt

@@ -1,72 +0,0 @@
-package io.legado.app.utils.canvasrecorder
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import com.bumptech.glide.Glide
-import io.legado.app.utils.canvasrecorder.pools.CanvasPool
-import splitties.init.appCtx
-
-class CanvasRecorderImpl : BaseCanvasRecorder() {
-
-    var bitmap: Bitmap? = null
-    var canvas: Canvas? = null
-
-    override val width get() = bitmap?.width ?: -1
-    override val height get() = bitmap?.height ?: -1
-
-    private fun init(width: Int, height: Int) {
-        if (width <= 0 || height <= 0) {
-            return
-        }
-        if (bitmap == null) {
-            bitmap = bitmapPool.get(width, height, Bitmap.Config.ARGB_8888)
-        }
-        if (bitmap!!.width != width || bitmap!!.height != height) {
-            if (bitmap!!.isMutable && canReconfigure(width, height)) {
-                bitmap!!.reconfigure(width, height, Bitmap.Config.ARGB_8888)
-            } else {
-                bitmapPool.put(bitmap!!)
-                bitmap = bitmapPool.get(width, height, Bitmap.Config.ARGB_8888)
-            }
-        }
-    }
-
-    private fun canReconfigure(width: Int, height: Int): Boolean {
-        return bitmap!!.allocationByteCount >= width * height * 4
-    }
-
-    override fun beginRecording(width: Int, height: Int): Canvas {
-        init(width, height)
-        bitmap?.eraseColor(Color.TRANSPARENT)
-        canvas = canvasPool.obtain().apply { setBitmap(bitmap) }
-        return canvas!!
-    }
-
-    override fun endRecording() {
-        bitmap?.prepareToDraw()
-        super.endRecording()
-        canvasPool.recycle(canvas!!)
-        canvas = null
-    }
-
-    override fun draw(canvas: Canvas) {
-        val b = bitmap ?: return
-        if (!b.isRecycled) {
-            canvas.drawBitmap(b, 0f, 0f, null)
-        }
-    }
-
-    override fun recycle() {
-        super.recycle()
-        val b = bitmap ?: return
-        bitmap = null
-        bitmapPool.put(b)
-    }
-
-    companion object {
-        private val canvasPool = CanvasPool(2)
-        private val bitmapPool = Glide.get(appCtx).bitmapPool
-    }
-
-}

+ 0 - 64
app/src/main/java/io/legado/app/utils/canvasrecorder/CanvasRecorderLocked.kt

@@ -1,64 +0,0 @@
-package io.legado.app.utils.canvasrecorder
-
-import android.graphics.Canvas
-import java.util.concurrent.locks.ReentrantLock
-
-class CanvasRecorderLocked(private val delegate: CanvasRecorder) :
-    CanvasRecorder by delegate {
-
-    var lock: ReentrantLock? = ReentrantLock()
-
-    private fun initLock() {
-        if (lock == null) {
-            synchronized(this) {
-                if (lock == null) {
-                    lock = ReentrantLock()
-                }
-            }
-        }
-    }
-
-    override fun beginRecording(width: Int, height: Int): Canvas {
-        initLock()
-        lock!!.lock()
-        return delegate.beginRecording(width, height)
-    }
-
-    override fun endRecording() {
-        delegate.endRecording()
-        lock!!.unlock()
-    }
-
-    override fun draw(canvas: Canvas) {
-        if (lock == null) {
-            return
-        }
-        lock!!.lock()
-        try {
-            delegate.draw(canvas)
-        } finally {
-            lock!!.unlock()
-        }
-    }
-
-    override fun isLocked(): Boolean {
-        if (lock == null) {
-            return false
-        }
-        return lock!!.isLocked
-    }
-
-    override fun recycle() {
-        if (lock == null) {
-            return
-        }
-        lock!!.lock()
-        try {
-            delegate.recycle()
-        } finally {
-            lock!!.unlock()
-        }
-        lock = null
-    }
-
-}

+ 0 - 21
app/src/main/java/io/legado/app/utils/canvasrecorder/pools/CanvasPool.kt

@@ -1,21 +0,0 @@
-package io.legado.app.utils.canvasrecorder.pools
-
-import android.graphics.Canvas
-import androidx.core.util.Pools
-
-class CanvasPool(size: Int) {
-
-    private val pool = Pools.SynchronizedPool<Canvas>(size)
-
-    fun obtain(): Canvas {
-        val canvas = pool.acquire() ?: Canvas()
-        return canvas
-    }
-
-    fun recycle(canvas: Canvas) {
-        canvas.setBitmap(null)
-        canvas.restoreToCount(1)
-        pool.release(canvas)
-    }
-
-}

+ 0 - 10
app/src/main/java/io/legado/app/utils/canvasrecorder/pools/PicturePool.kt

@@ -1,10 +0,0 @@
-package io.legado.app.utils.canvasrecorder.pools
-
-import android.graphics.Picture
-import io.legado.app.utils.objectpool.BaseObjectPool
-
-class PicturePool : BaseObjectPool<Picture>(64) {
-
-    override fun create(): Picture = Picture()
-
-}

+ 0 - 18
app/src/main/java/io/legado/app/utils/canvasrecorder/pools/RenderNodePool.kt

@@ -1,18 +0,0 @@
-package io.legado.app.utils.canvasrecorder.pools
-
-import android.graphics.RenderNode
-import android.os.Build
-import androidx.annotation.RequiresApi
-import io.legado.app.utils.objectpool.BaseObjectPool
-
-@RequiresApi(Build.VERSION_CODES.Q)
-class RenderNodePool : BaseObjectPool<RenderNode>(64) {
-
-    override fun recycle(target: RenderNode) {
-        target.discardDisplayList()
-        super.recycle(target)
-    }
-
-    override fun create(): RenderNode = RenderNode("CanvasRecorder")
-
-}

+ 0 - 19
app/src/main/java/io/legado/app/utils/objectpool/BaseObjectPool.kt

@@ -1,19 +0,0 @@
-package io.legado.app.utils.objectpool
-
-import androidx.annotation.CallSuper
-import androidx.core.util.Pools
-
-abstract class BaseObjectPool<T : Any>(size: Int) : ObjectPool<T> {
-
-    open val pool = Pools.SimplePool<T>(size)
-
-    override fun obtain(): T {
-        return pool.acquire() ?: create()
-    }
-
-    @CallSuper
-    override fun recycle(target: T) {
-        pool.release(target)
-    }
-
-}

+ 0 - 9
app/src/main/java/io/legado/app/utils/objectpool/BaseSafeObjectPool.kt

@@ -1,9 +0,0 @@
-package io.legado.app.utils.objectpool
-
-import androidx.core.util.Pools
-
-abstract class BaseSafeObjectPool<T : Any>(size: Int): BaseObjectPool<T>(size) {
-
-    override val pool = Pools.SynchronizedPool<T>(size)
-
-}

+ 0 - 11
app/src/main/java/io/legado/app/utils/objectpool/ObjectPool.kt

@@ -1,11 +0,0 @@
-package io.legado.app.utils.objectpool
-
-interface ObjectPool<T> {
-
-    fun obtain(): T
-
-    fun recycle(target: T)
-
-    fun create(): T
-
-}

+ 0 - 3
app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolExtensions.kt

@@ -1,3 +0,0 @@
-package io.legado.app.utils.objectpool
-
-fun <T> ObjectPool<T>.synchronized(): ObjectPool<T> = ObjectPoolLocked(this)

+ 0 - 15
app/src/main/java/io/legado/app/utils/objectpool/ObjectPoolLocked.kt

@@ -1,15 +0,0 @@
-package io.legado.app.utils.objectpool
-
-class ObjectPoolLocked<T>(private val delegate: ObjectPool<T>) : ObjectPool<T> by delegate {
-
-    @Synchronized
-    override fun obtain(): T {
-        return delegate.obtain()
-    }
-
-    @Synchronized
-    override fun recycle(target: T) {
-        return delegate.recycle(target)
-    }
-
-}