原理分析,Jetpack Compose 完全脫離 View 系統(tǒng)了嗎?

前言

Compose正式發(fā)布1.0已經(jīng)相當(dāng)一段時(shí)間了鳖枕,但相信很多同學(xué)對(duì)Compose還是有很多迷惑的地方 Compose跟原生的View到底是什么關(guān)系?是跟Flutter一樣完全基于Skia引擎渲染呼股,還是說(shuō)還是View的那老一套? 相信很多同學(xué)都會(huì)有下面的疑問(wèn)

下面我們就一起來(lái)看下下面這個(gè)問(wèn)題

現(xiàn)象分析

我們先看這樣一個(gè)簡(jiǎn)單布局

class TestActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        setContent {            
            ComposeBody()              
        }
    }
}

@Composable
fun ComposeBody() {
    Column {
        Text(text = "這是一行測(cè)試數(shù)據(jù)", color = Color.Black, style = MaterialTheme.typography.h6)
        Row() {
            Text(text = "測(cè)試數(shù)據(jù)1!", color = Color.Black, style = MaterialTheme.typography.h6)
            Text(text = "測(cè)試數(shù)據(jù)2!", color = Color.Black, style = MaterialTheme.typography.h6)
        }
    }
}

如上所示耕魄,就是一個(gè)簡(jiǎn)單的布局,包含Column,RowText 然后我們打開(kāi)開(kāi)發(fā)者選項(xiàng)中的顯示布局邊界彭谁,效果如下圖所示:

我們可以看到Compose的組件顯示了布局邊界,我們知道允扇,FlutterWebView H5內(nèi)的組件都是不會(huì)顯示布局邊界的缠局,難道Compose的布局渲染其實(shí)還是View的那一套?

我們下面再在onResume時(shí)嘗試遍歷一下View的層級(jí),看一下Compose到底會(huì)不會(huì)轉(zhuǎn)化成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) }
        }
    }

通過(guò)以上方式打印頁(yè)面的層級(jí),輸出結(jié)果如下:

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}

如上所示考润,我們寫(xiě)的Column,Row,Text并沒(méi)有出現(xiàn)在布局層級(jí)中狭园,跟Compose相關(guān)的只有ComposeViewAndroidComposeView兩個(gè)ViewComposeViewAndroidComposeView都是在setContent時(shí)添加進(jìn)去的Compose的容器,我們后面再分析糊治,這里先給出結(jié)論

Compose在渲染時(shí)并不會(huì)轉(zhuǎn)化成View唱矛,而是只有一個(gè)入口View,即AndroidComposeView我們聲明的Compose布局在渲染時(shí)會(huì)轉(zhuǎn)化成NodeTree,AndroidComposeView中會(huì)觸發(fā)NodeTree的布局與繪制 總得來(lái)說(shuō)井辜,Compose會(huì)有一個(gè)View的入口绎谦,但它的布局與渲染還是在LayoutNode上完成的,基本脫離了View

總得來(lái)說(shuō)粥脚,純Compose頁(yè)面的頁(yè)面層級(jí)如下圖所示:

原理分析

前置知識(shí)

我們知道窃肠,在View系統(tǒng)中會(huì)有一棵ViewTree,通過(guò)一個(gè)樹(shù)的數(shù)據(jù)結(jié)構(gòu)來(lái)描述整個(gè)UI界面 在Compose中,我們寫(xiě)的代碼在渲染時(shí)也會(huì)構(gòu)建成一個(gè)NodeTree,每一個(gè)組件就是一個(gè)ComposeNode,作為NodeTree上的一個(gè)節(jié)點(diǎn)

Compose 對(duì) NodeTree 管理涉及 Applier刷允、CompositionComposeNodeComposition 作為起點(diǎn)冤留,發(fā)起首次的 composition,通過(guò) Compose 的執(zhí)行树灶,填充 Slot Table纤怒,并基于 Table 創(chuàng)建 NodeTree。渲染引擎基于 Compose Nodes 渲染 UI天通, 每當(dāng) recomposition 發(fā)生時(shí)泊窘,都會(huì)通過(guò) Applier 對(duì) NodeTree 進(jìn)行更新。 因此

