前言
Compose正式發布1.0已經相當一段時間了,但相信很多同學對Compose還是有很多迷惑的地方
Compose跟原生的View到底是什么關系?是跟Flutter一樣完全基于Skia引擎渲染,還是說還是View的那老一套?
相信很多同學都會有下面的疑問
![[圖片上傳失敗...(image-211dc7-1634021553449)]](https://img.uj5u.com/2021/10/13/273679130834031.png)
下面我們就一起來看下下面這個問題
現象分析
我們先看這樣一個簡單布局
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
然后我們打開開發者選項中的顯示布局邊界,效果如下圖所示:
![[圖片上傳失敗...(image-9a3cb4-1634021553449)]](https://img.uj5u.com/2021/10/13/273679130834032.png)
我們可以看到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層:DecorView@c2f703f[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頁面的頁面層級如下圖所示:
![[圖片上傳失敗...(image-718043-1634021553449)]](https://img.uj5u.com/2021/10/13/273679130834033.png)
原理分析
前置知識
我們知道,在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的節點,每個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
val composition = Composition(
applier = NodeApplier(node = Node()),
parent = Recomposer(Dispatchers.Main)
)
composition.setContent {
// Composable function calls
}
如上所示
Composition中需要傳入兩個引數,Applier與RecomposerApplier上面已經介紹過了,Recomposer非常重要,他負責Compose的重組,當重組后,Recomposer通過呼叫Applier完成NodeTree的變更Composition#setContent為后續Compose的呼叫提供了容器
通過上面的介紹,我們了解了NodeTree構建的基本流程,下面我們一起來分析下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的創建
下面我們來看下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的實作
上面已經創建了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的繪制仍然依賴于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的特殊情況
上面我們介紹了在純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層:DecorView@182e858[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來實作水波紋效果
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/310604.html
標籤:其他
上一篇:Android隱私彈框
