Jetpack Compose 1.5 上新:性能升级,内存优化!

昨天,在 KUG 群看到了江佬分享 Compose 的新版本,这次的亮点在于性能上的升级。Compose 的大版本更新我都有发文章,那么这次自然也不落下。一起来看看新版本有些啥吧

前几篇:

1.3.0:Jetpack Compose 上新:瀑布流布局、下拉加载、DrawScope.drawText

1.4.0:Jetpack Compose 上新:Pager、跑马灯、FlowLayout

官方文档

以下内容翻译自 android-developers.googleblog.com/2023/08/wha...

今天(2023-08-09),作为 Compose 2023 年 8 月版本材料清单(BOM) 的一部分,我们发布了 Jetpack Compose 版本 1.5,这是安卓现代的本地 UI 工具包,被许多应用程序(例如 Play 商店DropboxAirbnb)所使用。此版本主要专注于性能改进,因为我们在 2022 年 10 月版本开始的Modifer 重构 的主要部分现在已合并。

性能

在我们首次发布 2021 年的 Compose 1.0 时,我们专注于确保 API 接口设计正确,为构建应用提供牢固的基础。我们希望有一个功能强大且表达能力强的 API,易于使用且稳定,以便开发人员可以自信地在生产中使用它。随着我们不断改进 API,性能成为了我们的首要任务,在 2023 年 8 月版本中,我们已经实现了许多性能改进。

Modifer 的性能

在此版本中, Modifer 在 Composition 时间上看到了大幅的性能改进,Composition 时间提升高达 80% 。最棒的是,由于我们在第一个版本中确保了正确的 API 接口设计,大多数应用只需升级到 BOM 2023.08.00 版本,即可从中受益

我们有一套用于监控性能回归并指导我们改进性能的基准测试。在 Compose 初始的 1.0 版本发布后,我们开始关注可以进行改进的地方。基准测试显示,我们花费了比预期更多的时间用于实例化 Modifer 。 Modifer 占据了 Composition Tree 的绝大部分,因此占据了 Compose 首次组合时间的最大一块儿。在 2022 年 10 月发布的版本中,我们对 Modifer 进行了更高效的设计重构,该版本包含了新的 API 和性能改进,它位于我们的最底层模块 Compose UI 中。

高级的 Modifer 依赖于更低级的 Modifier,所以我们开始在 Compose Foundation 将低级 Modifer 迁移到下一个版本,即 2023 年 3 月版本。这包括 graphicsLayer、低级焦点 Modifer 、Padding 和 Offset。这些低级 Modifer 被其他广泛使用的 Modifer (例如 Clickable)调用,并且还被许多基础 Composable(例如 Text)使用。在 2023 年 3 月版本中迁移 Modifer 为这些组件带来了性能改进,但真正的收益将在将更高级别的 Modifer 和 Composable 迁移到新的 Modifer 系统时产生。

在 2023 年 8 月版本中,我们已经开始 迁移 Clickable Modifer 到新的 Modifer 系统中,这在某些情况下使 Composition 显著加快,高达 80% 。这在包含可点击元素(如按钮)的 LazyColumn 中尤其重要。被 Clickable 使用的 Modifier.indication 仍在迁移过程中,因此我们预计在未来的版本中会有进一步的收益。

作为这项工作的一部分,我们发现了在最初的重构中未涵盖的组合 Modifer 用例,并添加了一个新的 API,用于创建消耗 CompositionLocal 实例的 Modifier.Node 元素。

我们正在撰写文档,指导您如何将您自己的 Modifer 迁移到新的 Modifier.Node API。要立即开始,请参考我们仓库中的示例

您可以在 Android Dev Summit '22 的 Compose Modifer 深入探讨 中了解更多关于这些变化背后的原因。

内存占用

此版本包含了许多在内存使用方面的改进。我们仔细检查了在不同的 Compose API 中发生的分配,并在许多方面,特别是在图形堆栈和矢量资源加载方面,减少了总的分配。这不仅减少了 Compose 的内存占用,还直接提高了性能,因为我们花费更少的时间分配内存并减少了垃圾回收。

此外,我们修复了在使用 ComposeView 时的 内存泄漏,这将使所有应用受益,特别是那些使用多 Activity 架构或大量的 View/Compose 互操作的应用。

文本

BasicText 已经迁移到了一个由 Modifer 支持的新渲染系统,这给初始组合时间带来了平均 22% 的收益,而在涉及文本的复杂布局的一个基准测试中,收益高达 70%

