Compose 二三事:绘制原理

news2025/1/25 1:51:47

setContent做了什么

我们基于一个最简单的例子进行分析

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text(text = "Hello World!")
        }
    }
}

这里setContent做了什么,熟悉Kotlin的应该知道,这里是一个函数,利用了Kotlin高阶函数的特性。说明Compose本质上是一个函数

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) 

如上,他是一个被Composale修饰的函数,所有的Compose代码都必须被包含在@Composable注解的作用域里,这样才能被Compose编译器识别。

    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }

继续分析函数体的内容,可以看到本质上是转换成了ComposeView,然后再调用Activity的setContentView方法。

在这里插入图片描述

那么继续分析ComposeView,可以发现他继承链为ComposeView ==> AbstractComposeView ==> ViewGroup,我们发现ComposeView本质上是一个ViewGroup,那么是否可以认为Compose还是用了view绘制那一套,只是换了个Kotlin的壳呢?

Compose 本质就是自定义的 ViewGroup?

我们通过adb命令分析布局层级,验证我们的猜想

adb shell dumpsys activity top 

结果如下

      DecorView@28fe157[MainActivity]
        android.widget.LinearLayout{e35a444 V.E...... ........ 0,0-1080,2296}
          android.view.ViewStub{57bd92d G.E...... ......I. 0,0-0,0 #10201b1 android:id/action_mode_bar_stub}
          android.widget.FrameLayout{7b55e62 V.E...... ........ 0,75-1080,2296 #1020002 android:id/content}
            androidx.compose.ui.platform.ComposeView{cc73bf3 V.E...... ........ 0,0-228,52}
              androidx.compose.ui.platform.AndroidComposeView{3779fb7 VFED..... ........ 0,0-228,52}
        android.view.View{60ac0b0 V.ED..... ........ 0,2296-1080,2340 #1020030 android:id/navigationBarBackground}
        android.view.View{169db29 V.ED..... ........ 0,0-1080,75 #102002f android:id/statusBarBackground}

在这里插入图片描述

可以看到最上层是AndroidComposeView,这个类也是ViewGroup。但是除此之外,并没有看到我们在布局中添加的Text

internal class AndroidComposeView(context: Context) : ViewGroup(context)

从setContent分析,我们添加的compose函数最终通过ComposeView的setContent设置到ComposeView里面,那么分析ComposeView的setContent方法

private fun doSetContent(
    owner: AndroidComposeView,
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    if (inspectionWanted(owner)) {
        owner.setTag(
            R.id.inspection_slot_table_set,
            Collections.newSetFromMap(WeakHashMap<CompositionData, Boolean>())
        )
        enableDebugInspectorInfo()
    }
  	// 创建Composition对象,传入UiApplier
    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)
        }
 		// 传入content函数
    wrapped.setContent(content)
    return wrapped
}

CompositionContext是什么?

WrappedComposition是什么?WrappedComposition继承Composition,接收Composition以及AndroidComposeView

private class WrappedComposition(
 val owner: AndroidComposeView,
 val original: Composition
) : Composition

分析调用链,发现他最终调用到了doSetContent方法

  1. 创建Composition对象,传入UiApplier
  2. 传入content函数
internal class UiApplier(
    root: LayoutNode
) 

在这里插入图片描述

在这里插入图片描述

这里一个个的来分析,UiApplier是什么?可以看到他传入了AndroidComposeView的LayoutNode对象。Android的View系统中有viewTree,描述整个UI界面,那么LayoutNode就不难理解,Compose渲染的时候,每一个组件就是一个LayoutNode,最终组成一个LayoutNode树,来描述UI界面。

LayoutNode的生成

以Text为例,我们查看他的源码

@Composable
fun Text(
    text: String,
    modifier: Modifier = Modifier,
    ...
) {

   ...
    BasicText(
        text,
        modifier,
        mergedStyle,
        onTextLayout,
        overflow,
        softWrap,
        maxLines,
    )
}

@OptIn(InternalFoundationTextApi::class)
@Composable
fun BasicText(
    text: String,
    modifier: Modifier = Modifier,
    ...
) {
   ...

    Layout(modifier.then(controller.modifiers), controller.measurePolicy)
}

// 布局
inline fun Layout(
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val density = LocalDensity.current
    val layoutDirection = LocalLayoutDirection.current
    val viewConfiguration = LocalViewConfiguration.current
    val materialized = currentComposer.materialize(modifier)
    ReusableComposeNode<ComposeUiNode, Applier<Any>>(
        factory = ComposeUiNode.Constructor,
        update = {
            set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
            set(density, ComposeUiNode.SetDensity)
            set(layoutDirection, ComposeUiNode.SetLayoutDirection)
            set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
            set(materialized, ComposeUiNode.SetModifier)
        },
    )
}

@Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
@Composable inline fun <T : Any, reified E : Applier<*>> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startReusableNode()
    if (currentComposer.inserting) {
      	// 生成LayoutNode
        currentComposer.createNode { factory() }
    } else {
        currentComposer.useNode()
    }
    currentComposer.disableReusing()
    Updater<T>(currentComposer).update()
    currentComposer.enableReusing()
    currentComposer.endNode()
}

从这里可看出,最终Text调用了Layout函数,生成了 ComposeUiNode,LayoutNode就是ComposeUiNode的实现类。

在这里插入图片描述

那么一个简单界面的布局关系就如下所示

在这里插入图片描述

那UiApplier就不难理解了,它是LayoutNode树的管理器,可以增删NodeTree的节点。

compose的起点:Composition

接着分析Composition,他是compose的起点,代表整个compose的执行。

private class WrappedComposition(
    val owner: AndroidComposeView,
    val original: Composition
) : Composition, LifecycleEventObserver {

    private var disposed = false
    private var addedToLifecycle: Lifecycle? = null
    private var lastContent: @Composable () -> Unit = {}

    override fun setContent(content: @Composable () -> Unit) {
        owner.setOnViewTreeOwnersAvailable {
            if (!disposed) {
                val lifecycle = it.lifecycleOwner.lifecycle
                lastContent = content
                if (addedToLifecycle == null) {
                  // 1.初始化流程,首次进入
                    addedToLifecycle = lifecycle
                    // this will call ON_CREATE synchronously if we already created
                  // 2.添加生命周期监听
                    lifecycle.addObserver(this)
                } else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
                  // 4.调用连接器的setContent
                    original.setContent {

                        @Suppress("UNCHECKED_CAST")
                        val inspectionTable =
                            owner.getTag(R.id.inspection_slot_table_set) as?
                                MutableSet<CompositionData>
                                ?: (owner.parent as? View)?.getTag(R.id.inspection_slot_table_set)
                                    as? MutableSet<CompositionData>
                        if (inspectionTable != null) {
                            inspectionTable.add(currentComposer.compositionData)
                            currentComposer.collectParameterInformation()
                        }

                        LaunchedEffect(owner) { owner.keyboardVisibilityEventLoop() }
                        LaunchedEffect(owner) { owner.boundsUpdatesEventLoop() }

                        CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
                            ProvideAndroidCompositionLocals(owner, content)
                        }
                    }
                }
            }
        }
    }

    override fun dispose() {
        if (!disposed) {
            disposed = true
            owner.view.setTag(R.id.wrapped_composition_tag, null)
          // 移除生命周期
            addedToLifecycle?.removeObserver(this)
        }
        original.dispose()
    }

    override val hasInvalidations get() = original.hasInvalidations
    override val isDisposed: Boolean get() = original.isDisposed

    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            dispose()
        } else if (event == Lifecycle.Event.ON_CREATE) {
            if (!disposed) {
              // 3.触发界面onCreate的时候重新执行
                setContent(lastContent)
            }
        }
    }
}

