原理分析,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 然后我们打开开发者选项中的显示布局边界,效果如下图所示:

我们可以看到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
}

如上所示

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

其中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系统这个问题,总结如下:

  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来实现水波纹效果

作者:程序员江同学
转载来源于:https://juejin.cn/post/7017811394036760612
如有侵权,请联系删除!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容