Compose 的執(zhí)行過(guò)程就是創(chuàng)建 Node 并構(gòu)建 NodeTree 的過(guò)程土砂。

為了了解NodeTree的構(gòu)建過(guò)程州既,我們來(lái)介紹下面幾個(gè)概念

Applier:增刪 NodeTree 的節(jié)點(diǎn)

簡(jiǎn)單來(lái)說(shuō),Applier的作用就是增刪NodeTree的節(jié)點(diǎn),每個(gè)NodeTree的運(yùn)算都需要配套一個(gè)Applier萝映。 同時(shí),Applier 會(huì)提供回調(diào)吴叶,基于回調(diào)我們可以對(duì) NodeTree 進(jìn)行自定義修改:

interface Applier<N> {

    val current: N // 當(dāng)前處理的節(jié)點(diǎn)

    fun onBeginChanges() {}

    fun onEndChanges() {}

    fun down(node: N)

    fun up()

    fun insertTopDown(index: Int, instance: N) // 添加節(jié)點(diǎn)(自頂向下)

    fun insertBottomUp(index: Int, instance: N)// 添加節(jié)點(diǎn)(自底向上)

    fun remove(index: Int, count: Int) //刪除節(jié)點(diǎn)
    
    fun move(from: Int, to: Int, count: Int) // 移動(dòng)節(jié)點(diǎn)

    fun clear() 
}

如上所示,節(jié)點(diǎn)增刪時(shí)會(huì)回調(diào)到Applier中序臂,我們可以在回調(diào)的方法中自定義節(jié)點(diǎn)添加或刪除時(shí)的邏輯蚌卤,后面我們可以一起看下在Android平臺(tái)Compose是怎樣處理的

Composition: Compose執(zhí)行的起點(diǎn)

Composition`是`Compose`執(zhí)行的起點(diǎn),我們來(lái)看下如何創(chuàng)建一個(gè)`Composition
val composition = Composition(
    applier = NodeApplier(node = Node()),
    parent = Recomposer(Dispatchers.Main)
)

composition.setContent {
    // Composable function calls
}

如上所示

  1. Composition中需要傳入兩個(gè)參數(shù)实束,ApplierRecomposer
  2. Applier上面已經(jīng)介紹過(guò)了,Recomposer非常重要逊彭,他負(fù)責(zé)Compose的重組咸灿,當(dāng)重組后,Recomposer 通過(guò)調(diào)用 Applier 完成 NodeTree 的變更
  3. Composition#setContent 為后續(xù) Compose 的調(diào)用提供了容器

通過(guò)上面的介紹侮叮,我們了解了NodeTree構(gòu)建的基本流程避矢,下面我們一起來(lái)分析下setContent的源碼

setContent過(guò)程分析

setContent入口

setContent的源碼其實(shí)比較簡(jiǎn)單,我們一起來(lái)看下:

