如何在 Jetpack Compose 中完成主线程工作?

问题描述 投票:0回答:3

在 Jetpack Compose 应用程序中,我有一个 LazyVerticalGrid 缩略图,每个缩略图都需要在撰写时在位图支持的 Canvas 中绘制。

如果我只是在Canvas的DrawScope中绘制缩略图,则可以正确绘制缩略图,但用户体验很差。当用户滚动 LazyVerticalGrid 时,每个缩略图都会自行绘制,因此会出现很多卡顿。

我原以为 Jetpack Compose 会在需要时在后台线程中进行组合,但这一切似乎都发生在主线程上,导致严重的卡顿,即使在最新的手机上也是如此。

我可以通过使用 LaunchedEffect withContext(IO) 在另一个线程上绘制 Canvas 的底层位图来解决卡顿问题。但问题是,Compose 不知道在绘制位图时重新组合 Canvas,所以我经常会得到半绘制的缩略图。

有没有办法在主线程之外完成工作,然后在工作完成后重新组合?

这是 janky 代码(为简洁起见进行了编辑),后面是非 janky 版本,绘图完成后并不总是重新组合:

   val imageBitmap = remember {Bitmap.createBitmap(515, 618, Bitmap.Config.ARGB_8888)}
   val bitmapCanvas = remember { android.graphics.Canvas(imageBitmap) }
   ElevatedCard() {
      Canvas() {
         bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
         penStrokes.forEach {
           inker.drawEvent(it)
         }
         this.drawImage(imageBitmap.asImageBitmap())
       }
   }

非卡顿但仍然不对

   val imageBitmap = remember {Bitmap.createBitmap(515, 618, Bitmap.Config.ARGB_8888)}
   val bitmapCanvas = remember { android.graphics.Canvas(imageBitmap) }
   LaunchedEffect(Unit) {
      withContext(IO) {
         bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
         penStrokes.forEach {
           inker.drawEvent(it)
         }
      }
   }
   ElevatedCard() {
      Canvas() {
         this.drawImage(imageBitmap.asImageBitmap())
       }
   }
android android-jetpack-compose android-canvas
3个回答
2
投票

最后我使用了常见的“invalidate++”拼凑,人们似乎用它来强制 Jetpack Compose 根据命令重绘。我认为它有一点代码味道,但它确实有效。

val imageBitmap = remember {Bitmap.createBitmap(515, 618, 
     Bitmap.Config.ARGB_8888)}
val bitmapCanvas = remember { android.graphics.Canvas(imageBitmap) }
var invalidate by remember { mutableStateOf(0) }


LaunchedEffect(Unit) {
  withContext(IO) {
     bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
     penStrokes.forEach {
       inker.drawEvent(it)
     }
     invalidate++
  }
}

ElevatedCard() {
   Canvas() {
      invalidate.let {
           this.drawImage(imageBitmap.asImageBitmap())
      }
   }
}

0
投票

如果我没记错的话,当您仅更改位图的 contents 时,Compose 不会重绘。更改remember对象的内容不会触发重绘或渲染。

也许这有帮助: android jetpack compose 中的“remember”和“mutableState”有什么区别?


0
投票

penStrokes
SnapshotStateList
吗?

Jetpack Compose 仅当依赖项为

StateObject
s 时才能可靠地更新。未针对 Compose 进行优化的可变对象不是
@Stable
,这意味着每次可组合项被重组时,所有具有不稳定依赖项的子可组合项也会被重组,从而影响性能。同样的道理不仅适用于构图,也适用于其他阶段,例如绘画,这是您在
Canvas
块中进行绘画的阶段。

您应该通过使用

penStrokes
 来构造 
SnapshotStateList
mutableStateListOf()
SnapshotList
StateObject
,因此它们可以与
Snapshot
系统很好地配合。

将绘图卸载到后台线程

为此,我们需要一种方法来监听

penStrokes
中的变化而不触发重组。这就是
snapshotFlow
的用途——它需要一个在快照内运行的块,每次其依赖项发生变化时,它都会再次运行,并且随着时间的推移,该块的结果会发送到
Flow
,即回。
snapshotFlow
可以有效地将 Compose
State
转换为协程
Flow
,例如:

var myState by remember { mutableStateOf(initialValue) }
LaunchedEffect(Unit) {
    val flow = snapshotFlow { myState }
}

但是,这适用于改变值,而不适用于内部可变性。由于我们使用的是

SnapshotList
,因此只有当
snapshotFlow
块实际上从列表中读取或迭代列表时,它才会起作用。一个愚蠢的修复是拥有
snapshotFlow { myList.toList() }
,其中
.toList()
实际上在该时间点创建了
myList
的不可变副本,并且由于它涉及迭代列表,快照系统会正确注册它并返回
Flow 
将迭代
myList
的不可变快照。然而,更有效的解决方案是简单地在
snapshotFlow
 块内进行工作,甚至忽略返回的
Flow

var penStrokes by remember { mutableStateListOf<PenStroke>() }

LaunchedEffect(Unit) {
    withContext(Dispatchers.IO) {
        snapshotFlow {
            bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
            penStrokes.forEach { stroke ->
                inker.drawEvent(stroke)
            }
            invalidate++
        }
    }
}

确实,由于我们正在改变

Bitmap
,而不是
StateObject
,我们需要计数器的帮助来触发失效,就像OP @John他们自己的答案中所示.

硬件加速

但是,请注意,在

Bitmap
上绘图不是硬件加速的,会产生显着的性能成本。这是我们需要将绘图卸载到后台线程的最重要原因。为了更有效地绘制,您可以在 Android 上使用
SurfaceView
,您可以使用
AndroidView()
可组合项将其集成到 Compose 中。然后你可以使用
Surface.lockHardwareCanvas()
来获得同样是硬件加速的
Canvas
。从 Android 14 开始,您还可以使用
HardwareBufferRenderer
来减少延迟,因为它是零复制并且内容在渲染之间保留,从而可以绘制新笔画,而无需在每帧上重新绘制先前的笔画。

© www.soinside.com 2019 - 2024. All rights reserved.