iT邦幫忙

2025 iThome 鐵人賽

DAY 15
0
Mobile Development

Android 性能戰爭:從 Profiler 開始的 30 天優化實錄系列 第 15

# Day 15:【流暢度戰爭】Jank 的元兇:掉幀是如何發生的?

  • 分享至 

  • xImage
  •  

各位戰士,歡迎來到第十五天的戰場,同時也是我們**第二場大型戰役【陣地戰 —— UI 流暢度攻防戰】**的開端。

如果說「啟動速度」是我們給使用者的第一印象,那麼「UI 流暢度」就是我們與使用者相處的每一分每一秒。一次成功的「閃電戰」可以贏得使用者的初見好感,但要真正留住他們,我們必須打贏這場曠日廢時的「陣地戰」。

這場戰爭的敵人,只有一個名字——Jank

「Jank」是所有卡頓、掉幀、滾動不順、動畫掉漆的統稱。它就像戰場上的泥沼,拖慢我們前進的步伐,消磨使用者的耐心,最終讓他們棄甲而去。

在開火之前,我們必須先理解這個敵人。今天,我們的任務就是深入敵後,徹底搞清楚 Jank 的元兇——「掉幀」——究竟是如何發生的。


系統的心跳:VSYNC 與 16ms 戰爭

要理解掉幀,首先要認識 Android 系統的「心跳」—— VSYNC (垂直同步) 信號。

你的手機螢幕,無論是 60Hz、90Hz 還是 120Hz,都在以固定的頻率刷新。

  • 60Hz 螢幕:每秒刷新 60 次。這意味著,系統每 16.67 毫秒 (1000ms / 60) 就需要一張新的畫面。
  • 120Hz 螢幕:每秒刷新 120 次。時間被壓縮到每 8.33 毫秒 (1000ms / 120)。

這個 16.67ms (或 8.33ms) 就是我們的「幀預算 (Frame Budget)」。VSYNC 就像是螢幕硬體每隔 16ms 就向系統發出的一次號令:「指揮官,我需要下一幀畫面,現在!」

如果我們的 App 在 VSYNC 信號到來之前,成功準備好了新畫面,螢幕就會流暢地顯示。但如果 App 因為某些原因花了 25ms 才完成工作,它就錯過了這次號令。系統沒辦法,只能再次顯示上一幀的舊畫面

這,就是一次「掉幀」(Dropped Frame)。 使用者的眼睛會敏銳地察覺到這次停頓,感覺到的就是「卡了一下」。


畫面的生產線:UI Thread 與 RenderThread

那麼,是什麼導致我們錯過 VSYNC 的號令呢?這就要看 App 內部那條名為「渲染管線」的畫面生產線了。這條生產線有兩個核心的工人:

1. UI Thread (主執行緒,又稱「總設計師」)

UI Thread 是 App 最核心、最繁忙的執行緒。它負責:

  • 接收使用者的點擊、滑動等輸入事件。
  • 執行我們大部分的 Kotlin/Java 程式碼(業務邏輯)。
  • 建立和更新畫面上的所有 View 元件

在畫面渲染這件事上,UI Thread 的核心任務是執行 onMeasure, onLayout, onDraw 等方法,將畫面佈局轉換成一份「繪製指令清單」(Display List)。

UI Thread 是 Jank 最主要的發生地。因為它太忙了,任何耗時的操作——網路請求、資料庫讀寫、大量的資料運算、複雜的 View 渲染——只要發生在這裡,就會阻塞整個生產線的源頭。如果「總設計師」被其他雜事絆住了,畫面的「設計圖」就出不來。

2. RenderThread (渲染執行緒,又稱「自動化工廠」)

從 Android 5.0 (Lollipop) 開始,系統引入了 RenderThread 來分擔 UI Thread 的壓力。它的工作非常專一:

  • 從 UI Thread 手中接過「繪製指令清單」。
  • 將這份清單轉譯成 GPU 能夠理解的指令。
  • 將指令交給 GPU 去執行真正的像素繪製。

RenderThread 是一間高效的自動化工廠,只要 UI Thread 能準時提供設計圖,它通常都能在 VSYNC 到來前完成生產任務。


一次掉幀的完整過程

讓我們用時間軸來形象化一次掉幀的發生:

正常情況 (流暢)

