跳至主要内容

SurfaceFlinger:Android 畫面是怎麼被「合成」出來的

前言

在 Android 上做系統整合,遲早會遇到這類 bug report:

  • 「開分割畫面之後耗電變高」
  • 「滑動列表偶爾掉幀,但 App 端 profiler 看起來很正常」
  • 「某個浮動視窗疊上去之後,整台機器溫度就上來了」

這三個問題的共同答案,往往都指向同一個 native process:SurfaceFlinger

它是 Android 圖形堆疊裡最容易被忽略、但對功耗與流暢度影響最大的一層。App 端的 profiler 看不到它,因為它根本不在 App 的 process 裡;而它做的決策(要用硬體還是 GPU 合成)幾乎不會出現在任何 UI 上,只會反映在電流與 frame time 上。

這篇文章整理 SurfaceFlinger 的角色、資料流、合成路徑的分歧點,以及實務上可用的除錯手段。


一、SurfaceFlinger 是什麼

SurfaceFlinger(以下簡稱 SF)是 Android 的畫面合成服務,以 native process 形式存在:

/system/bin/surfaceflinger

由 init 在開機早期啟動,屬於 Android 開機流程中相當前面的一環——沒有它,畫面出不來。

它的職責可以用一句話概括:

把系統中所有可見的 layer,合成成一張最終畫面,並在正確的時間點送到 display。

這裡的 layer 不只是「App 的視窗」。一個典型的畫面至少包含:

Layer來源
App 主視窗前景 Activity
Status barSystemUI
Navigation barSystemUI
輸入法視窗IME
Dim / scrim layer對話框背後的遮罩
Rounded corner overlay螢幕圓角遮罩
Cursor / pointer外接滑鼠、觸控筆

SF 要做的,就是決定這些 layer 的 Z-order、裁切區域、透明度混合方式,然後選擇一條最省成本的路徑把它們疊起來。

重要的觀念切分:

  • 繪製(rendering) 是 App 自己的事,發生在 App process 裡(Skia / HWUI / Vulkan / OpenGL)。
  • 合成(composition) 是 SF 的事。

App 畫得再快,如果合成端塞住,畫面照樣掉幀。反之也成立。這兩件事分開看,是後續除錯的基礎。


二、資料流:Buffer 是怎麼傳過去的

2.1 傳統 BufferQueue 模型

Android 圖形堆疊的核心是 producer–consumer 模型,中介是 BufferQueue

App process SurfaceFlinger process
┌──────────────────┐ ┌──────────────────────┐
│ Canvas / GL │ │ │
│ ↓ │ │ │
│ Surface │ BufferQueue │ Layer (consumer) │
│ (producer) │ ─────────────► │ ↓ │
│ │ dequeue/queue │ Composition │
└──────────────────┘ └──────────────────────┘

流程大致是:

  1. App 從 BufferQueue dequeueBuffer() 拿到一塊可寫入的 graphic buffer
  2. App 把畫面內容繪製進去(GPU 或 CPU)
  3. App queueBuffer() 把 buffer 交回佇列
  4. SF 在下一個合成週期 acquireBuffer() 取用
  5. 合成完成、buffer 不再被需要後 releaseBuffer() 歸還給 App 重複使用

Buffer 本身是 GraphicBuffer(底層是 gralloc 配置的共享記憶體),跨 process 傳遞的只有 handle,不是像素資料。這是整個機制能夠低成本的關鍵——沒有任何一次 memcpy。

通常會配置 2~3 塊 buffer(double / triple buffering)。Triple buffering 能吸收 App 偶發的 frame time 抖動,代價是多一幀的延遲。

2.2 BLAST:Android 12 之後的變化

Android 12 引入 BLAST(Buffer Layer At SurfaceFlinger Transaction),把 BufferQueue 的實作搬到 client 端,buffer 改以 transaction 形式提交給 SF。

動機是減少 App 與 SF 之間的 binder 往返次數與同步點——舊模型下 SF 是 consumer,dequeue/queue 都可能牽涉跨 process 呼叫;BLAST 讓 buffer 提交與其他 layer 屬性變更(位置、大小、透明度)能在同一個 atomic transaction 裡一起送出。

實務上的意義:以前「視窗 resize 時 buffer 尺寸與 layer 尺寸不同步」造成的一幀閃爍或黑邊,在 BLAST 之後結構性地變好了。這對平板的分割畫面拖曳、自由視窗調整尺寸特別有感。


三、節奏:VSYNC 與 Choreographer

合成不能隨時做,必須對齊 display 的掃描週期,否則會 tearing。

3.1 硬體 VSYNC 與軟體模型

Display 硬體會週期性產生 VSYNC 訊號。但 SF 不會直接把每個硬體 VSYNC 都轉發出去——那樣太耗電,而且時間點也不理想。

