资讯详情

【辨析】Jetpack Compose完全脱离View系统了吗?

5335fab8053c156cf0cd651fa868fa55.png

/ 今日科技快讯 /

最近,小米相关负责人表示,商业用地早在2018年上海总部规划,上海总部职能包括手机研发、金融、互联网等业务,是租赁物业分散办公室,土地未来建设规划是这些团队,而不是外部猜测。

/ 作者简介 /

本文来自RicardoMJiang文章主要分析比较了投稿Compose和View系统的相互关系,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

RicardoMJiang博客地址:

https://juejin.cn/user/668101431009496

/ 前言 /

Compose正式发布1.0已经很久了,但我相信很多学生都是对的Compose还有很多困惑。

Compose跟原生的View到底是什么关系?是和吗?Flutter同样完全基于Skia引擎渲染,或者View老一套?

相信很多同学都会有以下问题。

让我们来看看下面的问题。

/ 现象分析 /

让我们先看看这样一个简单的布局:

classTestActivity:ComponentActivity(){ overridefunonCreate(savedInstanceState:Bundle?){ setContent{ ComposeBody() } } }  @Composable funComposeBody(){ 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。

overridefunonResume(){ super.onResume() window.decorView.postDelayed({ (window.decorViewas?ViewGroup)?.let{transverse(it,1)} },2000) }  privatefuntransverse(view:View,index:Int){ Log.e("debug","第${index}层:" view) if(viewisViewGroup){ 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的节点,每个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。

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的源码其实比较简单,我们一起来看下:

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上。

下面我们来看下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中。

上面已经创建了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框架。

基于compose.runtime可以实现任意一套声明式UI框架,关于compose.runtime的详细介绍可参考fundroid大佬写的:Jetpack Compose Runtime : 声明式 UI 的基础。

Jetpack Compose Runtime : 声明式 UI 的基础:

https://juejin.cn/post/6976435919666544653

/   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的注释。

意思也很简单,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来实现水波纹效果

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

带倒计时RecyclerView的设计心路历程

Android 12上焕然一新的小组件

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

标签: 703f振动变送器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台