天天看点

Jetpack Compose 完全脱离 View 系统了吗?

前言

Compose

正式发布1.0已经相当一段时间了,但相信很多同学对

Compose

还是有很多迷惑的地方

Compose

跟原生的

View

到底是什么关系?是跟

Flutter

一样完全基于

Skia

引擎渲染,还是说还是

View

的那老一套?

相信很多同学都会有下面的疑问

Jetpack Compose 完全脱离 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

然后我们打开开发者选项中的

显示布局边界

,效果如下图所示:

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层:[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

页面的页面层级如下图所示:

Jetpack Compose 完全脱离 View 系统了吗?

原理分析

前置知识

我们知道,在

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

的过程。
Jetpack Compose 完全脱离 View 系统了吗?

为了了解

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
}
           

如上所示

  1. Composition

    中需要传入两个参数,

    Applier

    Recomposer

  2. Applier

    上面已经介绍过了,

    Recomposer

    非常重要,他负责

    Compose

    的重组,当重组后,

    Recomposer

    通过调用

    Applier

    完成

    NodeTree

    的变更
  3. 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

绘制的入口

小结

  1. Compose

    在构建

    NodeTree

    的过程中主要通过

    Composition

    ,

    Applier

    ,

    Recomposer

    构建,

    Applier

    会将所有节点添加到

    AndroidComposeView

    中的

    root

    节点下
  2. setContent

    的过程中,会创建

    ComposeView

    AndroidComposeView

    ,其中

    AndroidComposeView

    Compose

    的入口
  3. AndroidComposeView

    dispatchDraw

    中会通过

    root

    向下遍历子节点进行测量布局与绘制,这里是

    LayoutNode

    绘制的入口
  4. Android

    平台上,

    Compose

    的布局与绘制已基本脱离

    View

    体系,但仍然依赖于

    Canvas

Compose

与跨平台

上面说到,

Compose

的绘制仍然依赖于

Canvas

,但既然这样,

Compose

是怎么做到跨平台的呢?

这主要是通过良好的分层设计

Compose

在代码上自下而上依次分为6层:

Jetpack Compose 完全脱离 View 系统了吗?

其中

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层:[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

系统这个问题,总结如下:

  1. Compose

    在渲染时并不会转化成

    View

    ,而是只有一个入口

    View

    ,即

    AndroidComposeView

    ,纯

    Compose

    项目下,

    AndroidComposeView

    没有子

    View

  2. 我们声明的

    Compose

    布局在渲染时会转化成

    NodeTree

    ,

    AndroidComposeView

    中会触发

    NodeTree

    的布局与绘制,

    AndroidComposeView#dispatchDraw

    是绘制的入口
  3. Android

    平台上,

    Compose

    的布局与绘制已基本脱离

    View

    体系,但仍然依赖于

    Canvas

  4. 由于良好的分层体系,

    Compose

    可通过

    compose.runtime

    compose.compiler

    实现跨平台
  5. 在使用

    Button

    时,

    AndroidComposeView

    会有两层子

    View

    ,这是因为

    Button

    中使用了

    View

    来实现水波纹效果