SF 內部維護一個 VSYNC 的預測模型(早期叫 DispSync,後來演化為 VSyncPredictor / VsyncTracker):用取樣到的硬體 VSYNC 時間戳擬合出週期與相位,然後生成兩條虛擬時間軸:

  • VSYNC-app:喚醒 App 開始繪製下一幀
  • VSYNC-sf:喚醒 SF 開始合成

兩者之間刻意錯開一個 phase offset。這個 offset 是功耗與延遲之間的權衡參數:

  • offset 太小 → App 還沒畫完 SF 就要合成,容易掉幀
  • offset 太大 → 每一幀的 end-to-end latency 增加,觸控會有拖沓感

在支援可變刷新率(VRR)的裝置上還會多一層 Scheduler 決定當下該用哪個 refresh rate,並在切換時處理 offset 重算。

3.2 App 端的 Choreographer

App 這邊對應的入口是 ChoreographerView.invalidate() 不會立刻繪製,而是註冊一個 callback,等 VSYNC-app 到來時才真正走 measure / layout / draw。

所以 UI thread 的一幀時間預算是一個 refresh interval:60Hz 是 16.6ms,90Hz 是 11.1ms,120Hz 只有 8.3ms。高刷新率把預算壓縮得很緊,這是高刷平板上 jank 特別容易被看見的原因。


四、關鍵分歧點:Device Composition vs Client Composition

這是整篇文章最重要的一節。SF 有兩條合成路徑,成本差距很大。

4.1 Hardware Composer HAL

SF 透過 HWC(Hardware Composer HAL,新版本走 AIDL composer 介面)與顯示硬體溝通。流程的核心是兩個呼叫:

  1. validateDisplay() — SF 把這一幀所有 layer 的描述(位置、格式、blending mode、transform)交給 HWC,問:「這些你能自己處理嗎?」
  2. HWC 回覆每個 layer 的 composition type
  3. presentDisplay() — 真正送出

常見的 composition type:

Type意義
DEVICE由顯示硬體的 overlay engine 直接處理,SF 不碰
CLIENTHWC 做不到,退回讓 SF 用 GPU 畫進一張 framebuffer target
SOLID_COLOR純色,硬體直接填
CURSOR游標,可獨立低延遲更新
DISPLAY_DECORATION圓角、打孔等裝飾層(Android 13 起)

4.2 為什麼要在意這件事

Device composition 的路徑是:display controller 直接讀取各個 layer 的 buffer,在掃描輸出時做疊合。GPU 完全不參與,記憶體頻寬用量最小,功耗最低。

Client composition 的路徑是:SF 用 RenderEngine(Android 12/13 起預設 SkiaGLRenderEngine,較早版本是 GLESRenderEngine)把需要 fallback 的 layer 用 GPU 畫進一張暫存 buffer,再把這張 buffer 交給 HWC。

代價包含:

  • 喚醒 GPU(可能觸發升頻)
  • 多一輪 read + write 的記憶體頻寬
  • 多一塊 framebuffer target 的記憶體
  • SF 自己的 CPU 時間也拉長,壓縮合成的時間預算

在功耗敏感的裝置上,同一個畫面走 device 或 client composition,電流差距是可以直接量出來的。

4.3 什麼情況會 fallback 到 GPU

Overlay engine 是固定功能硬體,能力有邊界。常見的觸發原因:

Layer 數量超過硬體 overlay plane 上限。 這是最常見的一種。上限依 SoC 與 display pipeline 設計而異,需要查該平台的 display 規格;一旦超過,多出來的 layer 就會被合併成 client composition。

不支援的變換組合。 旋轉、縮放、鏡射,或這些變換的組合,硬體支援程度各不相同。

不支援的 pixel format 或 color space。 特別是 HDR 內容需要 tone mapping 時。

特殊的 blending 需求。 複雜的 alpha 混合、backdrop blur 這類效果。

Protected content 的路徑限制。 DRM 內容的 secure path 可能限制可用的 plane。

4.4 平板場景為什麼特別容易踩到

大螢幕 Android 的使用模式和手機不同,而 layer 數量會累加得很快:

分割畫面 + 浮動視窗的 layer 組成(示意)

App A 主視窗
App A 的 SurfaceView(影片播放)
分割線 divider
App B 主視窗
浮動視窗(PiP 或 bubble)
浮動視窗的陰影 layer
Status bar
Navigation bar
IME 視窗
Rounded corner overlay × 4

在手機上,全螢幕單一 App 的情境通常只有 3~5 個 layer,overlay plane 綽綽有餘。但平板的多工情境很容易一口氣衝上十幾層——這時候超過硬體上限、掉到 GPU composition 幾乎是必然的。

「開分割畫面之後耗電變高」這個 bug,多數情況下的根因就是這個。


五、除錯手段

5.1 dumpsys:第一站

