setContent做了什么
我们基于一个最简单的例子进行分析
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text(text = "Hello World!")
}
}
}
这里setContent做了什么,熟悉Kotlin的应该知道,这里是一个函数,利用了Kotlin高阶函数的特性。说明Compose本质上是一个函数。
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
)
如上,他是一个被Composale修饰的函数,所有的Compose代码都必须被包含在@Composable注解的作用域里,这样才能被Compose编译器识别。
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
继续分析函数体的内容,可以看到本质上是转换成了ComposeView,然后再调用Activity的setContentView方法。
那么继续分析ComposeView,可以发现他继承链为ComposeView ==> AbstractComposeView ==> ViewGroup,我们发现ComposeView本质上是一个ViewGroup,那么是否可以认为Compose还是用了view绘制那一套,只是换了个Kotlin的壳呢?
Compose 本质就是自定义的 ViewGroup?
我们通过adb命令分析布局层级,验证我们的猜想
adb shell dumpsys activity top
结果如下
DecorView@28fe157[MainActivity]
android.widget.LinearLayout{e35a444 V.E...... ........ 0,0-1080,2296}
android.view.ViewStub{57bd92d G.E...... ......I. 0,0-0,0 #10201b1 android:id/action_mode_bar_stub}
android.widget.FrameLayout{7b55e62 V.E...... ........ 0,75-1080,2296 #1020002 android:id/content}
androidx.compose.ui.platform.ComposeView{cc73bf3 V.E...... ........ 0,0-228,52}
androidx.compose.ui.platform.AndroidComposeView{3779fb7 VFED..... ........ 0,0-228,52}
android.view.View{60ac0b0 V.ED..... ........ 0,2296-1080,2340 #1020030 android:id/navigationBarBackground}
android.view.View{169db29 V.ED..... ........ 0,0-1080,75 #102002f android:id/statusBarBackground}
可以看到最上层是AndroidComposeView,这个类也是ViewGroup。但是除此之外,并没有看到我们在布局中添加的Text
internal class AndroidComposeView(context: Context) : ViewGroup(context)
从setContent分析,我们添加的compose函数最终通过ComposeView的setContent设置到ComposeView里面,那么分析ComposeView的setContent方法
private fun doSetContent(
owner: AndroidComposeView,
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
if (inspectionWanted(owner)) {
owner.setTag(
R.id.inspection_slot_table_set,
Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
)
enableDebugInspectorInfo()
}
// 创建Composition对象,传入UiApplier
val original = Composition(UiApplier(owner.root), parent)
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
// 传入content函数
wrapped.setContent(content)
return wrapped
}
CompositionContext是什么?
WrappedComposition是什么?WrappedComposition继承Composition,接收Composition以及AndroidComposeView
private class WrappedComposition( val owner: AndroidComposeView, val original: Composition ) : Composition
分析调用链,发现他最终调用到了doSetContent方法
- 创建Composition对象,传入UiApplier
- 传入content函数
internal class UiApplier(
root: LayoutNode
)
这里一个个的来分析,UiApplier是什么?可以看到他传入了AndroidComposeView的LayoutNode对象。Android的View系统中有viewTree,描述整个UI界面,那么LayoutNode就不难理解,Compose渲染的时候,每一个组件就是一个LayoutNode,最终组成一个LayoutNode树,来描述UI界面。
LayoutNode的生成
以Text为例,我们查看他的源码
@Composable
fun Text(
text: String,
modifier: Modifier = Modifier,
...
) {
...
BasicText(
text,
modifier,
mergedStyle,
onTextLayout,
overflow,
softWrap,
maxLines,
)
}
@OptIn(InternalFoundationTextApi::class)
@Composable
fun BasicText(
text: String,
modifier: Modifier = Modifier,
...
) {
...
Layout(modifier.then(controller.modifiers), controller.measurePolicy)
}
// 布局
inline fun Layout(
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val viewConfiguration = LocalViewConfiguration.current
val materialized = currentComposer.materialize(modifier)
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(density, ComposeUiNode.SetDensity)
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
set(materialized, ComposeUiNode.SetModifier)
},
)
}
@Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
@Composable inline fun <T : Any, reified E : Applier<*>> ReusableComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startReusableNode()
if (currentComposer.inserting) {
// 生成LayoutNode
currentComposer.createNode { factory() }
} else {
currentComposer.useNode()
}
currentComposer.disableReusing()
Updater<T>(currentComposer).update()
currentComposer.enableReusing()
currentComposer.endNode()
}
从这里可看出,最终Text调用了Layout函数,生成了 ComposeUiNode,LayoutNode就是ComposeUiNode的实现类。
那么一个简单界面的布局关系就如下所示
那UiApplier就不难理解了,它是LayoutNode树的管理器,可以增删NodeTree的节点。
compose的起点:Composition
接着分析Composition,他是compose的起点,代表整个compose的执行。
private class WrappedComposition(
val owner: AndroidComposeView,
val original: Composition
) : Composition, LifecycleEventObserver {
private var disposed = false
private var addedToLifecycle: Lifecycle? = null
private var lastContent: @Composable () -> Unit = {}
override fun setContent(content: @Composable () -> Unit) {
owner.setOnViewTreeOwnersAvailable {
if (!disposed) {
val lifecycle = it.lifecycleOwner.lifecycle
lastContent = content
if (addedToLifecycle == null) {
// 1.初始化流程,首次进入
addedToLifecycle = lifecycle
// this will call ON_CREATE synchronously if we already created
// 2.添加生命周期监听
lifecycle.addObserver(this)
} else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
// 4.调用连接器的setContent
original.setContent {
@Suppress("UNCHECKED_CAST")
val inspectionTable =
owner.getTag(R.id.inspection_slot_table_set) as?
MutableSet<CompositionData>
?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
as? MutableSet<CompositionData>
if (inspectionTable != null) {
inspectionTable.add(currentComposer.compositionData)
currentComposer.collectParameterInformation()
}
LaunchedEffect(owner) { owner.keyboardVisibilityEventLoop() }
LaunchedEffect(owner) { owner.boundsUpdatesEventLoop() }
CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
ProvideAndroidCompositionLocals(owner, content)
}
}
}
}
}
}
override fun dispose() {
if (!disposed) {
disposed = true
owner.view.setTag(R.id.wrapped_composition_tag, null)
// 移除生命周期
addedToLifecycle?.removeObserver(this)
}
original.dispose()
}
override val hasInvalidations get() = original.hasInvalidations
override val isDisposed: Boolean get() = original.isDisposed
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
dispose()
} else if (event == Lifecycle.Event.ON_CREATE) {
if (!disposed) {
// 3.触发界面onCreate的时候重新执行
setContent(lastContent)
}
}
}
}
简单分析以上流程,Composition会注册生命周期监听,在onCreate的时候才会触发界面的创建。另外看这里的方法,跟Flutter是一模一样。
Composition#setContent
override fun setContent(content: @Composable () -> Unit) {
check(!disposed) { "The composition is disposed" }
this.composable = content
parent.composeInitial(this, composable)
}
Recomposer#composeInitial
internal override fun composeInitial(
composition: ControlledComposition,
content: @Composable () -> Unit
) {
val composerWasComposing = composition.isComposing
composing(composition, null) {
composition.composeContent(content)
}
...
}
RecompositionImpl#composeContent
override fun composeContent(content: @Composable () -> Unit) {
// TODO: This should raise a signal to any currently running recompose calls
// to halt and return
trackAbandonedValues {
synchronized(lock) {
drainPendingModificationsForCompositionLocked()
composer.composeContent(takeInvalidations(), content)
}
}
}
Composer#composeContent
internal fun composeContent(
invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
content: @Composable () -> Unit
) {
runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
doCompose(invalidationsRequested, content)
}
private fun doCompose(
invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
content: (@Composable () -> Unit)?
) {
runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
trace("Compose:recompose") {
snapshot = currentSnapshot()
compositionToken = snapshot.id
providerUpdates.clear()
invalidationsRequested.forEach { scope, set ->
val location = scope.anchor?.location ?: return
invalidations.add(Invalidation(scope, location, set))
}
invalidations.sortBy { it.location }
nodeIndex = 0
var complete = false
isComposing = true
try {
startRoot()
// vv Experimental for forced
@Suppress("UNCHECKED_CAST")
val savedContent = nextSlot()
if (savedContent !== content && content != null) {
updateValue(content as Any?)
}
// ^^ Experimental for forced
// Ignore reads of derivedStateOf recalculations
observeDerivedStateRecalculations(
start = {
childrenComposing++
},
done = {
childrenComposing--
},
) {
if (content != null) {
startGroup(invocationKey, invocation)
invokeComposable(this, content)
endGroup()
} else if (
forciblyRecompose &&
savedContent != null &&
savedContent != Composer.Empty
) {
startGroup(invocationKey, invocation)
@Suppress("UNCHECKED_CAST")
invokeComposable(this, savedContent as @Composable () -> Unit)
endGroup()
} else {
skipCurrentGroup()
}
}
endRoot()
complete = true
} finally {
isComposing = false
invalidations.clear()
if (!complete) abortRoot()
}
}
}
再进入 invokeComposable(this, content)
public static final void invokeComposable(@NotNull Composer composer, @NotNull Function2 composable) {
Intrinsics.checkNotNullParameter(composer, "composer");
Intrinsics.checkNotNullParameter(composable, "composable");
Function2 realFn = (Function2)TypeIntrinsics.beforeCheckcastToFunctionOfArity(composable, 2);
realFn.invoke(composer, 1);
}
这里就是对布局进行组合了,这里就不再做分析了。
布局与绘制
布局与绘制需要分析dispatchDraw方法
override fun dispatchDraw(canvas: android.graphics.Canvas) {
...
// 测量与布局
measureAndLayout()
// we don't have to observe here because the root has a layer modifier
// that will observe all children. The AndroidComposeView has only the
// root, so it doesn't have to invalidate itself based on model changes.
// 绘制
canvasHolder.drawInto(canvas) { root.draw(this) }
...
}
虽然测量与布局是Compose自己实现的,但是绘制最终调用了Canvas。
Compose的Text和TextView的区别
根据Compose的绘制可知,本质上Compose还是通过Cavas来绘制的,所以他和TextView也一样,最终调用了drawText
Compose的性能
以一个开源的电影APP(tivi)为例,原来是基于Fragment和XML,现在逐步迁移到Compose。
迁移分为两步
- 迁移到迁移到 Navigatio` 与 Fragment, 每个 Fragment的 UI则由 Compose构建
- 移除 Fragment,完全基于 Compose实现 UI
下面就对这三种情况进行对比,迁移前,迁移第一步,迁移第二步
包体积
可以看到包体积减少了46%,方法数减少了17%。
代码行数
XML减少了76%
构建速度
构建时间缩短了29%。
渲染性能
针对列表页面进行测试。这是一个包含50个元素的列表,包含一个单选按钮和一些随机文本
需要对比的情况
- 完全使用compose
- 复杂试图使用compose,但是根布局依然在xml中
- 使用compose替换页面中一个个元素,而不是整个页面
- 可调试以及R8编译器的影响
可以看到compose的渲染并不快,尽管没有IO和反射操作,但是依然比XML慢。但是对于从代码复用以及声明式UI等优势上来讲,依然推荐使用compose
参考
沉思录 | 揭秘 Compose 原理:图解 Composable 的本质
原理分析,Jetpack Compose 完全脱离 View 系统了吗?
Jetpack Compose setContent 源码分析
Jetpack Compose — Before and after