昨日,在 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 商店、Dropbox 和 Airbnb)所运用。此版别主要专心于功用改善,由于咱们在 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 也现已稳定下来,包含:

  • TextMeasurer and 相关 APIs
  • LineHeightStyle.Alignment(topRatio)
  • Brush
  • DrawStyle
  • TextMotion
  • DrawScope.drawText
  • Paragraph.paint (brush, drawStyle, blendMode)
  • MultiParagraph.paint (brush, drawStyle, blendMode)
  • PlatformTextInput

中心功用的改善和修正

咱们还在中心 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…

 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,而这个类的完结如下:

+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 部分源码如下:

/**
 * 一个 [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 办法则用于更新这个实例。


// 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 是这样的

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

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

由于高档别 Modifier 实践依赖于单个或多个初等级 Modifier,并且有些 Modifier 还会持有状况,在旧的完结中,经过 Modifier.materialize 办法打开后,上面的 Composable 会被打开成下面这样的结构

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

这还不是全部,仅仅再打开屏幕放不下了

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

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

新版下 Compose Tree 的大致模型

更多细节,能够自行参阅源码

内存占用

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

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

替换部分函数

下面是 commit 的注释

  • 在组合中删除了最大的分配源(365个实例,也是其他测验中最大的源)。addPendingInvalidationsLocked() 运用了一个办法部分函数,导致每次调用时都会创立一个 Ref$ObjectRef。此更改将该函数晋级为一个办法,接纳必要的参数,并回来曾经直接分配给 addPendingInvalidationsLocked() 中的被无效变量的值。

旧的

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)
            }
        }
    }
}

新的

// 本来的部分函数被别离为了一个 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)
            }
        }
    }
}

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

// 旧的
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
}

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

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 }”

旧的

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

新的

// 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 是一个内联函数,它的完结如下

/**
 * 经过 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.internal.InlineOnly
public inline operator fun <T> MutableCollection<in T>.plusAssign(elements: Iterable<T>) {
    this.addAll(elements)
}

能够看到,它实践上是调用了 MutableCollection.addAll 办法。经过 debug 发现,这个 MutableCollection 实践上是一个 LinkedHashSet,而 addAll 办法的完结如下

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

内部实践上是经过 for 循环来遍历,然后调用 add 办法来添加元素。而检查它们反编译后的字节码,的确也是相似的状况:

// += 操作
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 的协助,我写了一个简单的测验,来比照一下这两种办法的功用差异

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
    }
} 

测验结果如下

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 的最新稳定版)。例如,其间的 “称谢” 便是分页动态加载的长列表,翻滚起来十分丝滑。