public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    //判斷ComposeView是否存在囊榜,如果存在則不創(chuàng)建
    if (existingComposeView != null) with(existingComposeView) {
        setContent(content)
    } else ComposeView(this).apply {
        //將Compose content添加到ComposeView上
        setContent(content)
        // 將ComposeView添加到DecorView上
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

上面就是setContent的入口审胸,主要作用就是創(chuàng)建了一個(gè)ComposeView并添加到DecorView

Composition的創(chuàng)建

下面我們來(lái)看下AndroidComposeViewComposition是怎樣創(chuàng)建的 通過(guò)ComposeView#setContent->AbstractComposeView#createComposition->AbstractComposeView#ensureCompositionCreated->ViewGroup#setContent 最后會(huì)調(diào)用到doSetContent方法,這里就是Compose的入口:Composition創(chuàng)建的地方

private fun doSetContent(
    owner: AndroidComposeView, //AndroidComposeView是owner
    parent: CompositionContext,
    content: @Composable () -> Unit
): Composition {
    //..
    //創(chuàng)建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內(nèi)容添加到Composition中   
    wrapped.setContent(content)
    return wrapped
}

如上所示卸勺,主要就是創(chuàng)建一個(gè)Composition并傳入UIApplierRecomposer,并將Compose content傳入Composition

UiApplier的實(shí)現(xiàn)

上面已經(jīng)創(chuàng)建了Composition并傳入了UIApplier砂沛,后續(xù)添加了Node都會(huì)回調(diào)到UIApplier

internal class UiApplier(
    root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
    //...
    
    override fun insertBottomUp(index: Int, instance: LayoutNode) {
        current.insertAt(index, instance)
    }

    //...
}

如上所示,在插入節(jié)點(diǎn)時(shí)曙求,會(huì)調(diào)用current.insertAt方法碍庵,那么這個(gè)current到底是什么呢?

private fun doSetContent(
    owner: AndroidComposeView, //AndroidComposeView是owner
): Composition {
    //UiApplier傳入的參數(shù)即為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中傳入的參數(shù)其實(shí)就是AndroidComposeViewroot静浴,即current就是AndroidComposeViewroot

    # AndroidComposeView
    override val root = LayoutNode().also {
        it.measurePolicy = RootMeasurePolicy
        //...
    }

如上所示,root其實(shí)就是一個(gè)LayoutNode,通過(guò)上面我們知道芽淡,所有的節(jié)點(diǎn)都會(huì)通過(guò)Applier插入到root

布局與繪制入口

上面我們已經(jīng)在AndroidComposeView中拿到NodeTree的根結(jié)點(diǎn)了马绝,那Compose的布局與測(cè)量到底是怎么觸發(fā)的呢?

    # AndroidComposeView
    override fun dispatchDraw(canvas: android.graphics.Canvas) {
        //Compose測(cè)量與布局入口
        measureAndLayout()
        
        //Compose繪制入口
        canvasHolder.drawInto(canvas) { root.draw(this) }
        //...
    }

    override fun measureAndLayout() {
        val rootNodeResized = measureAndLayoutDelegate.measureAndLayout()
        measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
    }

如上所示,AndroidComposeView會(huì)通過(guò)root,向下遍歷它的子節(jié)點(diǎn)進(jìn)行測(cè)量布局與繪制挣菲,這里就是LayoutNode繪制的入口

小結(jié)

  1. Compose在構(gòu)建NodeTree的過(guò)程中主要通過(guò)Composition,Applier,Recomposer構(gòu)建,Applier會(huì)將所有節(jié)點(diǎn)添加到AndroidComposeView中的root節(jié)點(diǎn)下
  2. setContent的過(guò)程中富稻,會(huì)創(chuàng)建ComposeViewAndroidComposeView,其中AndroidComposeViewCompose的入口
  3. AndroidComposeViewdispatchDraw中會(huì)通過(guò)root向下遍歷子節(jié)點(diǎn)進(jìn)行測(cè)量布局與繪制,這里是LayoutNode繪制的入口
  4. Android平臺(tái)上白胀,Compose的布局與繪制已基本脫離View體系椭赋,但仍然依賴于Canvas

Compose與跨平臺(tái)

上面說(shuō)到,Compose的繪制仍然依賴于Canvas或杠,但既然這樣哪怔,Compose是怎么做到跨平臺(tái)的呢? 這主要是通過(guò)良好的分層設(shè)計(jì)

Compose 在代碼上自下而上依次分為6層:

其中compose.runtimecompose.compiler最為核心,它們是支撐聲明式UI的基礎(chǔ)向抢。

而我們上面分析的AndroidComposeView這一部分认境,屬于compose.ui部分,它主要負(fù)責(zé)Android設(shè)備相關(guān)的基礎(chǔ)UI能力挟鸠,例如 layout叉信、measuredrawing艘希、input 等 但這一部分是可以被替換的硼身,compose.runtime 提供了 NodeTree 管理等基礎(chǔ)能力硅急,此部分與平臺(tái)無(wú)關(guān),在此基礎(chǔ)上各平臺(tái)只需實(shí)現(xiàn)UI的渲染就是一套完整的聲明式UI框架

Button的特殊情況

上面我們介紹了在純Compose項(xiàng)目下佳遂,AndroidComposeView不會(huì)有子View,而是遍歷LayoutnNode來(lái)布局測(cè)量繪制 但如果我們?cè)诖a中加入一個(gè)Button营袜,結(jié)果可能就不太一樣了

@Composable
fun ComposeBody() {
    Column {
        Text(text = "這是一行測(cè)試數(shù)據(jù)", color = Color.Black, style = MaterialTheme.typography.h6)
        Row() {
            Text(text = "測(cè)試數(shù)據(jù)1!", color = Color.Black, style = MaterialTheme.typography.h6)
            Text(text = "測(cè)試數(shù)據(jù)2!", color = Color.Black, style = MaterialTheme.typography.h6)
        }

        Button(onClick = {}) {
            Text(text = "這是一個(gè)Button",color = Color.White)
        }
    }
}

然后我們?cè)倏纯错?yè)面的層級(jí)結(jié)構(gòu)

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荚板,這是為什么呢?

