preface

Those of you who have done layout performance optimization know that in order to optimize the loading speed of the interface, you need to reduce the layout level as much as possible. This is mainly because the increase in layout levels can lead to an exponential increase in measurement time. Compose does not have this problem, and it fundamentally addresses the impact of the layout hierarchy on layout performance: the Compose interface allows only one measurement. This means that the measurement time increases only linearly as the layout level increases. Here’s a look at how Compose managed to get the job done just once:

  1. How does too deep a layout hierarchy affect performance?
  2. ComposeWhy is there no layout nesting problem?
  3. ComposeMeasurement process source code analysis

1. How does the layout hierarchy too deep affect performance?

We always say that having a deep layout hierarchy affects performance, but how does it affect performance? The main reason is that in some cases a ViewGroup is going to make multiple measurements of its subviews for example


      
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <View
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:background="@android:color/holo_red_dark" />

    <View
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:background="@android:color/black" />
</LinearLayout>
Copy the code
  1. LinearLayoutWidth iswrap_contentSo it’s going to select the sonViewThe maximum width of is its final width
  2. But it has a childViewThe width ismatch_parent, meaning it will takeLinearLayoutThe width of is the width, which is in an infinite loop
  3. So at this point,LinearLayoutWill start with0Measure for forced width all at onceViewAnd normally measure the remaining particlesViewAnd then you can use the other onesViewThe width of the widest one in. Measure this twicematch_parentThe son ofView, and finally get its size, and take this width as their final width.
  4. This is for a singletonViewThe second measure of phi, if it has more than one dimensionViewTo write amatch_parentAnd then you have to do a secondary measurement on each of them.
  5. In addition, if inLinearLayoutThe use of theweightCan result in 3 or more measurements, repeated measurements inAndroidIs very common

The above explains why repeated measurements occur, so what are the effects? Does it have a big impact on performance, just a few more measurements? The reason to avoid laying out layers too deep is because the impact on performance is exponential

  1. If we have a layout with two levels, where the parentViewIt’s going to apply to each childViewTake a second measure, and each of its componentsViewIt has to be measured twice
  2. If increased to three levels, and each parentViewWe’re still doing a secondary measurement, so this is the bottom guyViewThe number of times it’s measured is just doubled to four
  3. Similarly, it doubles again at 4 levels, and the subview needs to be measured 8 times

That said, for systems that do secondary measurements, the impact of a deeper level on measurement time is exponential, which is why the Android documentation recommends that we reduce layout levels

2. ComposeWhy is there no layout nesting problem?

We know that,ComposeOnly one measurement is allowed, no repeated measurement is allowed.

If each parent component measures each child component only once, that directly means that each component in the interface will be measured only once



In this way, even though the layout level deepens, the measurement time does not increase, reducing the component load time complexity fromO (2 ⁿ)Down toO(n).

The question then arises, as we have seen above, multiple measurements are sometimes necessary, but why not Compose? Intrinsic Measurement is introduced in Compose

Compose allows the parent component to measure the “intrinsic size” of the component before measuring the child component. The ViewGroup’s secondary measurement is also a “rough measurement” and then a final “formal measurement.

The main reason for the performance advantage of using intrinsic property measurement is that it does not double as the level increases and intrinsic property measurement is performed only once

ComposeThe entire component tree is evaluated firstIntrinsicMeasurement, and then formal measurement of the whole. By opening up two parallel measurement processes, we can avoid the constant doubling of measurement time caused by repeated measurement of the same subcomponent due to increasing levels.



Summed up in a word, inComposeCrazy nesting interface, and all components into the same layer, the performance is the same! soComposeThere are no layout nesting problems

2.1 Use of inherent characteristics measurement

Suppose we need to create a composable item that displays two text delimited by a separator line on the screen, as follows:

What can we do to make the separator line as high as the highest text?

@Composable
fun TwoTexts(
    text1: String,
    text2: String,
    modifier: Modifier = Modifier
) {
    Row(modifier = modifier.height(IntrinsicSize.Min)) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 4.dp)
                .wrapContentWidth(Alignment.Start),
            text = text1
        )
        Divider(
            color = Color.Black,
            modifier = Modifier
                .fillMaxHeight()
                .width(1.dp)
        )
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(end = 4.dp)
                .wrapContentWidth(Alignment.End),
            text = text2
        )
    }
}
Copy the code