一些文本 API 也已经稳定下来,包括:

核心功能的改进和修复

我们还在核心 API 中添加了新功能和改进,同时稳定了一些 API:

  • LazyStaggeredGrid 现在已经稳定。
  • 添加了 asComposePaint API,用于替换 toComposePaint,返回的对象包装了原始的 android.graphics.Paint。
  • 添加了 IntermediateMeasurePolicy,以支持 SubcomposeLayout 中的Lookahead 测量。
  • 添加了 onInterceptKeyBeforeSoftKeyboard Modifer ,以在软键盘出现之前拦截键盘事件。

开始吧!

我们对所有提交到我们的 问题追踪器 的错误报告和功能请求表示感谢 --- 它们帮助我们改进 Compose 并构建您所需的 API。请继续提供您的反馈,帮助我们使 Compose 变得更好!

想知道接下来会发生什么?请查看我们的路线图,了解我们目前正在思考和努力开发的功能。我们迫不及待地想看到您接下来会构建什么!

Happy composing!

看看代码

我们可以挑一些变化,看看代码层面到底干了什么

Clickable 迁移到新的 Modifier API

android.googlesource.com/platform/fr...

diff 复制代码
 fun Modifer.clickable(
    // ...
    onClick: () -> Unit
 ) = composed(
     factory = {
-        val onClickState = rememberUpdatedState(onClick)
-        val onLongClickState = rememberUpdatedState(onLongClick)
-        val onDoubleClickState = rememberUpdatedState(onDoubleClick)
         val hasLongClick = onLongClick != null
-        val hasDoubleClick = onDoubleClick != null
         val pressInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
         val currentKeyPressInteractions = remember { mutableMapOf<Key, PressInteraction.Press>() }
         if (enabled) {
@@ -314,48 +304,27 @@
                 }
             }
         }
-        val delayPressInteraction = remember { mutableStateOf({ true }) }
+        val centreOffset = remember { mutableStateOf(Offset.Zero) }
         val interactionModifier = if (enabled) {
             ClickableInteractionElement(
                 interactionSource,
                 pressInteraction,
-                currentKeyPressInteractions,
-                delayPressInteraction
+                currentKeyPressInteractions
             )
         } else Modifier
 
-        val centreOffset = remember { mutableStateOf(Offset.Zero) }
+        val pointerInputModifier = CombinedClickablePointerInputElement(
+            enabled,
+            interactionSource,
+            onClick,
+            centreOffset,
+            pressInteraction,
+            onLongClick,
+            onDoubleClick
+        )
 
-        val gesture =
-            Modifier.pointerInput(interactionSource, hasLongClick, hasDoubleClick, enabled) {
-                centreOffset.value = size.center.toOffset()
-                detectTapGestures(
-                    onDoubleTap = /**/,
-                    onLongPress = /**/,
-                    onPress = /**/,
-                    onTap = /**/
-                )
-            }
         Modifier
             .genericClickableWithoutGesture(
-                gestureModifiers = gesture,
                 interactionSource = interactionSource,
                 indication = indication,
                 indicationScope = rememberCoroutineScope(),
@@ -368,6 +337,7 @@
                 onLongClick = onLongClick,
                 onClick = onClick
             )

相较而言,一些 State 被移除,pointerInput 从原有的 Modifier.pointerInput 改为了 CombinedClickablePointerInputElement,而这个类的实现如下:

diff 复制代码
+private class ClickablePointerInputElement(
+    private val enabled: Boolean,
+    private val interactionSource: MutableInteractionSource,
+    private val onClick: () -> Unit,
+    private val centreOffset: MutableState<Offset>,
+    private val pressInteraction: MutableState<PressInteraction.Press?>
+) : ModifierNodeElement<ClickablePointerInputNode>() {
+    override fun create(): ClickablePointerInputNode = ClickablePointerInputNode(
+        enabled,
+        interactionSource,
+        onClick,
+        centreOffset,
+        pressInteraction
+    )
+
+    override fun update(node: ClickablePointerInputNode) = node.also {
+        it.updateParameters(enabled, interactionSource, onClick)
+    }
+
+   // omit codes like equals, hashCode, toString
+}
+
+private class CombinedClickablePointerInputElement(
+    private val enabled: Boolean,
+    private val interactionSource: MutableInteractionSource,
+    private val onClick: () -> Unit,
+    private val centreOffset: MutableState<Offset>,
+    private val pressInteraction: MutableState<PressInteraction.Press?>,
+    private val onLongClick: (() -> Unit)?,
+    private val onDoubleClick: (() -> Unit)?
+) : ModifierNodeElement<CombinedClickablePointerInputNode>() {
+    override fun create(): CombinedClickablePointerInputNode = CombinedClickablePointerInputNode(
+        enabled,
+        interactionSource,
+        onClick,
+        centreOffset,
+        pressInteraction,
+        onLongClick,
+        onDoubleClick
+    )
+
+    override fun update(node: CombinedClickablePointerInputNode) = node.also {
+        it.updateParameters(enabled, interactionSource, onClick, onLongClick, onDoubleClick)
+    }
+
+    // omit codes like equals, hashCode, toString
+}
+