简单分析以上流程,Composition会注册生命周期监听,在onCreate的时候才会触发界面的创建。另外看这里的方法,跟Flutter是一模一样。

Composition#setContent

    override fun setContent(content: @Composable () -> Unit) {
        check(!disposed) { "The composition is disposed" }
        this.composable = content
        parent.composeInitial(this, composable)
    }		

Recomposer#composeInitial

   internal override fun composeInitial(
        composition: ControlledComposition,
        content: @Composable () -> Unit
    ) {
        val composerWasComposing = composition.isComposing
        composing(composition, null) {
            composition.composeContent(content)
        }
        ...
    }

RecompositionImpl#composeContent

override fun composeContent(content: @Composable () -> Unit) {
        // TODO: This should raise a signal to any currently running recompose calls
        // to halt and return
        trackAbandonedValues {
            synchronized(lock) {
                drainPendingModificationsForCompositionLocked()
                composer.composeContent(takeInvalidations(), content)
            }
        }
    }

Composer#composeContent

    internal fun composeContent(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: @Composable () -> Unit
    ) {
        runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
        doCompose(invalidationsRequested, content)
    }

private fun doCompose(
        invalidationsRequested: IdentityArrayMap<RecomposeScopeImpl, IdentityArraySet<Any>?>,
        content: (@Composable () -> Unit)?
    ) {
        runtimeCheck(!isComposing) { "Reentrant composition is not supported" }
        trace("Compose:recompose") {
            snapshot = currentSnapshot()
            compositionToken = snapshot.id
            providerUpdates.clear()
            invalidationsRequested.forEach { scope, set ->
                val location = scope.anchor?.location ?: return
                invalidations.add(Invalidation(scope, location, set))
            }
            invalidations.sortBy { it.location }
            nodeIndex = 0
            var complete = false
            isComposing = true
            try {
                startRoot()

                // vv Experimental for forced
                @Suppress("UNCHECKED_CAST")
                val savedContent = nextSlot()
                if (savedContent !== content && content != null) {
                    updateValue(content as Any?)
                }
                // ^^ Experimental for forced

                // Ignore reads of derivedStateOf recalculations
                observeDerivedStateRecalculations(
                    start = {
                        childrenComposing++
                    },
                    done = {
                        childrenComposing--
                    },
                ) {
                    if (content != null) {
                        startGroup(invocationKey, invocation)
                        invokeComposable(this, content)
                        endGroup()
                    } else if (
                        forciblyRecompose &&
                        savedContent != null &&
                        savedContent != Composer.Empty
                    ) {
                        startGroup(invocationKey, invocation)
                        @Suppress("UNCHECKED_CAST")
                        invokeComposable(this, savedContent as @Composable () -> Unit)
                        endGroup()
                    } else {
                        skipCurrentGroup()
                    }
                }
                endRoot()
                complete = true
            } finally {
                isComposing = false
                invalidations.clear()
                if (!complete) abortRoot()
            }
        }
    }