VSYNC 號令
|<---- UI Thread (5ms) ---->|<-- RenderThread (4ms) -->| |
|---------------------- 9ms (遠小於 16ms) ---------------------->|
畫面成功更新
下一次 VSYNC 號令
|
這張圖描述了一次完美的畫面更新,整個過程遠快於系統要求的 16ms。

  • 第一步:VSYNC 號令

    • 這可以想像是比賽的「起跑槍響」。系統說:「開始!請在 16ms 內給我一張新畫面!」
  • 第二步:UI Thread (5ms)

    • UI 執行緒(總設計師)立刻開始工作。它可能在計算按鈕的新位置、準備文字等等。
    • 它花了 5ms 就完成了工作,把畫面的「設計圖」準備好了。
  • 第三步:RenderThread (4ms)

    • UI 執行緒把設計圖交給 Render 執行緒(自動化工廠)。
    • Render 執行緒接收設計圖後,把它轉譯成 GPU 能懂的指令,這個過程花了 4ms
  • 第四步:畫面成功更新

    • 總共耗時:5ms + 4ms = 9ms
    • 因為 9ms 遠小於 16ms 的期限,所以我們輕鬆地在「下一次 VSYNC 號令」到來前完成了所有工作。
    • 系統順利地拿到了新畫面並顯示出來。使用者感覺到的就是流暢

2. 掉幀情況 (Jank) 的時間軸解析

VSYNC 號令
|<---------------- UI Thread (執行了耗時 I/O,花了 22ms) ---------------->|
| |
| 錯過了 VSYNC 號令,只能顯示舊畫面 |
下一次 VSYNC 號令
|
|<-- RenderThread -->|
畫面終於更新
這張圖描述了一次失敗的畫面更新,也就是「卡頓」的瞬間。

  • 第一步:VSYNC 號令

    • 同樣,起跑槍響,16ms 的倒數計時開始。
  • 第二步:UI Thread (花了 22ms)

    • 這是問題的根源。UI 執行緒這次沒有在做單純的畫面計算,而是跑去做了一件非常耗時的事情(例如:讀取手機儲存空間的檔案、執行複雜的資料庫查詢等)。
    • 這件事讓它足足忙了 22ms
  • 第三步:錯過了 VSYNC 號令

    • 在時間軸跑到第 16ms 的時候,「下一次 VSYNC 號令」來了,系統來收作業了:「新畫面準備好了嗎?」
    • 但此時 UI 執行緒還在忙(22ms 的工作才進行到第 16ms),根本沒空交出設計圖。
    • 期限已到,但作業沒交。系統沒有新畫面可以顯示,只好把上一張舊的畫面再顯示一次
    • 這就是「掉幀」!使用者在視覺上感覺到的就是畫面「停頓」或「凍結」了一下。
  • 第四步:畫面終於更新

    • UI 執行緒在 22ms 後終於忙完了,把設計圖交給 Render 執行緒。
    • Render 執行緒快速完成工作,但為時已晚。這張千呼萬喚始出來的新畫面,只能等到再下一次的 VSYNC 號令才能被顯示出來。

簡單比喻

  • VSYNC 想像成每 16 分鐘一班的公車
  • 你 (UI Thread) 的工作是「準備好要上車的行李」。
  • 行李打包 (RenderThread) 的工作是「把行李裝上車」。

正常情況:你花了 5 分鐘整理行李,打包花了 4 分鐘。總共 9 分鐘,你悠閒地等著下一班公車來,順利上車。
掉幀情況:你在整理行李時,突然接了一個長達 22 分鐘的電話。當第 16 分鐘時公車來了,你的電話還沒講完,行李根本沒準備好。公車只好直接開走(顯示舊畫面),你錯過了這一班車。

今日總結

今天,我們進行了戰前的敵情偵查,從根本上理解了 Jank 的成因。

  • 我們認識了系統的心跳 VSYNC,以及它定下的 16ms 軍規。
  • 我們剖析了畫面的生產線,明確了 UI ThreadRenderThread 的職責。
  • 我們鎖定了 Jank 的頭號元兇:在 UI Thread 上執行了耗時操作,阻塞了渲染管線,最終錯過了 VSYNC 的時間點,導致掉幀。

我們知道了敵人是如何發動攻擊的。那麼,我們要如何偵測到敵人的蹤跡呢?明天,我們將學習使用第一批偵查工具:【開啟 GPU Overdraw 與 Profile GPU Rendering】,學會用肉眼快速定位戰場上的可疑區域。

準備好你的開發者選項,我們明天見!


上一篇
# Day 14:【啟動戰役】戰果驗收:數據對比與總結
系列文
Android 性能戰爭:從 Profiler 開始的 30 天優化實錄15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言