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

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值