Bootstrap

布局嵌套过多如何影响布局性能?

概述

  • 背景:布局层级过多、布局过度的嵌套会导致测量时间呈指数级增长,最终影响布局性能。
  • 现状:Compose没有上述布局嵌套问题,因为它根本上解决了布局层级对布局性能的影响。
  • 解决原理:Compose界面只允许一次测量,即随着布局层级的加深,测量时间仅线性增长。

本文将主要说明:

  1. 布局嵌套过多如何影响布局性能?
  2. ComposeUI如何解决嵌套问题?
  3. 为什么ComposeUI可以只允许一次测量?
  4. ComposeUI测量过程的源码分析

1. 布局嵌套过多如何影响布局性能?

原因主要是:「ViewGroup会对子View进行多次测量」。假设:父布局的布局属性是wrap_content、子布局是match_parent,此时的布局过程是:

  1. 父布局先以0为强制宽度测量子View、然后继续测量剩下的其他子View
  2. 再用其他子View里最宽的宽度,二次测量这个match_parent的子 View,最终得出它的尺寸,并把这个宽度作为自己最终的宽度。

即 这个过程就对单个子View进行了二次测量。

「而布局嵌套对性能影响则是指数形式的」,即:父布局会对每个子view做两次测量,子view也会对下面的子view进行两次测量,即相当于是 O(2ⁿ)测量。


2. ComposeUI如何解决嵌套问题?

ComposeUI规定:只允许一次测量,不允许重复测量。即每个父布局只对每个子组件测量一次,即测量复杂度变成了:O(n)。


3. 为什么ComposeUI可以只允许一次测量?

ComposeUI引入了:固有特性测量(Intrinsic Measurement)。即 Compose 允许父组件在对子组件测量前先测量子组件的“固有尺寸”,这相当于上面说的两次测量的 第一次 “粗略测量“。

而这种固定特性测量是对整个组件布局树进行一次测量即可,从而避免了随着层级的加深而增加测量次数。


4. ComposeUI测量过程的源码分析

此处主要分析:固定特性测量的测量过程。此处先介绍LayoutNodeWrapper链构建

4.1 LayoutNodeWrapper

先来看两个核心结论:

  • 子View都是以LayoutNode的形式,存在于Parent - children中的
  • 给Layout的设置的modifier会以LayoutNodeWrapper链的形式存储在LayoutNode中,然后后续做相应变换

下面说明LayoutNodeWrapper的构建:

  • 默认的LayoutNodeWrapper链即由LayoutNode , OuterMeasurablePlaceable, InnerPlaceable 组成
  • 当添加了modifier时,LayoutNodeWrapper链会更新,modifier会作为一个结点插入到其中
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            // 创建新的 LayoutNodeWrappers 链        // foldOut 相当于遍历 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 {                  // …… 省略了一些 Modifier判断                   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)                }                // 布局相关的 Modifier                if (mod is LayoutModifier) {                    wrapper = ModifiedLayoutNode(wrapper, mod).assignChained(toWrap)                }                if (mod is ParentDataModifier) {                    wrapper = ModifiedParentDataNode(wrapper, mod).assignChained(toWrap)                }                           }            wrapper        }        outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper        outerMeasurablePlaceable.outerWrapper = outerWrapper        ……    }// 假设:给Layout设置一些modifierModifier.size(100.dp).padding(10.dp).background(Color.Blue)

对应的LayoutNodeWrapper链如下图所示

图片

图片

<figcaption style="margin: 5px 0px 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; text-align: center; font-size: 13px;">image.png</figcaption>

这样一直链式调用下一个的measure,直到最后一个结点InnerPlaceable,最终调用到了自定义Layout时写的measure()

图片

图片

4.2 固有特性测量-实现原理

结论:LayoutNodeWrapper链中插入了一个Modifier

@Stablefun 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 {     //正式测量前先根据固有特性测量获得一个约束        val contentConstraints = calculateContentConstraints(measurable, constraints)        //正式测量        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)}

汇总说明:

  1. IntrinsicSize.Min其实也是个Modifier
  2. MinIntrinsicHeightModifier会在测量之间,先调用calculateContentConstraints计算约束
  3. calculateContentConstraints中则会递归地调用子项的minIntrinsicHeight,并找出最大值,这样父项的高度就确定了
  4. 固有特性测量完成后,再调用measurable.measure,开始真正的递归测量
;