前言
Compose正式发布1.0已经相当一段时间了,但相信很多同学对Compose还是有很多迷惑的地方 Compose跟原生的View到底是什么关系?是跟Flutter一样完全基于Skia引擎渲染,还是说还是View的那老一套? 相信很多同学都会有下面的疑问

下面我们就一起来看下下面这个问题
现象分析
我们先看这样一个简单布局
class TestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
ComposeBody()
}
}
}
@Composable
fun ComposeBody() {
Column {
Text(text = "这是一行测试数据", color = Color.Black, style = MaterialTheme.typography.h6)
Row() {
Text(text = "测试数据1!", color = Color.Black, style = MaterialTheme.typography.h6)
Text(text = "测试数据2!", color = Color.Black, style = MaterialTheme.typography.h6)
}
}
}
如上所示,就是一个简单的布局,包含Column,Row与Text 然后我们打开开发者选项中的显示布局边界,效果如下图所示:

我们可以看到Compose的组件显示了布局边界,我们知道,Flutter与WebView H5内的组件都是不会显示布局边界的,难道Compose的布局渲染其实还是View的那一套?
我们下面再在onResume时尝试遍历一下View的层级,看一下Compose到底会不会转化成View
override fun onResume() {
super.onResume()
window.decorView.postDelayed({
(window.decorView as? ViewGroup)?.let { transverse(it, 1) }
}, 2000)
}
private fun transverse(view: View, index: Int) {
Log.e("debug", "第${index}层:" + view)
if (view is ViewGroup) {
view.children.forEach { transverse(it, index + 1) }
}
}
通过以上方式打印页面的层级,输出结果如下:
E/debug: 第1层:DecorView@c2f703f[RallyActivity]
E/debug: 第2层:android.widget.LinearLayout{4202d0c V.E...... ........ 0,0-1080,2340}
E/debug: 第3层:android.view.ViewStub{2b50655 G.E...... ......I. 0,0-0,0 #10201b1 android:id/action_mode_bar_stub}
E/debug: 第3层:android.widget.FrameLayout{9bfc86a V.E...... ........ 0,90-1080,2340 #1020002 android:id/content}
E/debug: 第4层:androidx.compose.ui.platform.ComposeView{1b4d15b V.E...... ........ 0,0-1080,2250}
E/debug: 第5层:androidx.compose.ui.platform.AndroidComposeView{a8ec543 VFED..... ........ 0,0-1080,2250}
如上所示,我们写的Column,Row,Text并没有出现在布局层级中,跟Compose相关的只有ComposeView与AndroidComposeView两个View 而ComposeView与AndroidComposeView都是在setContent时添加进去的Compose的容器,我们后面再分析,这里先给出结论
Compose
在渲染时并不会转化成View,而是只有一个入口View,即AndroidComposeView我们声明的Compose布局在渲染时会转化成NodeTree,AndroidComposeView中会触发NodeTree的布局与绘制 总得来说,Compose会有一个View的入口,但它的布局与渲染还是在LayoutNode上完成的,基本脱离了View
总得来说,纯Compose页面的页面层级如下图所示:

原理分析
前置知识
我们知道,在View系统中会有一棵ViewTree,通过一个树的数据结构来描述整个UI界面 在Compose中,我们写的代码在渲染时也会构建成一个NodeTree,每一个组件就是一个ComposeNode,作为NodeTree上的一个节点
Compose 对 NodeTree 管理涉及 Applier、Composition 和 ComposeNode: Composition 作为起点,发起首次的 composition,通过 Compose 的执行,填充 Slot Table,并基于 Table 创建 NodeTree。渲染引擎基于 Compose Nodes 渲染 UI, 每当 recomposition 发生时,都会通过 Applier 对 NodeTree 进行更新。 因此
Compose的执行过程就是创建Node并构建NodeTree的过程。

为了了解NodeTree的构建过程,我们来介绍下面几个概念
Applier:增删 NodeTree 的节点
简单来说,Applier的作用就是增删NodeTree的节点,每个NodeTree的运算都需要配套一个Applier。 同时,Applier 会提供回调,基于回调我们可以对 NodeTree 进行自定义修改:
interface Applier<N> {
val current: N // 当前处理的节点
fun onBeginChanges() {}
fun onEndChanges() {}
fun down(node: N)
fun up()
fun insertTopDown(index: Int, instance: N) // 添加节点(自顶向下)
fun insertBottomUp(index: Int, instance: N)// 添加节点(自底向上)
fun remove(index: Int, count: Int) //删除节点
fun move(from: Int, to: Int, count: Int) // 移动节点
fun clear()
}
如上所示,节点增删时会回调到Applier中,我们可以在回调的方法中自定义节点添加或删除时的逻辑,后面我们可以一起看下在Android平台Compose是怎样处理的
Composition: Compose执行的起点
Composition`是`Compose`执行的起点,我们来看下如何创建一个`Composition
val composition = Composition(
applier = NodeApplier(node = Node()),
parent = Recomposer(Dispatchers.Main)
)
composition.setContent {
// Composable function calls
}
如上所示
-
Composition中需要传入两个参数,Applier与Recomposer -
Applier上面已经介绍过了,Recomposer非常重要,他负责Compose的重组,当重组后,Recomposer通过调用Applier完成NodeTree的变更 -
Composition#setContent为后续Compose的调用提供了容器
通过上面的介绍,我们了解了NodeTree构建的基本流程,下面我们一起来分析下setContent的源码
setContent过程分析
setContent入口
setContent的源码其实比较简单,我们一起来看下:
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
//判断ComposeView是否存在,如果存在则不创建
if (existingComposeView != null) with(existingComposeView) {
setContent(content)
} else ComposeView(this).apply {
//将Compose content添加到ComposeView上
setContent(content)
// 将ComposeView添加到DecorView上
setContentView(this, DefaultActivityContentLayoutParams)
}
}
上面就是setContent的入口,主要作用就是创建了一个ComposeView并添加到DecorView上
Composition的创建
下面我们来看下AndroidComposeView与Composition是怎样创建的 通过ComposeView#setContent->AbstractComposeView#createComposition->AbstractComposeView#ensureCompositionCreated->ViewGroup#setContent 最后会调用到doSetContent方法,这里就是Compose的入口:Composition创建的地方
private fun doSetContent(
owner: AndroidComposeView, //AndroidComposeView是owner
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
//..
//创建Composition,并传入Applier与Recomposer
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)
}
//将Compose内容添加到Composition中
wrapped.setContent(content)
return wrapped
}
如上所示,主要就是创建一个Composition并传入UIApplier与Recomposer,并将Compose content传入Composition中
UiApplier的实现
上面已经创建了Composition并传入了UIApplier,后续添加了Node都会回调到UIApplier中
internal class UiApplier(
root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
//...
override fun insertBottomUp(index: Int, instance: LayoutNode) {
current.insertAt(index, instance)
}
//...
}
如上所示,在插入节点时,会调用current.insertAt方法,那么这个current到底是什么呢?
private fun doSetContent(
owner: AndroidComposeView, //AndroidComposeView是owner
): Composition {
//UiApplier传入的参数即为AndroidComposeView.root
val original = Composition(UiApplier(owner.root), parent)
}
abstract class AbstractApplier<T>(val root: T) : Applier<T> {
private val stack = mutableListOf<T>()
override var current: T = root
}
}
可以看出,UiApplier中传入的参数其实就是AndroidComposeView的root,即current就是AndroidComposeView的root
# AndroidComposeView
override val root = LayoutNode().also {
it.measurePolicy = RootMeasurePolicy
//...
}
如上所示,root其实就是一个LayoutNode,通过上面我们知道,所有的节点都会通过Applier插入到root下
布局与绘制入口
上面我们已经在AndroidComposeView中拿到NodeTree的根结点了,那Compose的布局与测量到底是怎么触发的呢?
# AndroidComposeView
override fun dispatchDraw(canvas: android.graphics.Canvas) {
//Compose测量与布局入口
measureAndLayout()
//Compose绘制入口
canvasHolder.drawInto(canvas) { root.draw(this) }
//...
}
override fun measureAndLayout() {
val rootNodeResized = measureAndLayoutDelegate.measureAndLayout()
measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
如上所示,AndroidComposeView会通过root,向下遍历它的子节点进行测量布局与绘制,这里就是LayoutNode绘制的入口
小结
-
Compose在构建NodeTree的过程中主要通过Composition,Applier,Recomposer构建,Applier会将所有节点添加到AndroidComposeView中的root节点下 - 在
setContent的过程中,会创建ComposeView与AndroidComposeView,其中AndroidComposeView是Compose的入口 -
AndroidComposeView在dispatchDraw中会通过root向下遍历子节点进行测量布局与绘制,这里是LayoutNode绘制的入口 - 在
Android平台上,Compose的布局与绘制已基本脱离View体系,但仍然依赖于Canvas
Compose与跨平台
上面说到,Compose的绘制仍然依赖于Canvas,但既然这样,Compose是怎么做到跨平台的呢? 这主要是通过良好的分层设计
Compose 在代码上自下而上依次分为6层:

其中compose.runtime和compose.compiler最为核心,它们是支撑声明式UI的基础。
而我们上面分析的AndroidComposeView这一部分,属于compose.ui部分,它主要负责Android设备相关的基础UI能力,例如 layout、measure、drawing、input 等 但这一部分是可以被替换的,compose.runtime 提供了 NodeTree 管理等基础能力,此部分与平台无关,在此基础上各平台只需实现UI的渲染就是一套完整的声明式UI框架
Button的特殊情况
上面我们介绍了在纯Compose项目下,AndroidComposeView不会有子View,而是遍历LayoutnNode来布局测量绘制 但如果我们在代码中加入一个Button,结果可能就不太一样了
@Composable
fun ComposeBody() {
Column {
Text(text = "这是一行测试数据", color = Color.Black, style = MaterialTheme.typography.h6)
Row() {
Text(text = "测试数据1!", color = Color.Black, style = MaterialTheme.typography.h6)
Text(text = "测试数据2!", color = Color.Black, style = MaterialTheme.typography.h6)
}
Button(onClick = {}) {
Text(text = "这是一个Button",color = Color.White)
}
}
}
然后我们再看看页面的层级结构
E/debug: 第1层:DecorView@182e858[RallyActivity]
E/debug: 第2层:android.widget.LinearLayout{397edb1 V.E...... ........ 0,0-1080,2340}
E/debug: 第3层:android.widget.FrameLayout{e2b0e17 V.E...... ........ 0,90-1080,2340 #1020002 android:id/content}
E/debug: 第4层:androidx.compose.ui.platform.ComposeView{36a3204 V.E...... ........ 0,0-1080,2250}
E/debug: 第5层:androidx.compose.ui.platform.AndroidComposeView{a8ec543 VFED..... ........ 0,0-1080,2250}
E/debug: 第6层:androidx.compose.material.ripple.RippleContainer{28cb3ed V.E...... ......I. 0,0-0,0}
E/debug: 第7层:androidx.compose.material.ripple.RippleHostView{b090222 V.ED..... ......I. 0,0-0,0}
可以看到,很明显,AndroidComposeView下多了两层子View,这是为什么呢?
我们一起来看下RippleHostView的注释
Empty View that hosts a RippleDrawable as its background. This is needed as RippleDrawables cannot currently be drawn directly to a android.graphics.RenderNode (b/184760109), so instead we rely on View's internal implementation to draw to the background android.graphics.RenderNode. A RippleContainer is used to manage and assign RippleHostViews when needed - see RippleContainer.getRippleHostView.
意思也很简单,Compose目前还不能直接绘制水波纹效果,因此需要将水波纹效果设置为View的背景,这里利用View做了一个中转 然后RippleHostView与RippleContainer自然会添加到AndroidComposeView中,如果我们在Compose中使用了AndroidView,效果也是一样的 但是这种情况并没有违背我们上面说的,纯Compose项目下,AndroidComposeView下没有子View,因为Button并不是纯Compose的
总结
本文主要分析回答了Compose到底有没有完全脱离View系统这个问题,总结如下:
-
Compose在渲染时并不会转化成View,而是只有一个入口View,即AndroidComposeView,纯Compose项目下,AndroidComposeView没有子View - 我们声明的
Compose布局在渲染时会转化成NodeTree,AndroidComposeView中会触发NodeTree的布局与绘制,AndroidComposeView#dispatchDraw是绘制的入口 - 在
Android平台上,Compose的布局与绘制已基本脱离View体系,但仍然依赖于Canvas - 由于良好的分层体系,
Compose可通过compose.runtime和compose.compiler实现跨平台 - 在使用
Button时,AndroidComposeView会有两层子View,这是因为Button中使用了View来实现水波纹效果
作者:程序员江同学
转载来源于:https://juejin.cn/post/7017811394036760612
如有侵权,请联系删除!