Compose 二三事:绘制原理

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

NetApp FAS 混合闪存阵列协助您建构简单易用、聪明智慧、值得信赖的储存基础架构

NetApp FAS 混合闪存阵列 主要优势 1、简单易用&#xff1a;节省您宝贵的时间、金钱和人力 •几分钟内完成储存资源配置。 •以获证实的效率技术降低成本。 •可在单一系统上管理档案与区块资料。 2、聪明智慧&#xff1a;灵活因应瞬息万变的业务需求 •以不中断营运的方式扩…

java(springboot+ssm)/python/php/nodejs/基于vue的景区门票预约管理系统

后端&#xff1a;java(springbootssm)/python/php/nodejs/ 开发运行&#xff1a;微信开发者/hbuilderx 后端:idea/eclipse/vscode/pycharm 模块划分&#xff1a;公告类型、公告信息、用户信息、用户咨询、地区信息、景区信息、景区开放、景区预约、统计信息 本技术是Java平台的…

企企通“码上顺”清洗工具 | 让数据更有价值,让业务更出色

数据清理工作是企业数据管理、数据治理中的最基础的工作之一&#xff0c;不仅是一项苦活、累活&#xff0c;也是一个既考验业务又检验技术的活。 物料主数据作为企业核心的数据资产&#xff0c;在智慧供应链、业财一体化等数字化建设中发挥着重要作用。在当今高速发展的商业环…

2023新版Spring6全新讲解-HelloSpring入门案例

Spring的入门案例 Spring6.0要求的JDK最低版本是17 我们在本课程中使用的版本是5.x版本。这个Spring5的JDK的最低要求是8 一、环境要求 JDK&#xff1a;8 Maven&#xff1a;3.6 Spring:5.3.27 开发工具&#xff1a;IDEA 2021.1.1 二、项目创建 1. 构建项目 在idea中&…

GEE:GEDI数据提取值到矢量区域和点

作者:CSDN @ _养乐多_ 本文将介绍GEDI数据集从GEE上下载到本地,并将每一个激光点的值提取为一个矢量区域,并提取值到矢量区域的方法。 文章目录 一、GEDI数据下载二、GEDI数据栅格矢量化三、提取值到区域四、提取栅格值到点五、空间插值一、GEDI数据下载 GEDI数据下载链接:…

opencv相机标定

当你把摄像机放在一个特定的位置&#xff0c;在它的后面放一个目标图像&#xff0c;或者是把摄像机放到某个物体上&#xff0c;摄像机周围的物体是什么形状&#xff0c;你需要知道这些信息。 当你在计算机上处理图像时&#xff0c;会使用以下三个参数&#xff1a; 1.像素坐标&a…

【我的创作纪念日】—— 纪念四年的坚持

这是一篇和技术无关的博客&#xff0c;但对我而言&#xff0c;它承载了不菲的价值 普通且宁静的一天&#xff0c;被一条消息戳中&#xff0c;于是&#xff0c;写一篇分享帖&#xff0c;纪念我这 1460 天的坚持初衷&#xff1a; 前言&#xff1a;对过去的回顾 4 年前的我&#…

2098-DSD-020X 具有集成的DeviceNet通信接口

描述:2098-DSD-020X-DN是艾伦-布拉德利Ultra 3000运动控制系列的一部分。该产品是一种数字伺服驱动器&#xff0c;可在120VAC / 240 VAC、单相、50-60 Hz的输入电源电压和18安培的输入电流下运行。该伺服驱动器提供120 / 240 VAC的输出电压、3相、0-400 Hz的可编程频率范围、10…

深度学习训练营之Densenet网络

深度学习训练营 原文链接环境介绍前言设计理念网络结构实验结果和讨论pytorch实现DenseNet附录 原文链接 &#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f366; 参考文章&#xff1a;365天深度学习训练营-第J3周&#xff1a;Densenet网络学习&…

【数据分享】1901-2021年1km分辨率逐月平均气温栅格数据(全国/分省/免费获取)