可以看到,原先的几个 State 被合并到了一个 CombinedClickablePointerInputElement 中,而原先的 Modifier.pointerInput 则被拆分成了两个 Modifier,一个是 CombinedClickablePointerInputElement,另一个是 ClickablePointerInputElement,这两个 Modifier 都实现了 ModifierNodeElement 接口,这个接口的作用是用来创建和更新 Modifier.Node 部分源码如下:

kotlin 复制代码
/**
 * 一个 [Modifier.Element],用于管理特定 [Modifier.Node] 实现的实例。只有在将创建和更新该实现的 [ModifierNodeElement] 应用于布局时,才能使用给定的 [Modifier.Node] 实现。
 *
 * [ModifierNodeElement] 应该非常轻量级,除了保存创建和维护关联的 [Modifier.Node] 类型实例所需的信息外,几乎不做其他工作。
 *
 */
abstract class ModifierNodeElement<N : Modifier.Node> : Modifier.Element, InspectableValue {

    /** 省略一些 Inspect 相关的代码 */

    /**
     * 在第一次将 Modifier 应用于布局时将调用此函数,应构造并返回相应的 [Modifier.Node] 实例。
     */
    abstract fun create(): N

    /**
     * 当将 Modifier 应用于输入与上次应用不同的布局时调用。此函数将以当前节点实例作为参数传入,预期该节点将被更新到最新状态。
     */
    abstract fun update(node: N)

    // 省略一些检查器相关的代码、hashCode、equals 等
}

如果我们观察一下它的几个实现,会发现 create 方法用于新建一个 Modifier.Node 实例,而 update 方法则用于更新这个实例。

kotlin 复制代码
// ClickableElement
private class ClickableElement(
    private val interactionSource: MutableInteractionSource,
    private val enabled: Boolean,
    private val onClickLabel: String?,
    private val role: Role? = null,
    private val onClick: () -> Unit
) : ModifierNodeElement<ClickableNode>() {
    override fun create() = ClickableNode(
        interactionSource,
        enabled,
        onClickLabel,
        role,
        onClick
    )

    override fun update(node: ClickableNode) {
        node.update(interactionSource, enabled, onClickLabel, role, onClick)
    }
}

// LayoutElement
fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this then LayoutElement(measure)

private data class LayoutElement(
    val measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) : ModifierNodeElement<LayoutModifierImpl>() {
    override fun create() = LayoutModifierImpl(measure)

    override fun update(node: LayoutModifierImpl) {
        node.measureBlock = measure
    }
}

internal class LayoutModifierImpl(
    var measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult
) : LayoutModifierNode, Modifier.Node() {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ) = measureBlock(measurable, constraints)

    override fun toString(): String {
        return "LayoutModifierImpl(measureBlock=$measureBlock)"
    }
}

而作为对比,早期的 Modifier.layout 是这样的

kotlin 复制代码
fun Modifier.layout(
    measure: MeasureScope.(Measurable, Constraints) -> MeasureResult
) = this.then(
    // 这里直接创建了一个 LayoutModifierImpl,而新版是通过 LayoutElement 来管理的
    LayoutModifierImpl(
        measureBlock = measure,
        inspectorInfo = debugInspectorInfo {
            name = "layout"
            properties["measure"] = measure
        }
    )
)

private class LayoutModifierImpl(
    val measureBlock: MeasureScope.(Measurable, Constraints) -> MeasureResult,
    inspectorInfo: InspectorInfo.() -> Unit,
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ) = measureBlock(measurable, constraints)

    // 省略 hashCode、equals、toString 等
}

看起来,二者的区别就是早期的 Modifier.layout 直接创建了一个 LayoutModifierImpl,而新版则是通过 LayoutElement (ModifierNodeElement) 来管理的。这个 API 自 Compose 1.3.0-beta01 引入,具体来说是 这个 Commit。而实际上,二者在工作细节上已经有了很大变化。相较于原始的版本,新的 LayoutModifierImpl 改为继承自 Modifer.Node,并且实现了 LayoutModifierNode 接口。

