为什么 Compose 没有布局嵌套问题?
Posted 冬天的毛毛雨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么 Compose 没有布局嵌套问题?相关的知识,希望对你有一定的参考价值。
作者:RicardoMJiang
前言
做过布局性能优化的同学都知道,为了优化界面加载速度,要尽可能的减少布局的层级。这主要是因为布局层级的增加,可能会导致测量时间呈指数级增长。
而Compose
却没有这个问题,它从根本上解决了布局层级对布局性能的影响: Compose
界面只允许一次测量。这意味着随着布局层级的加深,测量时间也只是线性增长的.
下面我们就一起来看看Compose
到底是怎么只测量一次就把活给干了的,本文主要包括以下内容:
- 布局层级过深为什么影响性能?
Compose
为什么没有布局嵌套问题?Compose
测量过程源码分析
1. 布局层级过深为什么影响性能?
我们总说布局层级过深会影响性能,那么到底是怎么影响的呢?主要是因为在某些情况下ViewGroup
会对子View
进行多次测量
举个例子
<?xml version="1.0" encoding="utf-8"?>
<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>
LinearLayout
宽度为wrap_content
,因此它将选择子View
的最大宽度为其最后的宽度- 但是有个子
View
的宽度为match_parent
,意思它将以LinearLayout
的宽度为宽度,这就陷入死循环了 - 因此这时候,
LinearLayout
就会先以0
为强制宽度测量一下子View
,并正常地测量剩下的其他子View
,然后再用其他子View
里最宽的那个的宽度,二次测量这个match_parent
的子View
,最终得出它的尺寸,并把这个宽度作为自己最终的宽度。 - 这是对单个子
View
的二次测量,如果有多个子View
写了match_parent
,那就需要对它们每一个都进行二次测量。 - 除此之外,如果在
LinearLayout
中使用了weight
会导致测量3次甚至更多,重复测量在Android
中是很常见的
上面介绍了为什么会出现重复测量,那么会有什么影响呢?不过是多测量了几次,会对性能有什么大的影响吗?
之所以需要避免布局层级过深是因为它对性能的影响是指数级的
- 如果我们的布局有两层,其中父
View
会对每个子View
做二次测量,那它的每个子View
一共需要被测量 2 次 - 如果增加到三层,并且每个父
View
依然都做二次测量,这时候最下面的子View
被测量的次数就直接翻倍了,变成 4 次 - 同理,增加到 4 层的话会再次翻倍,子 View 需要被测量 8 次
也就是说,对于会做二次测量的系统,层级加深对测量时间的影响是指数级的,这就是Android
官方文档建议我们减少布局层级的原因
2. Compose
为什么没有布局嵌套问题?
我们知道,Compose
只允许测量一次,不允许重复测量。
如果每个父组件对每个子组件只测量一次,那就直接意味着界面中的每个组件只会被测量一次
这样即使布局层级加深,测量时间却没有增加,把组件加载的时间复杂度从O(2ⁿ)
降到了 O(n)
。
那么问题就来了,上面我们已经知道,多次测量有时是必要的,但是为什么Compose
不需要呢?
Compose
中引入了固有特性测量(Intrinsic Measurement
)
固有特性测量即Compose
允许父组件在对子组件进行测量之前,先测量一下子组件的「固有尺寸」
我们上面说的,ViewGroup
的二次测量,也是先进行这种「粗略测量」再进行最终的「正式测量」,使用固有特性测量可以产生同样的效果
而使用固有特性测量之所以有性能优势,主要是因为其不会随着层级的加深而加倍,固有特性测量也只进行一次
Compose
会先对整个组件树进行一次Intrinsic
测量,然后再对整体进行正式的测量。这样开辟两个平行的测量过程,就可以避免因为层级增加而对同一个子组件反复测量所导致的测量时间的不断加倍了。
总结成一句话就是,在Compose
里疯狂嵌套地写界面,和把所有组件全都写进同一层里面,性能是一样的!所以Compose
没有布局嵌套问题
2.1 固有特性测量使用
假设我们需要创建一个可组合项,该可组合项在屏幕上显示两个用分隔线隔开的文本,如下所示:
为了实现分隔线与最高的文本一样高,我们可以怎么做呢?
@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
)
}
}
注意,这里给Row
的height
设置为了IntrinsicSize.Min
,IntrinsicSize.Min
会递归查询它子项的最小高度,其中两个Text
的最小高度即文本的宽度,而Divider
的最小高度为0 因此最后Row
的高度即为最长的文本的高度,而Divider
的高度为fillMaxHeight
,也就跟最高的文本一样高了
如果我们这里不设置高度为IntrinsicSize.Min
的话,Divider
的高度是占满屏幕的,如下所示
3. Compose
测量过程源码分析
上面我们介绍了固有特性测量是什么,及固有特性测量的使用,下面我们来看看Compose
的测量究竟是怎么实现的
3.1 测量入口
我们知道,在Compose
中自定义Layout
是通过Layout
方法实现的
@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
)
主要传入3个参数
content
:自定义布局的子项,我们后续需要对它们测量和定位modifier
: 对Layout
添加的一些修饰modifier
measurePolicy
: 即测量规则,这个是我们主要需要处理的地方
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
}
可以看出:
- 使用固有特性测量的时候,会调用对应的
IntrinsicMeasureScope
方法,如使用Modifier.height(IntrinsicSize.Min)
,就会调用minIntrinsicHeight
方法 - 父项测量子项时,就是在
MeasureScope.measure
方法中调用measure.meausre(constraints)
,但是具体是怎么实现的呢?我们来看个例子
@Composable
fun MeasureTest() {
Row() {
Layout(content = { }, measurePolicy = { measurables, constraints ->
measurables.forEach {
it.measure(constraints)
}
layout(100, 100) {
}
})
}
}
一个简单的例子,我们在measure
方法中打个断点,如下图所示:
- 如下图所示,是由
Row
的MeasurePolicy
中开始测量子项,rowColumnMeasurePolicy
我们定义为ParentPolicy
- 然后调用到
LayoutNode
,OuterMeasurablePlaceable
,InnerPlaceable
的measure
方法 - 最后再由
InnerPlaceable
中调用到子项的MeasurePolicy
,即我们自定义Layout
实现的部分,我们定义它为ChildPolicy
- 子项中也可能会测量它的子项,在这种情况下它就变成了一个
ParentPolicy
,然后继续后续的测量
综上所述,父项在测量子项时,子项的测量入口就是LayoutNode.measure
,然后经过一系列调用到子项自己的MeasurePolicy
,也就是我们自定义Layout
中自定义的部分
3.2 LayoutNodeWrapper
链构建
上面我们说了,测量入口是LayoutNode
,后续还要经过OuterMeasurablePlaceable
,InnerPlaceable
的measure
方法,那么问题来了,这些东西是怎么来的呢?
首先给出结论
- 子项都是以
LayoutNode
的形式,存在于Parent
的children
中的 - 给
Layout
的设置的modifier
会以LayoutNodeWrapper
链的形式存储在LayoutNode
中,然后后续做相应变换
由于篇幅原因,关于第一点就不在这里详述了,有兴趣的同学可以参考:Jetpack Compose 测量流程源码分析
我们这里主要看下LayoutNodeWrapper
链是怎么构建的
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
……
}
如上所示:
- 默认的
LayoutNodeWrapper
链即由LayoutNode
,OuterMeasurablePlaceable
,InnerPlaceable
组成 - 当添加了
modifier
时,LayoutNodeWrapper
链会更新,modifier
会作为一个结点插入到其中
举个例子,如果我们给Layout
设置一些modifier
:
Modifier.size(100.dp).padding(10.dp).background(Color.Blue)
那么对应的LayoutNodeWrapper
链如下图所示
这样一个接一个链式调用下一个的measure
,直到最后一个结点InnerPlaceable
那么InnerPlaceable
又会调用到哪儿呢?
InnerPlaceable
最终调用到了我们自定义Layout
时写的measure
方法
3.3 固有特性测量是怎样实现的?
上面我们介绍了固有特性测量的使用,也介绍了LayoutNodeWrapper
链的构建,那么固有特性测量是怎么实现的呢?
其实固有特性测量就是往LayoutNodeWrapper
链中插入了一个Modifier
@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 {
//正式测量前先根据固有特性测量获得一个约束
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)
}
如上所示:
IntrinsicSize.Min
其实也是个Modifier
MinIntrinsicHeightModifier
会在测量之间,先调用calculateContentConstraints
计算约束calculateContentConstraints
中则会递归地调用子项的minIntrinsicHeight
,并找出最大值,这样父项的高度就确定了- 固有特性测量完成后,再调用
measurable.measure
,开始真正的递归测量
3.4 测量过程小结
准备阶段
子项在声明时,会生成LayoutNode
添加到父项的chindredn
中,同时子项的modifier
也将构建成LayoutNodeWrapper
链,保存在LayoutNode
中
值得注意的是,如果使用了固有特性测量,将会添加一个IntrinsicSizeModifier
到LayoutNodeWrapper
链中
测量阶段
父容器在其测量策略MeasurePolicy
的measure
函数中会执行child
的measure
函数。
child
的measure
方法按照构建好的LayoutNodeWrapper
链一步步的执行各个节点的measure
函数,最终走到InnerPlaceable
的measure
函数,在这里又会继续它的children
进行测量,此时它的children
就会和它一样进行执行上述流程,一直到所有children
测量完成。
用下面这张图总结一下上述流程。
总结
本文主要介绍了以下内容
- 在
Android
中布局层级过深为什么会对性能有影响? - 在
Compose
中为什么没有布局嵌套问题? - 什么是固有特性测量及固有特性测量的使用
Compose
测量过程源码分析及固有特性测量到底是怎样实现的?
以上是关于为什么 Compose 没有布局嵌套问题?的主要内容,如果未能解决你的问题,请参考以下文章
Android中布局层级过深为什么会对性能有影响?为什么 Compose 没有布局嵌套问题?