各位戰士,歡迎來到第十五天的戰場,同時也是我們**第二場大型戰役【陣地戰 —— UI 流暢度攻防戰】**的開端。
如果說「啟動速度」是我們給使用者的第一印象,那麼「UI 流暢度」就是我們與使用者相處的每一分每一秒。一次成功的「閃電戰」可以贏得使用者的初見好感,但要真正留住他們,我們必須打贏這場曠日廢時的「陣地戰」。
這場戰爭的敵人,只有一個名字——Jank。
「Jank」是所有卡頓、掉幀、滾動不順、動畫掉漆的統稱。它就像戰場上的泥沼,拖慢我們前進的步伐,消磨使用者的耐心,最終讓他們棄甲而去。
在開火之前,我們必須先理解這個敵人。今天,我們的任務就是深入敵後,徹底搞清楚 Jank 的元兇——「掉幀」——究竟是如何發生的。
要理解掉幀,首先要認識 Android 系統的「心跳」—— VSYNC (垂直同步) 信號。
你的手機螢幕,無論是 60Hz、90Hz 還是 120Hz,都在以固定的頻率刷新。
1000ms / 60
) 就需要一張新的畫面。1000ms / 120
)。這個 16.67ms (或 8.33ms) 就是我們的「幀預算 (Frame Budget)」。VSYNC 就像是螢幕硬體每隔 16ms 就向系統發出的一次號令:「指揮官,我需要下一幀畫面,現在!」
如果我們的 App 在 VSYNC 信號到來之前,成功準備好了新畫面,螢幕就會流暢地顯示。但如果 App 因為某些原因花了 25ms 才完成工作,它就錯過了這次號令。系統沒辦法,只能再次顯示上一幀的舊畫面。
這,就是一次「掉幀」(Dropped Frame)。 使用者的眼睛會敏銳地察覺到這次停頓,感覺到的就是「卡了一下」。
那麼,是什麼導致我們錯過 VSYNC 的號令呢?這就要看 App 內部那條名為「渲染管線」的畫面生產線了。這條生產線有兩個核心的工人:
UI Thread 是 App 最核心、最繁忙的執行緒。它負責:
在畫面渲染這件事上,UI Thread 的核心任務是執行 onMeasure
, onLayout
, onDraw
等方法,將畫面佈局轉換成一份「繪製指令清單」(Display List)。
UI Thread 是 Jank 最主要的發生地。因為它太忙了,任何耗時的操作——網路請求、資料庫讀寫、大量的資料運算、複雜的 View 渲染——只要發生在這裡,就會阻塞整個生產線的源頭。如果「總設計師」被其他雜事絆住了,畫面的「設計圖」就出不來。
從 Android 5.0 (Lollipop) 開始,系統引入了 RenderThread 來分擔 UI Thread 的壓力。它的工作非常專一:
RenderThread 是一間高效的自動化工廠,只要 UI Thread 能準時提供設計圖,它通常都能在 VSYNC 到來前完成生產任務。
讓我們用時間軸來形象化一次掉幀的發生:
VSYNC 號令
|<---- UI Thread (5ms) ---->|<-- RenderThread (4ms) -->| |
|---------------------- 9ms (遠小於 16ms) ---------------------->|
畫面成功更新
下一次 VSYNC 號令
|
這張圖描述了一次完美的畫面更新,整個過程遠快於系統要求的 16ms。
第一步:VSYNC 號令
第二步:UI Thread (5ms)
第三步:RenderThread (4ms)
第四步:畫面成功更新
VSYNC 號令
|<---------------- UI Thread (執行了耗時 I/O,花了 22ms) ---------------->|
| |
| 錯過了 VSYNC 號令,只能顯示舊畫面 |
下一次 VSYNC 號令
|
|<-- RenderThread -->|
畫面終於更新
這張圖描述了一次失敗的畫面更新,也就是「卡頓」的瞬間。
第一步:VSYNC 號令
第二步:UI Thread (花了 22ms)
第三步:錯過了 VSYNC 號令
第四步:畫面終於更新
正常情況:你花了 5 分鐘整理行李,打包花了 4 分鐘。總共 9 分鐘,你悠閒地等著下一班公車來,順利上車。
掉幀情況:你在整理行李時,突然接了一個長達 22 分鐘的電話。當第 16 分鐘時公車來了,你的電話還沒講完,行李根本沒準備好。公車只好直接開走(顯示舊畫面),你錯過了這一班車。
今天,我們進行了戰前的敵情偵查,從根本上理解了 Jank 的成因。
我們知道了敵人是如何發動攻擊的。那麼,我們要如何偵測到敵人的蹤跡呢?明天,我們將學習使用第一批偵查工具:【開啟 GPU Overdraw 與 Profile GPU Rendering】,學會用肉眼快速定位戰場上的可疑區域。
準備好你的開發者選項,我們明天見!