你可能很好奇,这样的变更到底有什么用呢?要了解这个问题,我十分推荐你去观看负责这部分更改的团队成员所做的解释:Compose Modifiers deep dive(如果你感兴趣,可以留个言,我也会把它翻译成文章)。直观点来说,对于下面这个简单的 Composable

由于高级别 Modifier 实际依赖于单个或多个低级别 Modifier,而且有些 Modifier 还会持有状态,在旧的实现中,通过 Modifier.materialize 方法展开后,上面的 Composable 会被展开成下面这样的结构

这还不是全部,只是再展开屏幕放不下了 😂

而在新的实现中,通过 Modifier.Node 结构,每一个 Modifier 会被对应成一个 Node (也就是通过 ModifierNodeElement::create 创建,ModifierNodeElement::update 更新)。从结构上,它就能被缩减为

新版下 Compose Tree 的大致模型

更多细节,可以自行参阅源码

内存占用

关于内存的优化,我们截取 compose.animation 的一些变化来看看

Removed allocations in recomposition, color animations, and AndroidComposeView (Ib2bfa)

替换局部函数

下面是 commit 的注释

  • 在组合中删除了最大的分配源(365个实例,也是其他测试中最大的源)。addPendingInvalidationsLocked() 使用了一个方法局部函数,导致每次调用时都会创建一个 Ref$ObjectRef。此更改将该函数升级为一个方法,接收必要的参数,并返回以前直接分配给 addPendingInvalidationsLocked() 中的被无效变量的值。

旧的

kotlin 复制代码
private fun addPendingInvalidationsLocked(values: Set<Any>, forgetConditionalScopes: Boolean) {
    var invalidated: HashSet<RecomposeScopeImpl>? = null

    fun invalidate(value: Any) {
        // 省略具体实现
    }

    values.fastForEach { value ->
        if (value is RecomposeScopeImpl) {
            value.invalidateForResult(null)
        } else {
            // 这里调用了局部函数
            invalidate(value)
            derivedStates.forEachScopeOf(value) {
                invalidate(it)
            }
        }
    }
}

新的

kotlin 复制代码
// 原本的局部函数被分离为了一个 private 的扩展函数
private fun HashSet<RecomposeScopeImpl>?.addPendingInvalidationsLocked(
        value: Any,
        forgetConditionalScopes: Boolean
    ): HashSet<RecomposeScopeImpl>? {
    var set = this
    // 省略具体实现
    return set
}

private fun addPendingInvalidationsLocked(values: Set<Any>, forgetConditionalScopes: Boolean) {
    var invalidated: HashSet<RecomposeScopeImpl>? = null

    values.fastForEach { value ->
        if (value is RecomposeScopeImpl) {
            value.invalidateForResult(null)
        } else {
            // 这里调用了上面的函数
            invalidated =
                invalidated.addPendingInvalidationsLocked(value, forgetConditionalScopes)
            derivedStates.forEachScopeOf(value) {
                invalidated =
                    invalidated.addPendingInvalidationsLocked(it, forgetConditionalScopes)
            }
        }
    }
}

从代码上无法直观看出,其实秘密藏在编译后。我们举个栗子:

kotlin 复制代码
// 旧的
fun foo() {
    var invalidated: HashSet<Any>? = null
    fun bar() {
        invalidated = HashSet()
    }
    bar()
    invalidated?.add("1")
}

// 新的
fun foo2(value: Any) {
    var invalidated: HashSet<Any>? = null
    invalidated = invalidated?.bar2(value)
}

private fun HashSet<Any>.bar2(value: Any): HashSet<Any> {
    val set = this
    set.add(value)
    return set
}

看起来差不多,但是反编译后却大相径庭

java 复制代码
public final void foo() {
    final Ref.ObjectRef invalidated = new Ref.ObjectRef();
    invalidated.element = null;
    <undefinedtype> $fun$bar$1 = new Function0() {
        // $FF: synthetic method
        // $FF: bridge method
        public Object invoke() {
        this.invoke();
        return Unit.INSTANCE;
        }

        public final void invoke() {
        invalidated.element = new HashSet();
        }
    };
    $fun$bar$1.invoke();
    HashSet var10000 = (HashSet)invalidated.element;
    if (var10000 != null) {
        var10000.add("1");
    }

}

