前言
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层:[email protected][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
简单来说,
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
是
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
入口
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
的创建
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
的实现
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
上面说到,
Compose
的绘制仍然依赖于
Canvas
,但既然这样,
Compose
是怎么做到跨平台的呢?
这主要是通过良好的分层设计
Compose
在代码上自下而上依次分为6层:
其中
compose.runtime
和
compose.compiler
最为核心,它们是支撑声明式UI的基础。
而我们上面分析的
AndroidComposeView
这一部分,属于
compose.ui
部分,它主要负责
Android
设备相关的基础
UI
能力,例如
layout
、
measure
、
drawing
、
input
等
但这一部分是可以被替换的,
compose.runtime
提供了
NodeTree
管理等基础能力,此部分与平台无关,在此基础上各平台只需实现
UI
的渲染就是一套完整的声明式
UI
框架
基于
compose.runtime
可以实现任意一套声明式
UI
框架,关于
compose.runtime
的详细介绍可参考
fundroid
大佬写的:Jetpack Compose Runtime : 声明式 UI 的基础
Button
的特殊情况
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层:[email protected][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