一文了解Compose
简介
Jetpack Compose 是一个适用于 Android 的新式声明性界面工具包。阅读官方介绍可以了解到,Compose 大概是这么个东西:
- Compose 是一个声明性界面框架,使用更少的代码、强大的工具和直观的 Kotlin API。
- 抛弃了原有安卓view的体系,完全重新实现了一套新的ui体系
- 使用可组合函数来替换view构建UI界面,只允许一次测量,避免了布局嵌套多次测量问题,从根本上解决了布局层级对布局性能的影响。
看完介绍后第一反应便有了以下几个疑问。
- Compose完全摒弃了View吗?ComposeUI结构是什么样?
- 可组合函数与View有什么不同?函数最终有没有转换成了view
- ComposeUI是如何解多次测量问题问题的?
这篇文章将会介绍Compose的整体结构并一个一个探索这些问题的答案。
基础概念
compose编程思想
Compose是用Kotlin写的,Kotlin版本不低于1.5.10;kotlin支持函数式编程是Compose实现的关键。简介里说了 Compose 是一个声明性框架,也就是声明式或者说函数式编程。他通过函数刷新屏幕上的内容,而不需要拿到组件的具体实例,UI是关于状态的函数,一切都是函数。
Composable注解
Compose函数都加了Composable注解修饰。Composable注解的函数只能被另一个Composable注解的函数调用。此注解可告诉编译器,被修饰的函数是Compose函数,用来描述UI界面的。这些函数不返回任何内容,他们旨在描述当前的UI状态。
微件
Text()、Image()、Row()、Coulm()等这些描述屏幕元素的函数都被称为微件,类似TextView、ImageView、LinearLayout。从代码上看 view 之间的关系是继承的关系,LinearLayout 继承ViewGroup,ViewGroup继承 View。微件之间没有任何关系,通过组合嵌套自由搭配。在继承关系中有些不必要的属性也会一并继承,这就显的多余。在这一点上,组合组合函数明显更好。
重组
在view 体系中如需更改某个组件,可以在该该件上调用 setter 以更改其内部状态。在 Compose 中,则是使用新数据再次调用可组合函数。在Compose中UI刷新的唯一方法就是重组,决定是否重组的条件就是与@Composable元素绑定的数据是否发生了变化。
简单使用
实现一个recycleView
@Composable
fun showList() {
//生产 100条数据
var datalist: MutableList<String> = mutableListOf()
for (i: Int in 0..100) {
datalist.add("$i")
}
//纵向滑动容器
LazyColumn {
//顶部吸顶布局
stickyHeader {
Text(text = "顶部吸顶布局",
color = Color.Cyan,
modifier = Modifier
.fillMaxWidth()
.background(Color.White),
fontSize = 20.sp,
textAlign = TextAlign.Center
)
}
//构建列表
items(datalist) { name ->
//构建一个item
BindItemView(name = name)
}
}
}
@Composable
fun BindItemView(name: String) {
var imageURL =
"https://img0.baidu.com/it/u=1766591091,2326601705&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=674"
//横向布局 包含文本和图片
Row(modifier = Modifier
.padding(10.dp)
//Item点击事件
.clickable {
Toast
.makeText(mContext, name, Toast.LENGTH_LONG)
.show()
}) {
//纵向文本布局
Column(
modifier = Modifier
.background(Color.White)
.weight(1f)
) {
Text(text = "我是个标题$name", fontSize = 16.sp, maxLines = 1)
Text(
color = Purple200,
modifier = Modifier.padding(top =6.dp, bottom = 4.dp)
.background(Color.White),
text = "我是个内容,我可能很长,但是我只能显示最多两行",
fontSize = 14.sp,
maxLines = 2
)
Text(text = "userName", fontSize = 10.sp)
Spacer(modifier = Modifier.padding(top = 4.dp).fillMaxWidth().height(1.dp).background(Color.Gray))
}
//加载一个网络图片
Image(
modifier = Modifier
.size(80.dp, 60.dp)
.align(Alignment.CenterVertically),
contentScale = ContentScale.Crop,
painter = rememberImagePainter(imageURL),
contentDescription = ""
)
}
}
上述60行代码中的Ui,包含了一个纵向滚动的list布局,item有文本有图片,有点击事件,还有顶部吸顶效果。没有xml没有adapter,相比xml-view体系,代码简洁度确实高了不少。
Compose 视图结构
Comepose抛弃了view体系,那Compose的视图结构是什么样的。
打开“显示布局边界”可以看到Compose的组件显示了布局边界,我们知道,Flutter与WebView H5内的组件都是不会显示布局边界的,难道Compose最终还是把函数变成了View?
通过android studio 的LayoutInspector看到ComposeActivity的布局结构
最上层还是DecorView、FrameLayout,然后就看到有一个AndroidComposeView,没有TextView或者其他的View了。ComposeActivity 的onCreate方法里setContent替换了原来的setContentView,点击去我们看到
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
//decorView 的第一个子View如果是 ComposeView 直接用,如果不是就创建一个ComposeView ,然后添加到跟布局
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
...
setContent(content)
} else ComposeView(this).apply {
...
setContent(content)
...
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
decorView 的第一个子View如果不是ComposeView就创建一个 ,然后添加到跟布局。而ComposeView 里又通过 Wrapper_android.kt 创建了一个 AndroidComposeView,我们前面见过。ComposeView 和 AndroidComposeView 都是继承ViewGroup,都是在setContent时添加进去的。除此之外我们写的Column,Row,Text并没有出现在布局层级中,也就是说Compose 并没有把函数转成View。AndroidComposeView 可以理解是一个连接view 和 compose 的入口。
我们知道,View系统通过一个View树的数据结构来存储TextView,ImageView等屏幕元素,渲染的时候去遍历View树。Compose里是怎么管理它的布局元素的呢。
- LayoutNote
我们点开任何一个微件函数,一系列调用最终都会到了Layout.kt的Layout()方法,Layout() 核心是调用ReusableComposeNode ()方法。这里有个参数 factory,factory 是一个构造器函数, factory 被调用就会创建一个LayoutNote,定义的布局属性modifier也在这里设置给了LayoutNode。每个微件最终都是一个LayoutNote。
@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
...
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
//1. factory 指向 ComposeUiNode的构造器,创建一个LayoutNode
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(density, ComposeUiNode.SetDensity)
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
},
//2. modifier设置给LayoutNode
skippableUpdate = materializerOf(modifier),
content = content
)
}
ComposeUiNode #{
val Constructor: () -> ComposeUiNode = LayoutNode.Constructor
}
LayoutNode #{
internal val Constructor: () -> LayoutNode = { LayoutNode() }
}
- Composables :ReusableComposeNode方法里又调用Composer 来插入或者更新LayoutNote。
inline fun <T, reified E : Applier<*>> ReusableComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit,
noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
content: @Composable () -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startReusableNode()
//插入一个新节点
if (currentComposer.inserting) {
currentComposer.createNode(factory)
} else {
//复用节点
currentComposer.useNode()
}
currentComposer.disableReusing()
//更新节点
Updater<T>(currentComposer).update()
currentComposer.enableReusing()
SkippableUpdater<T>(currentComposer).skippableUpdate()
currentComposer.startReplaceableGroup(0x7ab4aae9)
content()
currentComposer.endReplaceableGroup()
currentComposer.endNode()
}
- Composer :这里 factory方法才被执行创建了一个note,获取插入节点的位置,调用nodeApplier插入到root里。
override fun <T> createNode(factory: () -> T) {
...
//1. 获取插入节点的位置
val insertIndex = nodeIndexStack.peek()
val groupAnchor = writer.anchor(writer.parent)
groupNodeCount++
recordFixup { applier, slots, _ ->
@Suppress("UNCHECKED_CAST")
//2. 在这里才执行了 factory方法, 创建了一个note
val node = factory()
slots.updateNode(groupAnchor, node)
@Suppress("UNCHECKED_CAST") val nodeApplier = applier as Applier<T>
//3. 调用nodeApplier插入到root里。
nodeApplier.insertTopDown(insertIndex, node)
applier.down(node)
}
...
}
- Applier
Composer 里的 nodeApplier 是一个Applier接口,实现类是UiApplier,note 最终是在这里add到 root 节点 (LayoutNote) 的 _foldedChildren 列表里。
总结一下,compose 构建了一个LayoutNode树,每一个微件函数会生成一个LayoutNode,Composition 作为起点,发起首次的 构图,通过 Composer 的执行填充 NodeTree。渲染引擎基于 LayoutNote 渲染 UI, 每当重构发生时,都会通过 Applier 对 NodeTree 进行更新。简单来说就是 Composer 和 Applier 一起维护了一个 LayoutNote 树。
Compose在渲染时不会转化成View,而是有一个AndroidComposeView作为入口View,我们声明的Compose布局在渲染时会转化成LayoutNode,所有的屏幕元素都存储在这颗树上。AndroidComposeView 中会触发LayoutNode的布局与绘制。
compose界面只允许一次测量。
我们知道View体系有个问题, 如果父布局的布局属性是wrap_content、子布局是match_parent ,父布局先以0为强制宽度测量子View、然后继续测量剩下的其他子View,再用其他子View里最宽的宽度,二次测量这个match_parent的子 View。子View测量了两次,子view也会对下面的子view进行两次测量 。并且随着布局的增长测量次数成指数增长,Compose是如何处理的?
开头里的例子里我们用了一个线性视图容器Row,我们先试着自定义个AutoRow,实现自动换行的横向布局。以Row为样板,照葫芦画瓢。
定义一个@Composable 方法:AutoRow
@Composable
fun AutoRow(modifier: Modifier, content: @Composable () -> Unit) {
//实例一个 measurePolicy 定义测量和布局策略
var measurePolicy = object : MeasurePolicy{
//实现 measure 方法,
// Measurable : 是一个接口,通过断点可以看到,它的实例就是一个 LayoutNote,childs 就是子节点列表。
// constraints : 是父布局的约束条件,有点像View 的 MeasureSpec
override fun MeasureScope.measure(
childs: List<Measurable>,
constraints: Constraints
): MeasureResult {
var placeables = mutableListOf<Placeable>()
childs.forEach {
//1.遍历测量子项,得到一个可放置项 Placeable
// measure()方法传入一个Constraints:约束。 测量完成后返回 一个 Placeable
// 一次测量过程中,多次调用一个子项的measure()方法会抛异常
var constraints = Constraints(0, constraints.maxWidth, 0, constraints.maxHeight)
val placeable = it.measure(constraints)
placeables.add(placeable)
}
var layoutX = 0
var layoutY = 0
var padding = 20
//2.layout 方法确定布局的大小,把测量得到的Placeable摆放到指定位置
val measureResult = layout(width = constraints.minWidth, height = constraints.minHeight,placementBlock={
placeables.forEach { placeable ->
if (layoutX + placeable.width > constraints.maxWidth) {
layoutX = 0
layoutY += placeable.height
}
// 3.定位子项在父布局位置的位置
placeable.placeRelative(layoutX, layoutY)
layoutX += placeable.width + padding
}
})
return measureResult
}
}
//4.布局
//上面这些事定义了measure()、layout() 的策略,Layout这里才触发执行的。
Layout(content = content, modifier = modifier, measurePolicy = measurePolicy)
}
测量的 measure () 方法最终调用了 outerMeasurablePlaceable.measure(),其中check()方法里判断了当前节点是否正在被父布局测量,如果正在测量就会抛出异常。
override fun measure(constraints: Constraints) =
outerMeasurablePlaceable.measure(constraints)
// OuterMeasurablePlaceable ##
override fun measure(constraints: Constraints): Placeable {
// when we measure the root it is like the virtual parent is currently laying out
val parent = layoutNode.parent
if (parent != null) {
check(
layoutNode.measuredByParent == LayoutNode.UsageByParent.NotUsed ||
@Suppress("DEPRECATION") layoutNode.canMultiMeasure
) {
"measure() may not be called multiple times on the same Measurable. Current " +
"state ${layoutNode.measuredByParent}. Parent state ${parent.layoutState}."
}
...
}
其实Measurable的注释讲的很清楚
Measures the layout with constraints, returning a Placeable layout that has its new size. A Measurable can only be measured once inside a layout pass.
使用约束测量布局,返回具有新大小的可放置布局。一个Measurable 只能测量一次在一次layout 过程里。
这是Compose的特性,就只允许一次测量,从根本上解决了布局嵌套多次测量的问题。
测量过程分析
Modifier 链
我们构造一个compose 微件时,有一个参数很重要,他描述了这个微件的一些特性。比如:Text()通过 Modifier设置了size,padding,background等等。
Text( modifier = Modifier
.size(80.dp, 20.dp)
.padding(10.dp)
.background(Color.White),
text = "我是个内容,我可能很长,但是我只能显示最多两行",
)
fun Modifier.size(width: Dp, height: Dp) = this.then(
SizeModifier(
minWidth = width,
maxWidth = width,
minHeight = height,
maxHeight = height,
enforceIncoming = true,
inspectorInfo = debugInspectorInfo {
name = "size"
properties["width"] = width
properties["height"] = height
}
)
)
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
class CombinedModifier(
private val outer: Modifier,
private val inner: Modifier
) : Modifier {...}
size(),padding()可以理解成操作符,类似rxjava 里的那样。点进去会看到在这些方法里创建了对应的 SizeModifier,PaddingModifier,他们都继承自LayoutModifier(表示Layout相关的属性),background 里创建了Background 继承DrawModifier(表示绘制相关的属性),LayoutModifier、DrawModifier 继承Modifier.Element。SizeModifier,PaddingModifier、Background被包进CombinedModifier,形成了一条Modifier链。
LayoutNodeWrapper链
我们在前面Layout 里看到,modifier(链)被传入materializerOf()方法,最终被赋值给LayoutNote.modifier这个成员变量。
当被set()方法设置给LayoutNote时 ,
- 调用foldOut()方法遍历modifier链,把不同的Modifier类型包装成Node,LayoutModifier包装成ModifiedLayoutNode,
- ModifiedLayoutNode 挂在初始节点 innerLayoutNodeWrapper:LayoutNodeWrapper 上,形成一条Wrapper链,innerLayoutNodeWrapper在链尾。
- 遍历完成后把链头设置给outerMeasurablePlaceable.outerWrapper。
override var modifier: Modifier = Modifier
set(value) {// 成员变量modifier 的set()方法
...
//1. foldOut()遍历 modifier 每个节点
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
// 2. 类型判断,构建Node节点,挂在初始节innerLayoutNodeWrapper上
if (mod is DrawModifier) {
val drawEntity = DrawEntity(toWrap, mod)
drawEntity.next = toWrap.drawEntityHead
toWrap.drawEntityHead = drawEntity
drawEntity.onInitialize()
}
//toWrap = innerLayoutNodeWrapper
var wrapper = toWrap
...
//省略了一些Modifier类型
// 不同的Modifier类型被包装成不同类型的Node,
//LayoutModifier被包装成了ModifiedLayoutNode,
if (mod is FocusEventModifier) {
wrapper = ModifiedFocusEventNode(wrapper, mod)
.initialize()
.assignChained(toWrap)
}
if (mod is FocusRequesterModifier) {
wrapper = ModifiedFocusRequesterNode(wrapper, mod)
.initialize()
.assignChained(toWrap)
}
if (mod is FocusOrderModifier) {
wrapper = ModifiedFocusOrderNode(wrapper, mod)
.initialize()
.assignChained(toWrap)
}
if (mod is LayoutModifier) {
wrapper = ModifiedLayoutNode(wrapper, mod)
.initialize()
.assignChained(toWrap)
}
...
//3. 返回链头
wrapper
}
outerWrapper.wrappedBy = parent?.innerLayoutNodeWrapper
//outerWrapper是链头,innerLayoutNodeWrapper在链尾
outerMeasurablePlaceable.outerWrapper = outerWrapper
...
}
画成图如下:
测量入口
Layout()里把modifier设置给了LayoutNode,还把modifier转成了LayoutNodeWrapper链,那什么时候使用这些Modifier的呢。我们开始的地方知道了ComposeActivity布局的顶层还是View,那就是说视图的最顶层的渲染还是依赖view的。在 AndroidComposeView 看到了 dispatchDraw(),这就是Compose 渲染的入口。这里会触发布局测量。
override fun dispatchDraw(canvas: android.graphics.Canvas) {
//Compose 测量与布局入口
measureAndLayout()
//Compose 绘制入口
canvasHolder.drawInto(canvas) { root.draw(this) }
//...
}
测量布局过程
measureAndLayout()遍历所有需要测量的 LayoutNote,最终到MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded()方法,测量并布局每一个LayoutNote。
private fun remeasureAndRelayoutIfNeeded(layoutNode: LayoutNode): Boolean {
var sizeChanged = false
if (layoutNode.isPlaced ||
layoutNode.canAffectParent ||
layoutNode.alignmentLines.required
) {
if (layoutNode.layoutState == NeedsRemeasure) {
//1. 测量 layoutNode
sizeChanged = doRemeasure(layoutNode)
}
if (layoutNode.layoutState == NeedsRelayout && layoutNode.isPlaced) {
//2. 设置 layoutNode 位置
if (layoutNode === root) {
layoutNode.place(0, 0)
} else {
layoutNode.replace()
}
onPositionedDispatcher.onNodePositioned(layoutNode)
consistencyChecker?.assertConsistent()
}
。。。
}
return sizeChanged
}
- 测量
doRemeasure() 调用了 outerMeasurablePlaceabl.remeasure(),
outerMeasurablePlaceabl 是 LayoutNote 的一个成员,outerMeasurablePlaceabl 又调用outerWrapper.measure()。上面说到过,outerWrapper 是头节点 LayoutModifierNode 。
internal class ModifiedLayoutNode(
wrapped: LayoutNodeWrapper,
modifier: LayoutModifier
) : DelegatingLayoutNodeWrapper<LayoutModifier>(wrapped, modifier) {
override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
with(modifier) {
measureResult = measureScope.measure(wrapped, constraints)
this@ModifiedLayoutNode
}
}
根据我们写的代码,此时modifier 为 SizeModifier, 类型 LayoutModifier.kt
SizeModifier.kt
private class SizeModifier(
private val minWidth: Dp = Dp.Unspecified,
private val minHeight: Dp = Dp.Unspecified,
private val maxWidth: Dp = Dp.Unspecified,
private val maxHeight: Dp = Dp.Unspecified,
private val enforceIncoming: Boolean,
inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
override fun MeasureScope.measure(
measurable: Measurable,/*下一个LayoutNodeWrapper*/
constraints: Constraints/* 父容器或者上一个节点的约束 */
): MeasureResult {
val wrappedConstraints = targetConstraints.let { targetConstraints ->
if (enforceIncoming) {//当我们给控件指定大小时,这个值就为true
//结合父容器或者上一个节点的约束 和我们指定约束进行结合生成一个新的约束
constraints.constrain(targetConstraints)
} else {
……
}
}
// 进行下一个 LayoutNodeWrapper 节点测量
val placeable = measurable.measure(wrappedConstraints)
//测量完,开始摆放节点的位置
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
SizeModifier.measure自己测量完,进行下一个 LayoutNodeWrapper 节点的测量,一直到最后 InnerPlaceable 节点。InnerPlaceable触发LayoutNode子项的测量,这里就是Layout 的MeasurePolicy 的measure 执行的地方了
class InnerPlaceable{
override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
val measureResult = with(layoutNode.measurePolicy) {
// 触发LayoutNode子项的测量,这里就是Layout 的MeasurePolicy 的measure 执行的地方了
layoutNode.measureScope.measure(layoutNode.children, constraints)
}
layoutNode.handleMeasureResult(measureResult)
return this
}
}
- 布局
internal fun replace() {
try {
relayoutWithoutParentInProgress = true
outerMeasurablePlaceable.replace()
} finally {
relayoutWithoutParentInProgress = false
}
}
Layout. replace()方法,看到布局过程也是由outerMeasurablePlaceable完成的。和测量过程一样,过程就不一步步的分析了,经过一系列的链式调用,最终到 InnerPlaaceable.placeAt()。
InnerPlaaceable.placeAt(),设置 当前节点的位置。
onNodePlaced()最终会调用 MeasureResult.placeChildren(),触发子项位置的摆放。
override fun placeAt(
position: IntOffset,
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
//1.往下掉用LayoutNoteWrapper.placeAt(),设置当前节点的 的位置
super.placeAt(position, zIndex, layerBlock)
//2. 最终会调用 MeasureResult.placeChildren(),设置子项的位置。
layoutNode.onNodePlaced()
}
- 绘制
这些布局是怎么绘制到屏幕上的呢?在AndroidComposeView里看到绘制的入口是
canvasHolder.drawInto(canvas) { root.draw(this) }
root是LayoutNode的根节点。调用了LayoutNode.draw()方法,还传了个参数:canvas。LayoutNode继续调用outerLayoutNodeWrapper.draw(canvas),看到这应该能猜到了,绘制应该也是LayoutNodeWrapper完成的。还记得backgroung方法创建了BackGround继承DrawModifier,在LayoutNode里被包装成了DrawEntity
internal class DrawEntity(
val layoutNodeWrapper: LayoutNodeWrapper,//下一个节点
val modifier: DrawModifier
) : OwnerScope {
最终还是调用到了LayoutNodeWrapper.draw()
fun draw(canvas: Canvas) {
val layer = layer
if (layer != null) {
layer.drawLayer(canvas)
} else {
val x = position.x.toFloat()
val y = position.y.toFloat()
canvas.translate(x, y)
drawContainedDrawModifiers(canvas)
canvas.translate(-x, -y)
}
}
我们看到绘制还是基于canvas的。
总结:
- 在setContent的过程中,会创建ComposeView与AndroidComposeView,其中AndroidComposeView是Compose的入口,连接View体系和新的Compose体系。
- Compose虽然没有了view 树,但也需要一个数据结构来存储Text、Image等屏幕元素,那就是LayoutNote树。
- 微件里设置的modifier会构建一个LayoutModifier链,在设置进LayoutNode时转换成了LayoutNodeWrapper链,测量和布局是由一个个LayoutNodeWrapper节点按顺序完成的,最后一个节点InnerPlaaceable触发子项的测量和布局。
- Compose 特性禁止在一次Layout 过程中多次测量一个子项,这从根本上解决了布局嵌套多次测量的问题。
- Compose绘制虽然脱离了View,但还是依赖canvas。
文章到这里结束了,开头的那些问题也都有了答案。