public final void foo2(@NotNull Object value) {
    Intrinsics.checkNotNullParameter(value, "value");
    HashSet invalidated = null;
    invalidated = null;
}

private final HashSet bar2(HashSet $this$bar2, Object value) {
    $this$bar2.add(value);
    return $this$bar2;
}

旧的实现中怎么莫名其妙多出了一个 Ref.ObjectRefFunction0

  1. Ref.ObjectRef 是局部函数 bar 的闭包,因为 bar 里面用到了 invalidated,所以 invalidated 会被编译成一个 Ref.ObjectRef,而 Ref.ObjectRef 会被传入 bar 中,这样 bar 就能修改 foo 中的 invalidated 了。
  2. Function0:这是一个函数类型的匿名内部类,用于封装嵌套函数 bar() 的代码。在 Java 字节码中,函数类型被表示为接口和匿名类的组合。在这里,编译器生成了一个实现了 Function0 接口的匿名内部类,该接口代表一个没有参数和返回值的函数。这个匿名内部类的 invoke() 方法中放置了 bar() 函数的代码。

这就是局部函数(可能会产生)的代价。而新的实现中,我们只需要一个 bar2 函数,就能完成同样的功能。这个小变化确实带来了内存开销的优化,如果各位老铁们有对内存开销非常敏感的场景,也可以考虑使用这种方式来替换局部函数。

"MutableList +=" -> "for { add }"

旧的

kotlin 复制代码
// toComplete 是一个 MutableSet<> (实际为 LinkedHashSet),而 toApply 是一个 MutableList<> (实际为 ArrayList)
// val toApply = mutableListOf<ControlledComposition>()
// val toComplete = mutableSetOf<ControlledComposition>()
toComplete += toApply
toApply.fastForEach { composition ->
    composition.applyChanges()
}

新的

kotlin 复制代码
// We could do toComplete += toApply but doing it like below
// avoids unncessary allocations since toApply is a mutable list
// toComplete += toApply
toApply.fastForEach { composition ->
    toComplete.add(composition)
}
toApply.fastForEach { composition ->
    composition.applyChanges()
}

其中的 fastForEach 是一个内联函数,它的实现如下

kotlin 复制代码
/**
 * 通过 index 来遍历 [List],并且对每一个 item 调用 [action]。
 * 这不会像 [Iterable.forEach] 那样分配一个 iterator。
 */
internal inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
    contract { callsInPlace(action) }
    for (index in indices) {
        val item = get(index)
        action(item)
    }
}

如上,区别就是把 += 操作改成了 for 循环。那么这个 += 操作到底做了什么呢?我们来看一下它的实现

kotlin 复制代码
@kotlin.internal.InlineOnly
public inline operator fun <T> MutableCollection<in T>.plusAssign(elements: Iterable<T>) {
    this.addAll(elements)
}

可以看到,它实际上是调用了 MutableCollection.addAll 方法。通过 debug 发现,这个 MutableCollection 实际上是一个 LinkedHashSet,而 addAll 方法的实现如下