adb shell dumpsys SurfaceFlinger

輸出很長,但幾個區塊值得優先看:

  • Layer 列表與 Z-order:確認實際有幾層、哪些是意料之外冒出來的
  • 每個 layer 的 composition typeDEVICE 還是 CLIENT,直接對應上一節的分析
  • Display 資訊:解析度、目前 refresh rate、支援的 display mode
  • HWC dump:vendor 提供的 HWC 內部狀態,通常包含 plane 配置

只列 layer 名稱:

adb shell dumpsys SurfaceFlinger --list

單一 layer 的 frame latency:

adb shell dumpsys SurfaceFlinger --latency <layer name>

輸出是三欄時間戳(desired present / actual present / frame ready),可以算出實際的 frame interval 分布。

5.2 TimeStats:統計式的 jank 資料

Android 10 起 SF 內建 TimeStats,適合做長時間統計而非單點觀察:

# 啟用並清空
adb shell dumpsys SurfaceFlinger --timestats -enable -clear

# ...操作待測情境...

# 取出報告
adb shell dumpsys SurfaceFlinger --timestats -dump -maxlayers 10

裡面有 frame duration 的 histogram、missed frame 計數、present-to-present 分布。做壓測或迴歸比較時比逐幀 trace 好用得多。

5.3 Perfetto:看清楚是誰的問題

這是判斷「App 端還是合成端」最有效的工具。抓 trace 時務必包含 gfxviewsched 這些 category,然後在 UI 上看這幾條 track:

  • surfaceflinger processonMessageInvalidate / composite 這些 slice 的長度,就是 SF 每幀的 CPU 開銷。突然變長通常伴隨 client composition。
  • App 的 UI thread 與 RenderThreadChoreographer#doFrameDrawFrames 的長度。
  • VSYNC-app / VSYNC-sf:節奏是否穩定、有沒有 refresh rate 切換。
  • Frame Timeline(Android 12 起)Expected TimelineActual Timeline 兩條 track 會直接標出 jank,並且分類歸因——是 AppDeadlineMissed 還是 SurfaceFlingerCpuDeadlineMissed。這個分類省下大量猜測時間。

判讀原則很簡單:

現象指向
App RenderThread 超時、SF 正常App 端繪製問題
App 正常、SF slice 明顯變長合成端問題,先查 composition type
兩邊都正常但仍掉幀sched track,可能是 CPU 排程 / 大核搶不到
Frame interval 週期性跳動可能是 refresh rate 切換或 buffer 數不足

5.4 Winscope / Layer Trace

需要逐幀檢視 layer 樹的變化(例如視窗轉場動畫出現異常)時,可以錄製 layer trace 並用 Winscope 檢視。這在追「動畫過程中某一幀位置錯了」這類問題時幾乎是唯一有效的手段。具體開啟方式在不同 Android 版本略有差異,建議查對應版本的 developer 文件或直接用 Developer Options 裡的 trace 選項。

5.5 一個實務檢查順序

遇到「畫面卡 / 耗電高」的 bug report,我的順序是:

  1. dumpsys SurfaceFlinger 看 layer 數與 composition type → 先排除 GPU composition fallback
  2. 如果有 fallback,找出是哪個 layer 造成的,往上追是哪個元件建的
  3. 抓 Perfetto,用 Frame Timeline 的 jank 分類決定往 App 端還是 SF 端查
  4. 用 TimeStats 做修改前後的量化比較,避免「感覺好像順一點」這種結論

六、小結

SurfaceFlinger 的角色可以壓縮成三件事:

  1. 收集 buffer — 透過 BufferQueue / BLAST transaction 從各個 producer 取得畫面內容
  2. 對齊節奏 — 以 VSYNC 模型決定何時繪製、何時合成
  3. 選擇路徑 — 透過 HWC 盡可能走 device composition,做不到才 fallback 到 GPU

而第 3 點是最值得系統整合工程師關注的。它不會產生 crash,不會出現在任何 log 的 error level,只會安靜地讓電流上升、讓 frame time 逼近預算上限。要看見它,只能主動去 dumpsys 裡確認 composition type。

大螢幕與多工情境會系統性地放大這個問題——這也是為什麼手機平台調好的參數,搬到平板上未必成立。


延伸閱讀方向

  • AOSP 官方文件的 Graphics 章節(BufferQueue、Hardware Composer、VSYNC 相關頁面)
  • frameworks/native/services/surfaceflinger/ 的原始碼,特別是 SurfaceFlinger.cppcomposite() 路徑與 CompositionEngine
  • hardware/interfaces/graphics/composer/ 的 HAL 介面定義
  • Perfetto 官方的 Android frame timeline 使用說明
  • 各 SoC vendor 的 display pipeline 規格文件——overlay plane 數量與能力邊界只能從這裡確認