我們一起來(lái)看下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.

意思也很簡(jiǎn)單,Compose目前還不能直接繪制水波紋效果吩屹,因此需要將水波紋效果設(shè)置為View的背景啸驯,這里利用View做了一個(gè)中轉(zhuǎn) 然后RippleHostViewRippleContainer自然會(huì)添加到AndroidComposeView中,如果我們?cè)?code>Compose中使用了AndroidView祟峦,效果也是一樣的 但是這種情況并沒(méi)有違背我們上面說(shuō)的,純Compose項(xiàng)目下徙鱼,AndroidComposeView下沒(méi)有子View,因?yàn)?code>Button并不是純Compose

總結(jié)

本文主要分析回答了Compose到底有沒(méi)有完全脫離View系統(tǒng)這個(gè)問(wèn)題,總結(jié)如下:

  1. Compose在渲染時(shí)并不會(huì)轉(zhuǎn)化成View宅楞,而是只有一個(gè)入口View,即AndroidComposeView,純Compose項(xiàng)目下袱吆,AndroidComposeView沒(méi)有子View
  2. 我們聲明的Compose布局在渲染時(shí)會(huì)轉(zhuǎn)化成NodeTree,AndroidComposeView中會(huì)觸發(fā)NodeTree的布局與繪制,AndroidComposeView#dispatchDraw是繪制的入口
  3. Android平臺(tái)上厌衙,Compose的布局與繪制已基本脫離View體系,但仍然依賴于Canvas
  4. 由于良好的分層體系绞绒,Compose可通過(guò) compose.runtimecompose.compiler實(shí)現(xiàn)跨平臺(tái)
  5. 在使用Button時(shí)婶希,AndroidComposeView會(huì)有兩層子View,這是因?yàn)?code>Button中使用了View來(lái)實(shí)現(xiàn)水波紋效果

作者:程序員江同學(xué)
轉(zhuǎn)載來(lái)源于:https://juejin.cn/post/7017811394036760612
如有侵權(quán)蓬衡,請(qǐng)聯(lián)系刪除喻杈!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市狰晚,隨后出現(xiàn)的幾起案子筒饰,更是在濱河造成了極大的恐慌,老刑警劉巖壁晒,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瓷们,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡秒咐,警方通過(guò)查閱死者的電腦和手機(jī)谬晕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)携取,“玉大人攒钳,你說(shuō)我怎么就攤上這事〈醪瑁” “怎么了夕玩?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵你弦,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我燎孟,道長(zhǎng)禽作,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任揩页,我火速辦了婚禮旷偿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘爆侣。我一直安慰自己萍程,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布兔仰。 她就那樣靜靜地躺著,像睡著了一般乎赴。 火紅的嫁衣襯著肌膚如雪忍法。 梳的紋絲不亂的頭發(fā)上饿序,一...
    開(kāi)封第一講書(shū)人閱讀 52,337評(píng)論 1 310
  • 那天离唬,我揣著相機(jī)與錄音嫂用,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鸟缕,可吹牛的內(nèi)容都是我干的排抬。 我是一名探鬼主播叁扫,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼畜埋!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起畴蒲,我...
    開(kāi)封第一講書(shū)人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤悠鞍,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后模燥,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體咖祭,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年蔫骂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了么翰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辽旋,死狀恐怖浩嫌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情补胚,我是刑警寧澤码耐,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站溶其,受9級(jí)特大地震影響骚腥,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瓶逃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一束铭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧契沫,春花似錦带猴、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至牙甫,卻和暖如春窟哺,著一層夾襖步出監(jiān)牢的瞬間且轨,已是汗流浹背至朗。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工粤蝎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留祸挪,地道東北人峻仇。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像熔吗,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咐容,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容