java 复制代码
public boolean addAll(Collection<? extends E> c) {
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

内部实际上是通过 for 循环来遍历,然后调用 add 方法来添加元素。而查看它们反编译后的字节码,确实也是类似的情况:

java 复制代码
// += 操作
CollectionsKt.addAll(var23, var24);

// for 循环
for(var52 = ((Collection)toApplyNew).size(); index$iv < var52; ++index$iv) {
    item$iv = $this$fastForEach$iv.get(index$iv);
    composition = (String)item$iv;
    var33 = false;
    toCompleteNew.add(composition);
}

所以我就很好奇,这样的变化实际运行起来又是怎样的呢?

借助 ChatGPT 的帮助,我写了一个简单的测试,来对比一下这两种方式的性能差异

kotlin 复制代码
import org.junit.Test
import kotlin.system.measureNanoTime

class MemoryAllocationTest {
    @Test
    fun test() {
        val iterations = 1000 // Adjust the number of iterations as needed

        // Test the old implementation
        var oldTimeUsage = 0L
        val oldMemoryUsage = measureMemoryUsage {
            repeat(iterations) {
                val toComplete: MutableSet<String> = mutableSetOf("A", "B", "C")
                val toApply: MutableList<String> = mutableListOf("D", "E", "F")

                oldTimeUsage += measureNanoTime {
                    toComplete += toApply
                    toApply.fastForEach { composition ->
                        composition.applyChanges()
                    }
                }
            }
        }

        // Test the new implementation
        var newTimeUsage = 0L
        val newMemoryUsage = measureMemoryUsage {
            repeat(iterations) {
                val toCompleteNew: MutableSet<String> = mutableSetOf("A", "B", "C")
                val toApplyNew: MutableList<String> = mutableListOf("D", "E", "F")

                newTimeUsage += measureNanoTime {
                    toApplyNew.fastForEach { composition ->
                        toCompleteNew.add(composition)
                    }
                    toApplyNew.fastForEach { composition ->
                        composition.applyChanges()
                    }
                }
            }
        }

        println("Old time usage: $oldTimeUsage, new time usage: $newTimeUsage, ratio: ${oldTimeUsage.toDouble() / newTimeUsage}")
        println("Old memory usage: $oldMemoryUsage, new memory usage: $newMemoryUsage, ratio: ${oldMemoryUsage.toDouble() / newMemoryUsage}")
    }

    @Test
    fun test5times(){
        repeat(5) {
            Runtime.getRuntime().gc()
            test()
            println()
        }
    }

    private inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
        for (index in indices) {
            val item = get(index)
            action(item)
        }
    }

    private fun String.applyChanges() {
        // Simulate applying changes to the string
    }

    private inline fun measureMemoryUsage(block: () -> Unit): Long {
        val runtime = Runtime.getRuntime()
        val before = runtime.freeMemory()
        block()
        val after = runtime.freeMemory()
        return before - after
    }
} 

测试结果如下

text 复制代码
Old time usage: 2060200, new time usage: 846300, ratio: 2.434361337587144
Old memory usage: 3921656, new memory usage: 547376, ratio: 7.164464645874134

Old time usage: 846600, new time usage: 898700, ratio: 0.9420273728719261
Old memory usage: 545328, new memory usage: 414432, ratio: 1.315844336344684

Old time usage: 595700, new time usage: 940100, ratio: 0.6336559940431868
Old memory usage: 503392, new memory usage: 589192, ratio: 0.8543768415049763

Old time usage: 640500, new time usage: 670500, ratio: 0.9552572706935123
Old memory usage: 587296, new memory usage: 463344, ratio: 1.267516143513243

Old time usage: 541900, new time usage: 772800, ratio: 0.7012163561076604
Old memory usage: 587288, new memory usage: 463368, ratio: 1.267433228017472

我在 Kotlin 1.8.10 上运行了多次,结果均有类似的情况。第一次测试,新的实现在内存和时间上都有明显的优势,但是后续的测试,内存上的优势就不明显了,时间上反而经常取得劣势。这里也请教一下各位大佬,这是什么原因呢?

结尾与实测

文章写到此已经非常长了,不知不觉花了我一天半的时间。Jetpack Compose 一直因为列表性能问题的差距而被人诟病,而如今这一点点问题也在逐步越变越好。

Jetpack Compose 构建的应用,现在用起来到底怎么样,我想只有亲身体验后才更有发言权。我自己的开源应用 译站 已经全局使用 Jetpack Compose 一年半,我也在昨天升级到了 Compose BOM 2023.08.00,感兴趣的同学可以到仓库的 release 下载体验 (官网上的版本没有更新,只有 Github 仓库的这个文件是更新到了 Compose 的最新稳定版)。例如,其中的 "致谢" 就是分页动态加载的长列表,滚动起来非常丝滑。

相关推荐
你过来啊你2 小时前
Android Handler机制与底层原理详解
android·handler
RichardLai883 小时前
Kotlin Flow:构建响应式流的现代 Kotlin 之道
android·前端·kotlin
AirDroid_cn3 小时前
iQOO手机怎样相互远程控制?其他手机可以远程控制iQOO吗?
android·智能手机·iphone·远程控制·远程控制手机·手机远程控制手机
YoungHong19923 小时前
如何在 Android Framework层面控制高通(Qualcomm)芯片的 CPU 和 GPU。
android·cpu·gpu·芯片·高通
xzkyd outpaper3 小时前
Android 事件分发机制深度解析
android·计算机八股
努力学习的小廉3 小时前
深入了解linux系统—— System V之消息队列和信号量
android·linux·开发语言
程序员江同学4 小时前
Kotlin/Native 编译流程浅析
android·kotlin
移动开发者1号5 小时前
Kotlin协程与响应式编程深度对比
android·kotlin
花花鱼14 小时前
android studio 设置让开发更加的方便,比如可以查看变量的类型,参数的名称等等
android·ide·android studio