Android开发|Jetpack Compose 完全脱离 View 系统了吗?
前言 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
然后我们打开开发者选项中的
显示布局边界
,效果如下图所示:【Android开发|Jetpack Compose 完全脱离 View 系统了吗?】
文章图片
我们可以看到
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 {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(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(val root: T) : Applier {
private val stack = mutableListOf()
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
框架基于
compose.runtime
可以实现任意一套声明式UI
框架,关于compose.runtime
的详细介绍可参考fundroid
大佬写的:Jetpack Compose Runtime : 声明式 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
来实现水波纹效果
推荐阅读
- android第三方框架(五)ButterKnife
- 深入理解Go之generate
- Android中的AES加密-下
- 标签、语法规范、内联框架、超链接、CSS的编写位置、CSS语法、开发工具、块和内联、常用选择器、后代元素选择器、伪类、伪元素。
- 带有Hilt的Android上的依赖注入
- android|android studio中ndk的使用
- Android事件传递源码分析
- RxJava|RxJava 在Android项目中的使用(一)
- Android7.0|Android7.0 第三方应用无法访问私有库
- 深入理解|深入理解 Android 9.0 Crash 机制(二)