再进入 invokeComposable(this, content)

   public static final void invokeComposable(@NotNull Composer composer, @NotNull Function2 composable) {
      Intrinsics.checkNotNullParameter(composer, "composer");
      Intrinsics.checkNotNullParameter(composable, "composable");
      Function2 realFn = (Function2)TypeIntrinsics.beforeCheckcastToFunctionOfArity(composable, 2);
      realFn.invoke(composer, 1);
   }

这里就是对布局进行组合了,这里就不再做分析了。

布局与绘制

布局与绘制需要分析dispatchDraw方法

override fun dispatchDraw(canvas: android.graphics.Canvas) {
        ...
  			
  			// 测量与布局
        measureAndLayout()

        // we don't have to observe here because the root has a layer modifier
        // that will observe all children. The AndroidComposeView has only the
        // root, so it doesn't have to invalidate itself based on model changes.
  			// 绘制
        canvasHolder.drawInto(canvas) { root.draw(this) }

       ...
    }

虽然测量与布局是Compose自己实现的,但是绘制最终调用了Canvas。

Compose的Text和TextView的区别

根据Compose的绘制可知,本质上Compose还是通过Cavas来绘制的,所以他和TextView也一样,最终调用了drawText

Compose的性能

以一个开源的电影APP(tivi)为例,原来是基于Fragment和XML,现在逐步迁移到Compose。

