Jetpack Compose 1.5 released: new Modifier system helps improve performance

Not long ago, the stable version of Compose 1.5.0 was released, which has significantly improved the performance of composition, mainly due to the continuous reconstruction of the Modifier API.

Modifier is an important concept in Compose, which Compositionconfigures LayoutNodevarious style information for subsequent rendering. Modifier before 1.3.0 had performance problems in its implementation mechanism. Starting from 1.3.0, the Modifier system has been reconstructed. Through the Modifier.Nodeintroduction of , the performance has been greatly improved. Starting from 1.3.0, the existing Modifier operator is gradually reconstructed to the new architecture.

Modifier before 1.3.0

modifier = Modifier.xxxNormally, we would add multiple Modifier operators to Composable like this

@Composable
fun Box(
    modifier = Modifier
        .background(..)
        .size(..)
        .clickable(..)
)

The operator will be composed {}generated internally by ComposedModifier.

The following takes as .clickable()an example :

fun Modifier.clickable(..onClick: () -> Unit): Modifier = composed {
    
    
    val onClickState = rememberUpdatedState(onClick)
    val pressedInteraction = remember {
    
     mutableStateOf<PressInteraction.Press?>(null) }
    ..

    return this
        ..
        .composed {
    
     remember {
    
     FocusRequesterModifier() } } 
}

And will be further expanded ComposedModifierthrough the chain structure of Element during the construction process (factory call), and the necessary status will be stored in Composition as the associated information ofComposer.materialize()Modifier.Elementremember { }LayoutNode

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

composed {}The original intention of and Composer.materialize()is to generate different states for different Just-In-Time LayoutNode. Even if they share the same Modifier, such as the following case, although m is shared, the water ripple effect brought Content1by onClickshould not be affectedContent2

@Composable
fun App() {
    
    
    val m = Modifier
        .padding(10.dp)
        .background(Color.Blue)
        .clickable {
    
    ...}
    Row {
    
    
        Content1(m)
        Content2(m)
    }
}

However, such a mechanism also brings performance issues. The number of Modifier on-site expansion Modifier.Elementis much more than the number of operators seen on the surface. This process will cause a lot of memory overhead and a higher GC probability. Moreover, the call point of Modifier is not in Composable, so the reorganization cannot be skipped intelligently, and each reorganization will be executed.

Introduction of Modifier.Node

The ideas to solve the above problems are, first, to reduce Modifier.Elementthe number of creations, and second, to allow the status of Modifier to participate in comparison and reorganize more intelligently. Therefore, Compose introducesModifier.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
        
        ..
    }
}

Under the new system, each Modifier operator will no longer generate too many Modifier.Element, only one-to-one corresponding Modifier.Node will be generated.

Generally speaking, Modifier.Nodeit brings three benefits:

  • Fewer allocations : The number of generated Elements is greatly reduced, avoiding memory jitter and memory usage.

  • Lighter tree : The state is stored on the Node and no longer relies on remember {} storage. The number of nodes in the Composition is also reduced, and the tree traversal speed is also faster.

  • Faster reorganization : Modifier.Node provides a comparable target for reorganization, Node is not regenerated unless necessary, and reorganization performance is improved.

Migrate to the new Modifier system

Compose's preset Modifier operators will be gradually migrated to the new system, and will have no impact on upper-layer usage. How to migrate custom Modifiers to the new system? The following is roundRectanglean example. This operator is used to draw a colored rounded rectangle.

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

The effect of the call is as follows:

val color by animateColorAsState(..)

Box(modifier = Modifier..roundRectangleNode(color = color)) {
    
     .. }

Step 1: Define Modifier.Node

First, create RoundRectangleModifierNodeand implement self , just keep it consistent Modifier.Nodewith the previous signature.RoundRectangleModifier

@OptIn(ExperimentalComposeUiApi::class)
class RoundRectangleModifierNode(
    var color: Color,
): Modifier.Node()

Note that the parameter color is used varinstead of val, the reason will be explained later.

Step 2: Implement DrawModifierNode

The second step is to provide Modifier with its original painting capabilities. You need to add an interface DrawModifierNodeand DrawModifierrewrite ContentDrawScope.drawthe method like:

class RoundRectangleNodeModifier(
    var color: Color,
): DrawModifierNode, .. {
    
    
    override fun ContentDrawScope.draw() {
    
    
        drawRoundRect(
            color = color,
            cornerRadius = CornerRadius(8.dp.toPx(), 8.dp.toPx())
        )
    }
}

This DrawModifierNodeis implemented since DelegatableNode, which is Modifier.Nodea packaging for and acts as an agent as the name suggests

/** 
* 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()
    ..
}

There are many similar proxy classes

/** ..
 * @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 {
    
     .. }

Step 3: Create ModifierNodeElement

Under the new system, the number of Elements generated will be greatly reduced, but not none. We still need to create an Element with a matching number of Node, that is, ModifierNodeElement. It needs to participate in Node comparison during reorganization. The core of the definition of Modifier.roundRectangle is to use modifierElementOf to construct ModifierNodeElement.

@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.roundRectangle(
    color: Color
) = this then modifierElementOf(
        params = color,
        create = {
    
     RoundRectangleModifierNode(color) },
        update = {
    
     currentNode -> currentNode.color = color },
        definitions = ..
    )

Several key parameters here are relatively easy to understand, and are briefly explained as follows:

  • param: A parameter that identifies whether the Modifier changes. When this parameter changes, the update callback function will be called. Similar to the use of remember in the previous example of RoundRectangleModifier

  • create: Used to create a new Modifier.Node, where we can write the initialization logic of Modifier.Node.

  • update: Used to update Modifier.Node instances. The current Modifier.Node instance will be passed as a parameter to this callback function, and the updated Modifier.Node will be returned. Here, you can reset the color (why it is set to var) and use the updated color the next time you draw

Latest progress (as of 2023/08)

As we all know, the Compose library is divided into multiple layers from the bottom up, and the Modifier API reconstruction is also progressing steadily from the bottom up. At present, Compose UIall Compose Foundationlow-level Modifier APIs in and have been migrated and are beginning to be covered by more advanced Modifier APIs. The coverage in 1.5.0 has been further expanded, and Clickablesuch commonly used APIs have also been migrated, which has resulted in significant performance improvements, in some cases even an increase of 80%. It is expected that when Modifier completes all migrations in the near future, the performance of Compose will It will definitely reach a new level.

reference

  • 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

Supongo que te gusta

Origin blog.csdn.net/vitaviva/article/details/132797385
Recomendado
Clasificación