气温数据是我们最常用的气象指标之一&#xff0c;之前我们给大家分享过1950-2022年0.1 x 0.1精度的逐月平均气温栅格数据和逐年平均气温栅格数据&#xff08;均可查询之前的文章获悉详情&#xff09;&#xff01; 本次我们分享的是精度更高的气温栅格数据——1901-2021年1km分…

【C++】类和对象(3)

文章目录 一、初始化列表二、explicit关键字三、static成员四、友元4.1 友元函数4.2 友元类 五、内部类六、匿名对象七、编译器的优化 一、初始化列表 首先我们先回顾一下构造函数&#xff0c;对象的初始化由构造函数来完成&#xff0c;我们可以在构造函数的函数体内对对象的成…

asp.net就业满意度问调查系统

本系统主要有会员&#xff08;调查者&#xff09;和管理员&#xff0c;他们具体的功能如下&#xff1a; 会员功能&#xff1a;注册&#xff0c;登录&#xff0c;修改个人信息&#xff0c;调查&#xff0c;查看调查结果及影响&#xff0c;留言,首先是会员注册&#xff0c;注册后…

广域通信网 - HDLC 高级数据链路控制协议

文章目录 1 概述2 HDLC2.1 帧类型2.2 帧结构 3 扩展3.1 网工软考真题 1 概述 #mermaid-svg-JEuFH1qP4tY5jI5p {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JEuFH1qP4tY5jI5p .error-icon{fill:#552222;}#mermaid-…

python如何连接mysql数据库

python链接mysql数据库要用到pymysql模块中的connect &#xff0c;connect函数是pymysql模块中 用于连接MySQL数据库的一个函数。 所以连接mysql之前需要先导入pymysql模块。 第一步&#xff0c;mysql模块下载 方法1&#xff08;使用pip命令安装&#xff09;&#xff1a; 因…

MySQL数据库基础4-内置函数

文章目录 日期函数字符串函数数学函数其他函数 日期函数 函数名称描述current date()当前日期current time()当前时间current timestamp()当前时间戳date(datetime)返回datetime参数的日期部分date add(date, interval d_value type)在date中添加日期或时间&#xff0c;interv…

调用百度API自动生成春联

目录 1、作者介绍2、百度智能春联介绍录2.1 功能介绍2.2 技术特色 3、智能春联API接口介绍3.1 请求参数3.2 返回参数 4. 操作流程5. 代码实现 1、作者介绍 范宇帅&#xff0c;男&#xff0c;西安工程大学电子信息学院&#xff0c;2022级研究生 研究方向&#xff1a;多机器人协…

Windows安装Ubuntu双系统

Windows安装Ubuntu双系统 1.下载Ubuntu 16.04&#xff0c;地址https://releases.ubuntu.com/16.04/ 2.下载Rufus&#xff0c;地址https://rufus.ie/zh/ 3.准备U盘&#xff0c;烧录系统 4.磁盘分区 5.重启&#xff0c;按住shift键 本人电脑是联想小新 Windows11系统&#xff0…

Elasticsearch环境搭建(Windows)

一、介绍 布式、RESTful 风格的搜索和分析。 Elasticsearch 是位于 Elastic Stack 核心的分布式搜索和分析引擎。Logstash 和 Beats 有助于收集、聚合和丰富您的数据并将其存储在 Elasticsearch 中。Kibana 使您能够以交互方式探索、可视化和分享对数据的见解&#xff0c;并管…

Spring Cloud第二季--服务网关Gateway

文章目录 一、Gateway和Zuul的区别二、Gateway的核心概念三、小试牛刀3.1、代码测试3.2、关于Predicate3.3、关于Filter 一、Gateway和Zuul的区别 Spring Cloud Gateway是在Spring生态系统之上构建的API网关服务&#xff0c;基于Spring 5&#xff0c;Spring Boot 2和 Project …

【文献研究】轴辐式航线网络设计—Liner hub-and-spoke shipping network design

学习文献&#xff1a;轴辐式航线网络设计—Liner hub-and-spoke shipping network design 3. 模型建立 轴辐式航线网络设计 三级轴辐式网络&#xff1a;喂给港-二级枢纽港-一级枢纽港 主要考虑的限制条件&#xff1a;多种类型的集装箱船舶、转运时间、多种类型的集装箱 转运操…