• Jetpack Compose 1.5 发布:全新 Modifier 系统助力性能提升


    不久前 Compose 1.5.0 稳定版发布,在组合的性能方面得到明显改善,这主要归功于对 Modifier API 的持续重构。

    Modifier 是 Compose 中的重要概念,为 Composition 中的 LayoutNode 配置各种样式信息以用于后续渲染。在 1.3.0 之前的 Modifier 在实现机制上存在性能问题,从 1.3.0 起 Modifier 系统得到重构,通过 Modifier.Node 的引入,性能大幅提升。从 1.3.0 起,既有的 Modifier 操作符逐渐重构到新的架构。

    1.3.0 之前的 Modifier

    通常,我们会为 Composable 像 modifier = Modifier.xxx 这样添加多个 Modifier 操作符

    @Composable
    fun Box(
        modifier = Modifier
            .background(..)
            .size(..)
            .clickable(..)
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    操作符内部会通过 composed {} 生成 ComposedModifier

    以下以 .clickable() 为例:

    fun Modifier.clickable(..onClick: () -> Unit): Modifier = composed {
        val onClickState = rememberUpdatedState(onClick)
        val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
        ..
    
        return this
            ..
            .composed { remember { FocusRequesterModifier() } } 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ComposedModifier 会通过 Composer.materialize() 进一步展开 Modifier.Element 的链式结构,在 Element 构建过程中(factory 调用),remember { } 将必要的状态作为 LayoutNode 的关联信息存入 Composition

    /** 
    * Materialize any instance-specific [composed modifiers][composed] for applying to a raw tree node. 
    * Call right before setting the returned modifier on an emitted node. 
    * You almost certainly do not need to call this function directly. 
    */
    
    @Suppress("ModifierFactoryExtensionFunction")
    fun Composer.materialize(modifier: Modifier): Modifier {
         val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
              acc.then(
                  val factory = element.factory as Modifier.(Composer, Int) -> Modifier
                  val composedMod = factory(Modifier, this, 0)
                  materialize(composedMod)
              )
              ..
         }
    
         return result
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    composed {}Composer.materialize() 的初衷是可以 Just-In-Time 地为不同 LayoutNode 生成不同的状态。即使是共用同一个 Modifier ,例如下面这样的 case ,虽然共用 m,但是 Content1onClick 带来的水波纹效果不应该影响到 Content2

    @Composable
    fun App() {
        val m = Modifier
            .padding(10.dp)
            .background(Color.Blue)
            .clickable {...}
        Row {
            Content1(m)
            Content2(m)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    但是,这样的机制也带来了性能问题。Modifier 现场展开 Modifier.Element 数量远多于表面上看到的操作符数量,这个过程会造成大量内存的开销和更高的 GC 概率。而且 Modifier 的调用处不在 Composable 中,无法智能的跳过重组,每次重组都会执行。

    Modifier.Node 的引入

    解决上述问题的思路,一是减少 Modifier.Element 的创建数量,二是让 Modifier 的状态也可以参与比较,更智能地重组。 因此,Compose 引入了 Modifier.Node

    interface Modifier {
        /**     
        * The longer-lived object that is created for each [Modifier.Element] applied to a     
        * [androidx.compose.ui.layout.Layout]..     
        */
        abstract class Node : DelegatableNode, OwnerScope {
            final override var node: Node = this
            private setinternal var parent: Node? = null
            internal var child: Node? = null
            
            ..
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    新的系统下,每个 Modifier 操作符不会再生成过多的 Modifier.Element,只会生成一一对应的 Modifier.Node

    总体来说 Modifier.Node 带来三个好处:

    • 更少的分配: 生成的 Element 的数量大大降低,避免了内存抖动和内存占用。

    • 更轻量的树:状态存在 Node 上,不再依靠 remember {} 存储,Composition 的节点数也随之减少,树的遍历速度也更快了。

    • 更快的重组:Modifier.Node 为重组提供可比较标的物, 非必要不重新生成 Node,重组性能得到提升。

    迁移到新的 Modifier 系统

    Compose 预置的 Modifier 操作符会逐渐迁移到新系统,对于上层使用没有影响,对于自定义 Modifier 如何向新系统迁移呢?以下以 roundRectangle 举例,这个操作符用来绘制带颜色的圆角矩形

    // Draw round rectangle with a custom color
    fun Modifier.roundRectangle(
        color: Color
    ) = composed {
        // when color changed,create and return new RoundRectanleModifier instance
        val modifier = remember(color) {
            RoundRectangleModifier(color = color)
        }
        return modifier
    }
    
    // implement DrawModifier 
    class RoundRectangleModifier(
        private val color: Color,
    ): DrawModifier {
        override fun ContentDrawScope.draw() {
            drawRoundRect(
                color = color,
                cornerRadius = CornerRadius(8.dp.toPx(), 8.dp.toPx())
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    调用效果如下:

    val color by animateColorAsState(..)
    
    Box(modifier = Modifier..roundRectangleNode(color = color)) { .. }
    
    • 1
    • 2
    • 3

    Step 1 :定义 Modifier.Node

    首先,创建 RoundRectangleModifierNode ,实现自 Modifier.Node,,同之前的 RoundRectangleModifier 签名保持一致即可

    @OptIn(ExperimentalComposeUiApi::class)
    class RoundRectangleModifierNode(
        var color: Color,
    ): Modifier.Node()
    
    • 1
    • 2
    • 3
    • 4

    注意参数 color 用 var 而非 val,理由稍后说明

    Step 2:实现 DrawModifierNode

    第二步为 Modifier 提供原本的绘画能力,需要添加接口 DrawModifierNode,并像 DrawModifier 一样重写 ContentDrawScope.draw 方法:

    class RoundRectangleNodeModifier(
        var color: Color,
    ): DrawModifierNode, .. {
        override fun ContentDrawScope.draw() {
            drawRoundRect(
                color = color,
                cornerRadius = CornerRadius(8.dp.toPx(), 8.dp.toPx())
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个 DrawModifierNode 实现自 DelegatableNode,后者是对 Modifier.Node 的包装,顾名思义起到代理的作用

    /** 
    * Represents a [Modifier.Node] which can be a delegate of another [Modifier.Node]. Since 
    * [Modifier.Node] implements this interface, in practice any [Modifier.Node] can be delegated. 
    */
    @ExperimentalComposeUiApi
    interface DelegatableNode {
        val node: Modifier.Node
    }
    
    interface DrawModifierNode : DelegatableNode {
        fun ContentDrawScope.draw()
        ..
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    类似的代理类还有很多

    /** ..
     * @see androidx.compose.ui.node.LayoutModifierNode
     * @see androidx.compose.ui.node.DrawModifierNode
     * @see androidx.compose.ui.node.SemanticsModifierNode
     * @see androidx.compose.ui.node.PointerInputModifierNode
     * @see androidx.compose.ui.node.ParentDataModifierNode
     * @see androidx.compose.ui.node.LayoutAwareModifierNode
     * @see androidx.compose.ui.node.GlobalPositionAwareModifierNode
     * @see androidx.compose.ui.node.IntermediateLayoutModifierNode
     */
     @ExperimentalComposeUiApi
     abstract class Node : DelegatableNode, OwnerScope { .. }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Step 3:创建 ModifierNodeElement

    新系统下,Element 生成的数量会大大减少,但并非没有。我们仍然需要创建 Node 数量匹配的 Element,即 ModifierNodeElement 。它需要在重组中参与 Node 比较的。Modifier.roundRectangle 的定义的核心就是使用 modifierElementOf 构建 ModifierNodeElement 。

    @OptIn(ExperimentalComposeUiApi::class)
    fun Modifier.roundRectangle(
        color: Color
    ) = this then modifierElementOf(
            params = color,
            create = { RoundRectangleModifierNode(color) },
            update = { currentNode -> currentNode.color = color },
            definitions = ..
        )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里的几个关键参数比较好懂,简单说明如下:

    • param : 标识 Modifier 是否变化的参数 。当该参数发生变化时,将调用 update 回调函数。类似于前面 RoundRectangleModifier 的例子中的 remember 的用途

    • create: 用于创建新的 Modifier.Node ,我们可以在此处编写 Modifier.Node 的初始化逻辑。

    • update: 用于更新 Modifier.Node 实例。当前的 Modifier.Node 实例将作为参数传递给此回调函数,并返回更新后的 Modifier.Node。在这里,可以重新设置颜色( 为什么设置成 var ),并在下次绘制时使用更新后的颜色

    最新进展 (截止到 2023/08)

    众所周知,Compose 库自底向上分为多层,Modifier API 重构也是在自底向上稳步进行,目前 Compose UI 的全部 和 Compose Foundation 中的低级别 Modifier API 都完成了迁移,开始向更多高级 Modifier API 覆盖。1.5.0 中覆盖率进一步扩大,Clickable 这样的常用 API 也完成了迁移,因此才带了明显的性能提升,在某些情况下甚至提高了 80%,预计不久将来 Modifier 完成全部迁移时, Compose 的性能定会上一个新台阶。

    参考

    • https://www.youtube.com/watch?v=BjGX2RftXsU
    • https://goodpatch-tech.hatenablog.com/entry/modifier-node-summary
    • https://android-developers.googleblog.com/2023/08/whats-new-in-jetpack-compose-august-23-release.html
  • 相关阅读:
    【31】GPU(下):为什么深度学习需要使用GPU?
    浅谈 JVM GC 收集器--系列(一)
    ubuntu18.4(后改为20.4)部署chatglm2并进行基于 P-Tuning v2 的微调
    在非IOS系统打开HEIC格式图片
    2023-09-22力扣每日一题
    AI人工智能老师大模型讲师叶梓 OneLLM:开创性的多模态大型语言模型技术
    基于django电影推荐系统
    golang pprof 监控系列(3) —— memory,block,mutex 统计原理
    C语言 实现贪吃蛇 | 十分钟入门案例 | 初学者案例 | 附带设计思路 + 代码 + 图文分析
    力扣第77题 组合 c++ 回溯经典题 注释加优化 代码
  • 原文地址:https://blog.csdn.net/vitaviva/article/details/132797385