迁移分为两步

  1. 迁移到迁移到 Navigatio` 与 Fragment, 每个 Fragment的 UI则由 Compose构建
  2. 移除 Fragment,完全基于 Compose实现 UI

下面就对这三种情况进行对比,迁移前,迁移第一步,迁移第二步

包体积

在这里插入图片描述

在这里插入图片描述

可以看到包体积减少了46%,方法数减少了17%。

代码行数

在这里插入图片描述

XML减少了76%

构建速度

在这里插入图片描述

构建时间缩短了29%。

渲染性能

针对列表页面进行测试。这是一个包含50个元素的列表,包含一个单选按钮和一些随机文本

在这里插入图片描述

需要对比的情况

  • 完全使用compose
  • 复杂试图使用compose,但是根布局依然在xml中
  • 使用compose替换页面中一个个元素,而不是整个页面
  • 可调试以及R8编译器的影响

在这里插入图片描述

可以看到compose的渲染并不快,尽管没有IO和反射操作,但是依然比XML慢。但是对于从代码复用以及声明式UI等优势上来讲,依然推荐使用compose

参考

沉思录 | 揭秘 Compose 原理:图解 Composable 的本质

原理分析,Jetpack Compose 完全脱离 View 系统了吗?

Jetpack Compose setContent 源码分析

Jetpack Compose — Before and after

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/559415.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Jmeter入门】使用Jmeter做接口测试

目录 一、Jmeter简介 二、Jmeter相关插件安装&#xff1a; 三、Jmeter运行模式及参数 四、Jmeter函数 五、Jmeter属性与变量 六、Jmeter如何做接口测试 【1】、Jmeter 的使用步骤 【2】、接口请求实例 1. “用户定义的变量” 的应用 3. 建立接口间的关联 4…

Gaussian量子化学计算、LAMMPS分子动力学模拟

Gaussian是做半经验计算和从头计算使用最广泛的量子化学软件&#xff0c;可研究诸如分子轨道&#xff0c;结构优化&#xff0c;过渡态搜索&#xff0c;热力学性质&#xff0c;偶极矩和多极矩&#xff0c;电子密度和电势&#xff0c;极化率和超极化率&#xff0c;红外和拉曼光谱…

超越竞争的获客之道:DTC品牌出海策略全面解析

随着全球数字化的快速发展&#xff0c;DTC品牌正迎来一个全新的时代。然而&#xff0c;随着越来越多的DTC品牌进入国际市场&#xff0c;如何在激烈的竞争中脱颖而出&#xff0c;并获得新客户成为一个关键的挑战。本文Nox聚星将和大家深入探讨DTC品牌在出海时代如何破解获客困局…

外包干了五年,废了...

先说一下自己的情况。大专生&#xff0c;17年通过校招进入湖南某软件公司&#xff0c;干了接近5年的测试点点点&#xff0c;今年年上旬&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01;而我已经在一个企业干了五年的点工…

10年开发,浅谈eolink 、aifox、apipost 横向对比

功能对比 在实际工作中,eolink 、apifox、apipost 三个工具,我个人都有使用。接下来,我会对三款功能对比,于是我拉了个功能对比的清单。 特别说明:以下的对比,不吹不黑,只列功能,纯客观比对,不带有任何商业带货的意义。 eolink 、aifox、apipost 三款工具有很多功能模块,本次仅…

acwing提高——DFS之连通性问题+搜索顺序

1 连通性问题&#xff08;内部搜索&#xff09; 内部搜索一般不用恢复现场 1.迷宫 题目http://ybt.ssoier.cn:8088/problem_show.php?pid1215 #include<bits/stdc.h> using namespace std; const int N110; bool st[N][N]; char g[N][N]; int n; int sx,sy,ex,ey; …

95后字节八年测开晒出工资单:狠补了这个,真香···

最近一哥们跟我聊天装逼&#xff0c;说他最近从字节跳槽了&#xff0c;我问他跳出来拿了多少&#xff1f;哥们表示很得意&#xff0c;说跳槽到新公司一个月后发了工资&#xff0c;月入5万多&#xff0c;表示很满足&#xff01;这样的高薪资着实让人羡慕&#xff0c;我猜这是税后…

2022年深圳杯数学建模B题基于用电可靠性的配电网规划解题全过程文档及程序

2022年深圳杯数学建模 B题 基于用电可靠性的配电网规划 原题再现&#xff1a; 如果一批用户变压器&#xff08;下面简称用户&#xff09;仅由一个电源变电站&#xff08;下面简称电源&#xff09;供电&#xff0c;称为单供。这时配电网由电线和开关联接成以电源为根节点的树状…

k8s使用ECK形式部署elasticsearch+kibana

文章目录 前言一、ECK是什么&#xff1f;二、安装ECK1.crd.yaml2.operator.yaml 三、安装es&#xff0c;elasticsearch-cluster.yaml四、安装kibana总结 前言 在k8s上基于ECK&#xff08;2.4&#xff09;部署elasticsearch&#xff0c;简单记录一下&#xff0c;主要是quicksta…

《程序员面试金典(第6版)》面试题 02.03. 删除中间节点(特殊的删除节点操作)

题目描述 若链表中的某个节点&#xff0c;既不是链表头节点&#xff0c;也不是链表尾节点&#xff0c;则称其为该链表的「中间节点」。 题目传送门&#xff1a;面试题 02.03. 删除中间节点 假定已知链表的某一个中间节点&#xff0c;请实现一种算法&#xff0c;将该节点从链表中…

服务(第二十九篇)zabbix

zabbix 是什么&#xff1f; zabbix 是一个基于 Web 界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。 zabbix 能监视各种网络参数&#xff0c;保证服务器系统的安全运营&#xff1b;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。 zabbi…

太阳升起和落下(长文警告)

今天分享一个太阳升起落下的动画场景。 有朋友问我为什么只发代码不做说明&#xff0c;今天我们尝试下对代码进行注解说明一下。 首先这个场景大致的可拆分为4个部分&#xff1a;太阳/月亮&#xff0c;右下角的按钮&#xff0c;天上的云和最显眼的建筑。 我们先做一个按钮&am…

stable diffusion AI绘图工具的安装和使用centos7.8系统

stable diffusion 作图工具本地部署 重要【AI作画】stable diffusion webui Linux虚拟机 Centos 详细部署教程 服务器CentOS 7 安装 Stable Diffusion WebUI &#xff0c;并映射到本地浏览器 CentOs7 Stable Diffusion Novel AI实现AI绘画 stable diffusion webui安装部署…

二、数据仓库详细介绍

基础概念 架构与框架&#xff0c;架构是结构&#xff0c;框架是规范 模块与组件&#xff0c;模块是逻辑概念&#xff0c;通过分解使复杂问题简单化&#xff0c;组件是物理概念&#xff0c;将具体的模块落地&#xff0c;且各个组件间保持松散耦合 定义&#xff1a;架构&#x…

分布式全局唯一id实现总结

前言&#xff1a;本文意在对借助db和程序生成分布式id进行一些总结&#xff0c;以及对其特性进行比较分析&#xff1b; 1 实现方式&#xff1a; Db 通过配置步长和初始值的方式&#xff0c;使得每个db库生成id 的不同性&#xff0c;如 3个db 实例情况下&#xff1a;其步长均设…

我3年前写的博客,又被别人抄去发论文了,该论文整个正文部分几乎直接照抄我的博客

我想说每一篇原创博客都是作者的心血&#xff0c;有时候写一篇博客也许会花一天&#xff0c;甚至好几天的时间&#xff0c;尊重原创&#xff0c;营造好的环境&#xff0c;才有可能出现更多优质的博文&#xff0c;而不是到处都是抄来抄去的低质量水文。 前几天接到来自粉丝的私信…

加密解密软件VMProtect教程(八)许可制度之集成到应用程序

VMProtect是新一代软件保护实用程序。VMProtect支持德尔菲、Borland C Builder、Visual C/C、Visual Basic&#xff08;本机&#xff09;、Virtual Pascal和XCode编译器。 同时&#xff0c;VMProtect有一个内置的反汇编程序&#xff0c;可以与Windows和Mac OS X可执行文件一起…

全网最快PCB打样| 急速小批量打样的秘密在这里

对于广大爱好者以及电子工程师来说&#xff0c;电子行业的快速发展&#xff0c;导致电子产品的多样化和个性化&#xff0c;电路设计的复杂度和难度提高&#xff0c;需要更多的PCB打样来验证和测试&#xff0c;满足细分市场的客户需求。 随着PCB打样厂家的服务优化和价格降低&am…

信息收集-服务器信息

服务器上面可以运行大量的系统服务和第三方应用服务&#xff0c;如果操作系统或者第三方软件没有及时升级打补丁&#xff0c;攻击者就有可能直接通过服务器上运行的服务进行攻击。 服务器需要收集的信息包含三个方面&#xff1a; 操作系统信息等识别waf&#xff08;Web应用程…

高级测试必备技能:从session请求到token请求,傻瓜式掌握Charles和postman断言技巧

引言 在快速发展的数字化时代&#xff0c;软件测试作为保障软件品质和用户体验的重要一环&#xff0c;显得愈加重要。SESSION请求、TOKEN请求、charles使用和Postman断言等技术&#xff0c;成为测试人员不可或缺的必备技能。 在这篇文章中&#xff0c;我将深入浅出地为您讲解…