Note, to the height of a Row set to IntrinsicSize. Min, IntrinsicSize. Min recursive query it item minimum height, two of the minimum height that the width of the Text, the Text The Divider has a minimum height of 0 so the final Row height is the height of the longest text and the Divider’s height is fillMaxHeight which is the same height as the highest text if we do not set the height to intrinsicsize.min The Divider’s height fills the screen as shown below

3. ComposeMeasurement process source code analysis

For example, Compose is composed, which is composed, and is composed

3.1 Measurement Inlet

We know that customizing a Layout in Compose is done using the Layout method

@Composable inline fun Layout(
    content: @Composable() - >Unit,
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
)
Copy the code

Three main arguments are passed in

  1. content: custom layout items that we need to measure and position later
  2. modifier:LayoutSome added modifiersmodifier
  3. measurePolicy: the measurement rules, this is the main area we need to deal with

There are five main interfaces in measurePolicy

fun interface MeasurePolicy {
    fun MeasureScope.measure(measurables: List<Measurable>,constraints: Constraints): MeasureResult

    fun IntrinsicMeasureScope.minIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

    fun IntrinsicMeasureScope.minIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int

    fun IntrinsicMeasureScope.maxIntrinsicWidth(measurables: List<IntrinsicMeasurable>,height: Int): Int

    fun IntrinsicMeasureScope.maxIntrinsicHeight(measurables: List<IntrinsicMeasurable>,width: Int): Int
}
Copy the code

As can be seen:

  1. When using intrinsic property measurements, the correspondingIntrinsicMeasureScopeMethods, such as usingModifier.height(IntrinsicSize.Min), will be calledminIntrinsicHeightmethods
  2. When the parent measures the child, it’s atMeasureScope.measureMethod callmeasure.meausre(constraints)But how does it work? Let’s look at an example
@Composable
fun MeasureTest(a) {
    Row() {
        Layout(content = { }, measurePolicy = { measurables, constraints ->
        	measurables.forEach {
            	it.measure(constraints)
        	}
            layout(100.100) {}})}}Copy the code

A simple example is what we have heremeasureCreate a breakpoint in the method, as shown below:

  1. As shown in the figure below, is made up ofRowtheMeasurePolicyThe measurement subitem,rowColumnMeasurePolicyLet’s define it asParentPolicy
  2. And then call toLayoutNode.OuterMeasurablePlaceable.InnerPlaceablethemeasuremethods
  3. Finally, byInnerPlaceableIs called to the child ofMeasurePolicy, that is, we customizeLayoutThe implementation part, we define it asChildPolicy
  4. The subterm may also measure its subterm, in which case it becomes oneParentPolicyAnd then continue with subsequent measurements

To sum up, when the parent item measures its child, its measurement entry is LayoutNode.measure, and then it makes a series of calls to its MeasurePolicy, which is the customized part of our custom Layout

3.2 LayoutNodeWrapperChain to build

We said the above, measure the entrance is LayoutNode, subsequent pass OuterMeasurablePlaceable, InnerPlaceable measure method, so the question comes, how do these things? First, draw a conclusion

  1. All the subterms areLayoutNodeExists in the form ofParentthechildrenIn the
  2. toLayoutThe set ofmodifierinLayoutNodeWrapperThe chain form is stored inLayoutNode, and then do the corresponding transformation

The LayoutNodeWrapper chain can be used to construct the LayoutNodeWrapper chain for Jetpack Compose

  internal val innerLayoutNodeWrapper: LayoutNodeWrapper = InnerPlaceable(this)
  private val outerMeasurablePlaceable = OuterMeasurablePlaceable(this, innerLayoutNodeWrapper)
  override fun measure(constraints: Constraints) = outerMeasurablePlaceable.measure(constraints)
  override var modifier: Modifier = Modifier
        set(value) {
            / /... code
            field = value
            / /... code

        
            // Create a new LayoutNodeWrappers chain
            // foldOut is equivalent to traversing the modifier
            val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod / * 📍 modifier * / , toWrap ->
                var wrapper = toWrap
                if (mod is OnGloballyPositionedModifier) {
                    onPositionedCallbacks += mod
                }
                if (mod is RemeasurementModifier) {
                    mod.onRemeasurementAvailable(this)}val delegate = reuseLayoutNodeWrapper(mod, toWrap)
                if(delegate ! =null) {
                    wrapper = delegate
                } else {
                      / /... Some of the Modifier judgments are omitted
                      if (mod is KeyInputModifier) {
                        wrapper = ModifiedKeyInputNode(wrapper, mod).assignChained(toWrap)
                    }
                    if (mod is PointerInputModifier) {
                        wrapper = PointerInputDelegatingWrapper(wrapper, mod).assignChained(toWrap)
                    }
                    if (mod is NestedScrollModifier) {
                        wrapper = NestedScrollDelegatingWrapper(wrapper, mod).assignChained(toWrap)
                    }
                    // Layout related Modifier
                    if (mod is LayoutModifier) {
                        wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)
                    }
                    if (mod isParentDataModifier) { wrapper = ModifiedParentDataNode(wrapper, mod).assignChained(toWrap) } } wrapper } outerWrapper.wrappedBy = parent? . InnerLayoutNodeWrapper outerMeasurablePlaceable. OuterWrapper = outerWrapper... }Copy the code

