前言
Compose
正式釋出1.0已經相當一段時間了,但相信很多同學對
Compose
還是有很多迷惑的地方
Compose
跟原生的
View
到底是什麼關系?是跟
Flutter
一樣完全基于
Skia
引擎渲染,還是說還是
View
的那老一套?
相信很多同學都會有下面的疑問

下面我們就一起來看下下面這個問題
現象分析
我們先看這樣一個簡單布局
class TestActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
ComposeBody()
}
}
}
@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)
}
}
}
如上所示,就是一個簡單的布局,包含
Column
,
Row
與
Text
然後我們打開開發者選項中的
顯示布局邊界
,效果如下圖所示:
我們可以看到
Compose
的元件顯示了布局邊界,我們知道,
Flutter
與
WebView H5
内的元件都是不會顯示布局邊界的,難道
Compose
的布局渲染其實還是
View
的那一套?
我們下面再在
onResume
時嘗試周遊一下
View
的層級,看一下
Compose
到底會不會轉化成
View
override fun onResume() {
super.onResume()
window.decorView.postDelayed({
(window.decorView as? ViewGroup)?.let { transverse(it, 1) }
}, 2000)
}
private fun transverse(view: View, index: Int) {
Log.e("debug", "第${index}層:" + view)
if (view is ViewGroup) {
view.children.forEach { transverse(it, index + 1) }
}
}
通過以上方式列印頁面的層級,輸出結果如下:
E/debug: 第1層:[email protected][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
的節點
Applier
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
Compose
Composition
是
Compose
執行的起點,我們來看下如何建立一個
Composition
val composition = Composition(
applier = NodeApplier(node = Node()),
parent = Recomposer(Dispatchers.Main)
)
composition.setContent {
// Composable function calls
}
如上所示
-
中需要傳入兩個參數,Composition
與Applier
Recomposer
-
上面已經介紹過了,Applier
非常重要,他負責Recomposer
的重組,當重組後,Compose
通過調用Recomposer
完成Applier
的變更NodeTree
-
為後續Composition#setContent
的調用提供了容器Compose
通過上面的介紹,我們了解了
NodeTree
建構的基本流程,下面我們一起來分析下
setContent
的源碼
setContent
過程分析
setContent
setContent
入口
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
上
Composition
的建立
Composition
下面我們來看下
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
中
UiApplier
的實作
UiApplier
上面已經建立了
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
繪制的入口
小結
-
在建構Compose
的過程中主要通過NodeTree
,Composition
,Applier
建構,Recomposer
會将所有節點添加到Applier
中的AndroidComposeView
節點下root
- 在
的過程中,會建立setContent
與ComposeView
,其中AndroidComposeView
是AndroidComposeView
的入口Compose
-
在AndroidComposeView
中會通過dispatchDraw
向下周遊子節點進行測量布局與繪制,這裡是root
繪制的入口LayoutNode
- 在
平台上,Android
的布局與繪制已基本脫離Compose
體系,但仍然依賴于View
Canvas
Compose
與跨平台
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 的基礎
Button
的特殊情況
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層:[email protected][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
的注釋
Empty View that hosts a RippleDrawable as its background. This is needed as RippleDrawables cannot currently be drawn directly to a android.graphics.RenderNode (b/184760109), so instead we rely on View’s internal implementation to draw to the background android.graphics.RenderNode. A RippleContainer is used to manage and assign RippleHostViews when needed - see RippleContainer.getRippleHostView.
意思也很簡單,
Compose
目前還不能直接繪制水波紋效果,是以需要将水波紋效果設定為
View
的背景,這裡利用
View
做了一個中轉
然後
RippleHostView
與
RippleContainer
自然會添加到
AndroidComposeView
中,如果我們在
Compose
中使用了
AndroidView
,效果也是一樣的
但是這種情況并沒有違背我們上面說的,純
Compose
項目下,
AndroidComposeView
下沒有子
View
,因為
Button
并不是純
Compose
的
總結
本文主要分析回答了
Compose
到底有沒有完全脫離
View
系統這個問題,總結如下:
-
在渲染時并不會轉化成Compose
,而是隻有一個入口View
,即View
,純AndroidComposeView
項目下,Compose
沒有子AndroidComposeView
View
- 我們聲明的
布局在渲染時會轉化成Compose
,NodeTree
中會觸發AndroidComposeView
的布局與繪制,NodeTree
是繪制的入口AndroidComposeView#dispatchDraw
- 在
平台上,Android
的布局與繪制已基本脫離Compose
體系,但仍然依賴于View
Canvas
- 由于良好的分層體系,
可通過Compose
和compose.runtime
實作跨平台compose.compiler
- 在使用
時,Button
會有兩層子AndroidComposeView
,這是因為View
中使用了Button
來實作水波紋效果View