Skip to main content

Compose 三階段渲染管線

核心三階段 (The Three Phases)

Compose 將 UI 的生成與更新拆解為三個依序執行的獨立階段,以實現最大化的工作重用與性能優化。

  1. 組合 (Composition): 此階段的核心任務是執行 @Composable 函式,並產出一個描述 UI 結構的 UiNode 樹。它定義了「顯示什麼」,但尚未涉及尺寸或位置。當 State 變更時,Compose 會智能地僅「重組」受影響的節點,生成更新後的 UI 描述。
  2. 版面配置 (Layout): 此階段負責確定每個 UI 元素在螢幕上的尺寸與位置。它包含兩個子步驟:
    • 測量 (Measure): 父元件向子元件傳遞約束 (Constraints),子元件回報其所需尺寸 (Size)。這是一個遞迴的、由上至下的過程。
    • 放置 (Place): 父元件根據其佈局邏輯,將測量完成的子元件放置在特定的 (x, y) 座標。
  3. 繪製 (Drawing): 這是管線的最後一步。系統會遍歷佈局完成的 UI 樹,將其轉換為實際的 GPU 繪圖指令,最終在螢幕上渲染成像素。

由於階段分離,Compose 得以實現高效優化。例如,若元件僅位置變更,則可跳過組合階段,僅重跑佈局與繪製,從而節省運算資源。

Intrinsic Measurements

標準的單趟 (single-pass) 測量模型極為高效,但無法處理一種特定情況:當父元件的尺寸依賴於子元件的內容尺寸時。這便產生了一個佈局悖論——父元件無法在不了解子元件尺寸的情況下,為其提供準確的約束。

為此,Compose 引入了固有特性測量 (Intrinsic Measurements)。這是在正式測量前的一個「預先查詢」步驟。

  1. 查詢傳遞 (Query Pass): 父元件查詢子元件的「固有」尺寸 (min/maxIntrinsicWidth/Height),即其內容所需的理想大小。
  2. 最終測量 (Final Pass): 父元件根據查詢結果決定自身尺寸後,再執行標準的測量與放置流程。

wrapContentSize 等修飾符的背後,正是利用此機制。然而,這種兩趟式 (two-pass) 測量必然比單趟測量成本更高,因此框架僅在必要時才啟用它。


你這麼一說,還真是,一語驚醒夢中人。

搞了半天,Compose 裡這套聽起來很潮的 Intrinsic Measurements,骨子裡不就是為了解決我們當年那個老問題嗎?

想當年,我們為了讓自定義 View 能完美處理 wrap_content,得在 onMeasure 裡跟 MeasureSpec 大戰三百回合。用位元運算去解析 AT_MOST、EXACTLY,寫一堆 if-else,小心翼翼地呼叫 setMeasuredDimension(),深怕一個不小心就觸發了 requestLayout 的無限循環。那一套手動測量的流程,簡直是每個資深 Android 開發者的必經之路。

結果現在 Compose 倒好,它直接把這套流程抽象化了。它跟你說:「嘿,老兄,別寫那麼多指令了。我現在就問你一個問題,『你理想的寬度是多少?』,你給我個數字 (Int) 就行。」

這… 說穿了不就是把 onMeasure 的雙重測量給自動化、對話化了嗎?

問題還是那個問題:父容器需要先知道子元件的大小,才能決定自己。 解法也還是那個解法:需要兩趟測量。

差別在於,以前我們是那個親自下場、滿頭大汗的施工隊長;現在我們成了只提供一個數據的顧問。

只能感嘆,時代真的變了。以前我們是自己動手、指令分明,現在是跟框架「聊天」、回應提問。問題的本質沒變,但解法,確實優雅多了。


這套機制沒什麼黑魔法,靠的就是我們最熟悉的介面 (Interface)

Modifier.layout 裡的那個 measurable 物件,它其實同時實現了兩個介面,等於能扮演兩種角色:

  1. Measurable (施工隊長): 它的 measure() 方法是最終命令。父佈局用它來下達「你必須是這個尺寸」的最終指示,走的是正規、單向的測量流程。
  2. IntrinsicMeasurable (顧問): 它的 min/maxIntrinsic… 等方法則用來回應「諮詢」。讓父佈局能預先探聽「你最希望自己多大?」的尺寸偏好。

所以,Compose 只是用了一套乾淨俐落的介面設計,把我們以前在 onMeasure 裡用 MeasureSpec 做的髒活,巧妙地抽象成兩種對話模式。父佈局看情況,決定要「下命令」還是「問建議」。