As shown above:

  1. The defaultLayoutNodeWrapperChain byLayoutNode , OuterMeasurablePlaceable.InnerPlaceablecomposition
  2. When adding themodifierWhen,LayoutNodeWrapperThe chain will update,modifierIt’s going to be inserted as a node

For example, if we set some modifier for Layout:

Modifier.size(100.dp).padding(10.dp).background(Color.Blue)
Copy the code

So the correspondingLayoutNodeWrapperThe chain is shown below



And so on and so on and so onmeasure, until the last nodeInnerPlaceable

thenInnerPlaceableWhere does it go?



InnerPlaceableFinally call our customLayoutWhen writingmeasuremethods

3.3 How is the inherent characteristic measurement achieved?

We have described the use of intrinsic property measurement and the construction of the LayoutNodeWrapper chain. How is intrinsic property measurement implemented? The intrinsic property measure is simply inserting a Modifier into the LayoutNodeWrapper chain

@Stable
fun Modifier.height(intrinsicSize: IntrinsicSize) = when (intrinsicSize) {
    IntrinsicSize.Min -> this.then(MinIntrinsicHeightModifier)
    IntrinsicSize.Max -> this.then(MaxIntrinsicHeightModifier)
}

private object MinIntrinsicHeightModifier : IntrinsicSizeModifier {
	override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
    	// Obtain a constraint based on the inherent property measurement before formal measurement
        val contentConstraints = calculateContentConstraints(measurable, constraints)
        // Official measurement
        val placeable = measurable.measure(
            if (enforceIncoming) constraints.constrain(contentConstraints) else contentConstraints
        )
        return layout(placeable.width, placeable.height) {
            placeable.placeRelative(IntOffset.Zero)
        }
    }

    override fun MeasureScope.calculateContentConstraints(
        measurable: Measurable,
        constraints: Constraints
    ): Constraints {
        val height = measurable.minIntrinsicHeight(constraints.maxWidth)
        return Constraints.fixedHeight(height)
    }

    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
        measurable: IntrinsicMeasurable,
        width: Int
    ) = measurable.minIntrinsicHeight(width)
}
Copy the code

As shown above:

  1. IntrinsicSize.MinIt’s also aModifier
  2. MinIntrinsicHeightModifierWill be called first between measurementscalculateContentConstraintsCalculation of the constraint
  3. calculateContentConstraintsWill recursively call the subitemminIntrinsicHeightAnd find the maximum, so that the height of the parent term is determined
  4. After the inherent characteristics are measured, call againmeasurable.measureAnd start the real recursive measurement

3.4 Summary of measurement process

The modifier will also be constructed as a LayoutNodeWrapper chain and stored in the LayoutNode. It is worth noting that if intrinsic property measurement is used, A IntrinsicSizeModifier will be added to the LayoutNodeWrapper chain

Measurement-phase The parent container performs the measure function of the child in measure function of MeasurePolicy. The child’s measure method executes each node’s measure function step by step in accordance with the built LayoutNodeWrapper chain, eventually moving to the innermeasure function, where it then continues to measure its children. At this point, its children will follow the same process, until all the children are measured.

The following diagram summarizes the process.

conclusion

This article mainly introduces the following contents

  1. inAndroidWhat is the performance impact if the layout level is too deep?
  2. inComposeWhy is there no layout nesting problem in?
  3. What are inherent property measurements and their use
  4. ComposeHow is the measurement process source code analysis and intrinsic property measurement implemented?

If this article has helped you, please feel free to like it

The resources

View nesting too deep will card? To use Jetpack Compose, arbitrary set – – Intrinsic Measurement Jetpack Compose source code analysis