浅析 JetPack Compose 是如何安装到View视图上
Hi , : )
看完本文可以帮你解开什么问题?
- 为什么
Compose
无需在意view
层级问题,怎样嵌套都行? (最简单10s就能明白); Compose
如何安装到传统View
视图上;
门外汉-从布局窥一眼
这是一段 Compose
的简单代码,我们演示了多层嵌套下的示例:
如果按照传统 View
的思维,我们不难发现,当前 content(R.id.content(FrameLayout)->) 布局中存在5层嵌套,这是极不可取的一种做法。
但是现在是 Compose
,最终的绘制真的会有5层吗?
我们打开 Filpper 看一下:
显然 R.id.content 下只有一个 ComposeView
,然后内部包含了一个 AndroidComposeView
,我们上述中的 Box
最终都被解析并安装到了这个自定义view上。
所以我们简单点可以总结为:
JetPack-Compose 其自定义了一个 基础容器- ComposeView
,以及其他扩展View,比如 AndroidComposeView
,并对其进行封装,对外提供了各种我们在上层所使用的各种组件或者容器。
所以当我们在 Compose
中 setContent
后,其初始化了一个 ComposeView
,并且添加了一个 AndroidComposeView
,其承载了我们代码中所写的全部组件,并进行解析,最终绘制在了传统UI中。
所以为什么说Compose不在意布局层级呢?
因为人家只有两层啊,即业务代码中,
ComposeView
下就只有一个AndroidComposeView
,而其他Image
,Box
等组件都是人家自己绘制的。你说相比 传统View 还会存在层级问题吗?
一些猜测:
为什么叫 AndroidComposeView
呢?
Compose
现在不仅仅支持Android
,现在预览版也支持Desktop
,所以很可能ComposeView
很可能还会涉及其他平台系统。所以最顶层是ComposeView
,而对于Android
的支持为AndroidComposeView
。
当然上述只是我一个猜测,如果大佬们有其他想法,也欢迎分享。
解析-setContent内部实现
我们在上面知道了 Compose
最终在 Android View
的展现形式,那么它到底是怎样设置上去的呢,接下来我们就简单解析一下,不涉及Compose
相关过多源码,比较好理解:
ComponentActivity
setContent
- public fun ComponentActivity.setContent(
- parent: CompositionContext? = null,
- content: @Composable () -> Unit
- ) {
-
- //获取decorView->R.id.content->内第一个view,在compose中,默认为composeView
- val existingComposeView = window.decorView
- .findViewById<ViewGroup>(android.R.id.content)
- .getChildAt(0) as? ComposeView
-
- ? -> 如果view不为null,则证明已经安装过,则重新设置内容
- if (existingComposeView != null) with(existingComposeView) {
- //设置父Compose内容
- setParentCompositionContext(parent)
- //设置当前页面内容
- setContent(content)
-
- ? -> 如果上面获取的view是空,也就是现在还没有安装,则生成一个新的去安装到 R.id.content 上
- } else ComposeView(this).apply {
- //设置父Compose内容
- setParentCompositionContext(parent)
- //设置当前页面内容
- setContent(content)
- //设置TreeLifecycleOwner,实际上是修复Appcompat1.3及以下的bug,忽视即可
- setOwners()
- //设置内容视图,activity的方法
- setContentView(this, DefaultActivityContentLayoutParams)
- }
- }
从上面的源码,我们的下一步主要关注点为:
- setParentCompositionContext()
- setContent()
ComposeView
setParentCompositionContext
设置视图合成的父级 context
,里面仅仅是一个赋值,暂时跳过
- fun setParentCompositionContext(parent: CompositionContext?) {
- parentContext = parent
- }
setContent
设置 compose UI
内容,当视图被添加到窗口时调用。
- fun setContent(content: @Composable () -> Unit) {
- shouldCreateCompositionOnAttachedToWindow = true
- this.content.value = content
- //如果已经挂接到窗口,即view完全显示时,isAttachedToWindow=true
- //第一次调用时,这里为false,所以当onAttachToWindows调用时,下面的方法才会被调用
- if (isAttachedToWindow) {
- createComposition()
- }
- }
我们接下来去看看 AbstractComposeView
的 onAttachToWindows()
方法。
AbstractComposeView
onAttachToWindows
当被安装到windows上时调用。
- override fun onAttachedToWindow() {
- super.onAttachedToWindow()
- previousAttachedWindowToken = windowToken
- //这里当onAttached时为true,因为上一步已经赋值为true
- if (shouldCreateCompositionOnAttachedToWindow) {
- ensureCompositionCreated()
- }
- }
ensureCompositionCreated
- private fun ensureCompositionCreated() {
- //整个流程中,我们没看见composition的初始化,所以这里为null
- if (composition == null) {
- try {
- ? 1. ///结合步骤1来看,就是一个用于判断是否add过的变量
- creatingComposition = true
- ? 2. // 先看 resolveParentCompositionContext() -> 得到一个顶级CompositionContext
-
- ? 3. // 拿着parentContext,传入setContent方法
- composition = setContent(resolveParentCompositionContext()) {
- Content()
- }
- } finally {
- creatingComposition = false
- }
- }
- }
resolveParentxxxContext
其作用是解析父组合上下文。
- private fun resolveParentCompositionContext() = parentContext
- //?1.
- ?: findViewTreeCompositionContext()?.also { cachedViewTreeCompositionContext = it }
- ?: cachedViewTreeCompositionContext
- // ?2.
- ?: windowRecomposer.also { cachedViewTreeCompositionContext = it }
1. findViewTreexxxContext
查找当前view树的父context
- fun View.findViewTreeCompositionContext(): CompositionContext? {
- //先取自己的compositionContenxt,如果取到直接返回,否则不断向上找父级的context.
- var found: CompositionContext? = compositionContext
- if (found != null) return found
- var parent: ViewParent? = parent
- while (found == null && parent is View) {
- found = parent.compositionContext
- parent = parent.getParent()
- }
- return found
- }
View.compositionContext
compositionContext
是一个扩展函数,内部使用tag保存当前context
var View.compositionContext: CompositionContext?
get() = getTag(R.id.androidx_compose_ui_view_composition_context) as? CompositionContext
set(value) {
setTag(R.id.androidx_compose_ui_view_composition_context, value)
}
2. windowRecomposer
我们接着上面的流程继续分析:
- @OptIn(InternalComposeUiApi::class)
- internal val View.windowRecomposer: Recomposer
- get() {
- ...
- //这个rootView就是R.id.content对应的FrameLayout的第一个子view,即ComposeView
- val rootView = contentChild
- // 因为上述是初始化状态,所以 rootParentRef 第一次也为null,所以我们继续往下看
- return when (val rootParentRef = rootView.compositionContext) {
- //调用窗口Recomposer创建一个新的,并且把根content传入进去
- null -> WindowRecomposerPolicy.createAndInstallWindowRecomposer(rootView)
- ...
- }
- }
View.contentChild
- private val View.contentChild: View
- get() {
- var self: View = this
- var parent: ViewParent? = self.parent
- while (parent is View) {
- if (parent.id == android.R.id.content) return self
- self = parent
- parent = self.parent
- }
- return self
- }
createAndxxxRecomposer(rootView)
创建一个 Recomposer
,并且将其赋值给rootView的扩展变量 compositionContext
- internal fun createAndInstallWindowRecomposer(rootView: View): Recomposer {
- //使用默认的工厂创建一个Recomposer
- val newRecomposer = factory.get().createRecomposer(rootView)
- //将其赋值给rootView的compositionContext,而compositionContext也是一个扩展函数,通用使用tag保存
- rootView.compositionContext = newRecomposer
- ...
- }
-
- -- View.compositionContext
- var View.compositionContext: CompositionContext?
- get() = getTag(R.id.androidx_compose_ui_view_composition_context) as? CompositionContext
- set(value) {
- setTag(R.id.androidx_compose_ui_view_composition_context, value)
- }
在这里,我们知道了resolveParentCompositionContext()
实际上是初始化了 ComposeView
的扩展变量 compositionContext
。并且我们得到了这个返回值 parentContext
。
ViewGroup.setContent
接着我们继续回到setContent中,去看看:
?3.
setContent(resolveParentCompositionContext())
- internal fun ViewGroup.setContent(
- parent: CompositionContext,
- content: @Composable () -> Unit
- ): Composition {
- GlobalSnapshotManager.ensureStarted()
- val composeView =
- if (childCount > 0) {
- //第一次调用这里肯定为null,注意下面方法调用
- getChildAt(0) as? AndroidComposeView
- } else {
- removeAllViews(); null
- //初始化一个AndroidComposeView,并将我们最外面的content函数传入,
- //将初始化的AndroidComposeView添加到ComposeView上
- } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
- //这里留一个伏笔,即我们的下个阶段将
- return doSetContent(composeView, parent, content)
- }
如上所述,这里拿到当前 viewGroup
即 ComposeView
,然后判断 childCount
否有子view,因为是第一次,所以肯定执行到了 flase
.
即执行到了 AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
,也就是生成了一个AndroidComposeView
的 ViewGroup
容器,内部构造函数中的context
正是我们第三步的 content() ,也就是我们自己的业务代码。然后调用 ComposeView
的 addView()
方法,将自己添加到 ComposeView
中。
到这里为止,如果你还记得我们最开始的布局层级,那就应该能明白最基础的流程。
总结
- 当我们调用
Compsoe
的setContent()
之后,其内部先判断当前的基础 (R.id.content) 的View
是不是ComposeView
,如果不是则初始化一个,并且调用其的setContent()
方法,将shouldCreateCompositionOnAttachedToWindow
置为true
,并且将最开始传入的content
函数赋值给Compose
内部的变量content
。 - 接着使用
Activity
的setContentView()
,将初始化的ComposeView
添加到底层布局 R.id.content 上; - 在
view
完全可见时,即onAttachView
被调用时,开始去初始化当前compose
页面,其内部初始化了一个名为AndroidComposeView
的子View。然后调用我们传入的content()
函数,生成一个content
,将其作为构造函数传入AndroidComposeView
中,从而生成了子view。然后将其 add 到了ComposeView
上。从而完成了布局的初始化。
碎碎念
本文是理解 Compose
设计中比较简单的一篇,适合初学的同学简单了解 Compose与View 的相爱相杀。后续我将继续深追 Compose
的部分源码设计以及在实际落地中的场景解决方案。