Android 屏幕刷新機制

這次就來梳理一下 Android 的屏幕刷新機制,把我這段時間因爲研究動畫而梳理出來的一些關於屏幕刷新方面的知識點分享出來,能力有限,有錯的地方還望指點一下。另外,內容有點多,畢竟要講清楚不容易,所以慢慢看哈

提問環節

閱讀源碼還是得帶着問題或目的性的去閱讀,這樣閱讀過程中比較有條理性,不會跟偏或太深入,所以,還是先來幾個問題吧:
大夥都清楚,Android 每隔 16.6ms 會刷新一次屏幕。

Q1:但是大夥想過沒有,這個 16.6ms 刷新一次屏幕到底是什麼意思呢?是指每隔 16.6ms 調用 onDraw() 繪製一次麼?

Q2:如果界面一直保持沒變的話,那麼還會每隔 16.6ms 刷新一次屏幕麼?

Q3:界面的顯示其實就是一個 Activity 的 View 樹裏所有的 View 都進行測量、佈局、繪製操作之後的結果呈現,那麼如果這部分工作都完成後,屏幕會馬上就刷新麼?

Q4:網上都說避免丟幀的方法之一是保證每次繪製界面的操作要在 16.6ms 內完成,但如果這個 16.6ms 是一個固定的頻率的話,請求繪製的操作在代碼裏被調用的時機是不確定的啊,那麼如果某次用戶點擊屏幕導致的界面刷新操作是在某一個 16.6ms 幀快結束的時候,那麼即使這次繪製操作小於 16.6 ms,按道理不也會造成丟幀麼?這又該如何理解?

Q5:大夥都清楚,主線程耗時的操作會導致丟幀,但是耗時的操作爲什麼會導致丟幀?它是如何導致丟幀發生的?

本篇主要就是搞清楚這幾個問題,分析的源碼基本只涉及 ViewRootImpl 和 Choreographer 這兩個類。
源碼分析
ps:本篇分析的源碼均是 android-25 版本,版本不一樣,源碼可能會有些許差異,大夥過的時候注意一下。


基本概念

首先,先來過一下一些基本概念

在一個典型的顯示系統中,一般包括CPU、GPU、display三個部分, CPU負責計算數據,把計算好數據交給GPU,GPU會對圖形數據進行渲染,渲染好後放到buffer裏存起來,然後display(有的文章也叫屏幕或者顯示器)負責把buffer裏的數據呈現到屏幕上。
顯示過程,簡單的說就是CPU/GPU準備好數據,存入buffer,display每隔一段時間去buffer裏取數據,然後顯示出來。display讀取的頻率是固定的,比如每個16ms讀一次,但是CPU/GPU寫數據是完全無規律的。

上述內容概括一下,大體意思就是說,屏幕的刷新包括三個步驟:CPU 計算屏幕數據、GPU 進一步處理和緩存、最後 display 再將緩存中(buffer)的屏幕數據顯示出來。

(ps:開發過程中應該接觸不到 GPU、display 這些層面的東西,所以我把這部分工作都稱作底層的工作了,下文出現的底層指的就是除了 CPU 計算屏幕數據之外的工作。)

對於 Android 而言,第一個步驟:CPU 計算屏幕數據指的也就是 View 樹的繪製過程,也就是 Activity 對應的視圖樹從根佈局 DecorView 開始層層遍歷每個 View,分別執行測量、佈局、繪製三個操作的過程。

也就是說,我們常說的 Android 每隔 16.6ms 刷新一次屏幕其實是指:底層以固定的頻率,比如每 16.6ms 將 buffer 裏的屏幕數據顯示出來。

如果還不清楚,那再看一張網上很常見的圖:
Android 屏幕刷新機制
結合這張圖,再來講講 16.6 ms 屏幕刷新一次的意思。

Display 這一行可以理解成屏幕,所以可以看到,底層是以固定的頻率發出 VSync 信號的,而這個固定頻率就是我們常說的每 16.6ms 發送一個 VSync 信號,至於什麼叫 VSync 信號,我們可以不用深入去了解,只要清楚這個信號就是屏幕刷新的信號就可以了。

繼續看圖,Display ×××的這一行裏有一些數字:0, 1, 2, 3, 4,可以看到每次屏幕刷新信號到了的時候,數字就會變化,所以這些數字其實可以理解成每一幀屏幕顯示的畫面。也就是說,屏幕每一幀的畫面可以持續 16.6ms,當過了 16.6ms,底層就會發出一個屏幕刷新信號,而屏幕就會去顯示下一幀的畫面。

以上都是一些基本概念,也都是底層的工作,我們瞭解一下就可以了。接下去就還是看這圖,然後講講我們 app 層該乾的事了:

繼續看圖,CPU 藍色的這行,上面也說過了,CPU 這塊的耗時其實就是我們 app 繪製當前 View 樹的時間,而這段時間就跟我們自己寫的代碼有關係了,如果你的佈局很複雜,層次嵌套很多,每一幀內需要刷新的 View 又很多時,那麼每一幀的繪製耗時自然就會多一點。

繼續看圖,CPU 藍色這行裏也有一些數字,其實這些數字跟 Display ×××的那一行裏的數字是對應的,在 Display 裏我們解釋過這些數字表示的是每一幀的畫面,那麼在 CPU 這一行裏,其實就是在計算對應幀的畫面數據,也叫屏幕數據。也就是說,在當前幀內,CPU 是在計算下一幀的屏幕畫面數據,當屏幕刷新信號到的時候,屏幕就去將 CPU 計算的屏幕畫面數據顯示出來;同時 CPU 也接收到屏幕刷新信號,所以也開始去計算下一幀的屏幕畫面數據。

CPU 跟 Display 是不同的硬件,它們是可以並行工作的。要理解的一點是,我們寫的代碼,只是控制讓 CPU 在接收到屏幕刷新信號的時候開始去計算下一幀的畫面工作。而底層在每一次屏幕刷新信號來的時候都會去切換這一幀的畫面,這點我們是控制不了的,是底層的工作機制。之所以要講這點,是因爲,當我們的 app 界面沒有必要再刷新時(比如用戶不操作了,當前界面也沒動畫),這個時候,我們 app 是接收不到屏幕刷新信號的,所以也就不會讓 CPU 去計算下一幀畫面數據,但是底層仍然會以固定的頻率來切換每一幀的畫面,只是它後面切換的每一幀畫面都一樣,所以給我們的感覺就是屏幕沒刷新。

所以,我覺得上面那張圖還可以再繼續延深幾幀的長度,這樣就更容易理解了:
Android 屏幕刷新機制
Android 屏幕刷新機制
我在那張圖的基礎上延長了幾幀,我想這樣應該可以更容易理解點。

看我畫的這張圖,前三幀跟原圖一樣,從第三幀之後,因爲我們的 app 界面不需要刷新了(用戶不操作了,界面也沒有動畫),那麼這之後我們 app 就不會再接收到屏幕刷新信號了,所以也就不會再讓 CPU 去繪製視圖樹來計算下一幀畫面了。但是,底層還是會每隔 16.6ms 發出一個屏幕刷新信號,只是我們 app 不會接收到而已,Display 還是會在每一個屏幕刷新信號到的時候去顯示下一幀畫面,只是下一幀畫面一直是第4幀的內容而已。

好了,到這裏 Q1,Q2,Q3 都可以先回答一半了,那麼我們就先稍微來梳理一下:
1.我們常說的 Android 每隔 16.6 ms 刷新一次屏幕其實是指底層會以這個固定頻率來切換每一幀的畫面

2.這個每一幀的畫面也就是我們的 app 繪製視圖樹(View 樹)計算而來的,這個工作是交由 CPU 處理,耗時的長短取決於我們寫的代碼:佈局復不復雜,層次深不深,同一幀內刷新的 View 的數量多不多。

3.CPU 繪製視圖樹來計算下一幀畫面數據的工作是在屏幕刷新信號來的時候纔開始工作的,而當這個工作處理完畢後,也就是下一幀的畫面數據已經全部計算完畢,也不會馬上顯示到屏幕上,而是會等下一個屏幕刷新信號來的時候再交由底層將計算完畢的屏幕畫面數據顯示出來。

4.當我們的 app 界面不需要刷新時(用戶無操作,界面無動畫),app 就接收不到屏幕刷新信號所以也就不會讓 CPU 再去繪製視圖樹計算畫面數據工作,但是底層仍然會每隔 16.6 ms 切換下一幀的畫面,只是這個下一幀畫面一直是相同的內容。
這部分雖然說是一些基本概念,但其實也包含了一些結論了,所以可能大夥看着會有些困惑:爲什麼界面不刷新時 app 就接收不到屏幕刷新信號了?爲什麼繪製視圖樹計算下一幀畫面的工作會是在屏幕刷新信號來的時候纔開始的?等等。

emmm,有這些困惑很棒,這樣,我們下面一起過源碼時,大夥就更有目的性了,這樣過源碼我覺得效率是比較高一點的。繼續看下去,跟着過完源碼,你就清楚爲什麼了。好了,那我們下面就開始過源碼了。


ViewRootImpl 與 DecorView 的綁定

閱讀源碼從哪開始看起一直都是個頭疼的問題,所以找一個合適的切入點來跟的話,整個梳理的過程可能會順暢一點。本篇是研究屏幕的刷新,那麼建議就是從某個會導致屏幕刷新的方法入手,比如 View#invalidate()。
View#invalidate() 是請求重繪的一個操作,所以我們切入點可以從這個方法開始一步步跟下去。
我們這裏就直接說結論了。我們跟着 invalidate() 一步步往下走的時候,發現最後跟到了 ViewRootImpl#scheduleTraversals() 就停止了。而 ViewRootImpl 就是今天我們要介紹的重點對象了。

大夥都清楚,Android 設備呈現到界面上的大多數情況下都是一個 Activity,真正承載視圖的是一個 Window,每個 Window 都有一個 DecorView,我們調用 setContentView() 其實是將我們自己寫的佈局文件添加到以 DecorView 爲根佈局的一個 ViewGroup 裏,構成一顆 View 樹。

這些大夥都清楚,每個 Activity 對應一顆以 DecorView 爲根佈局的 View 樹,但其實 DecorView 還有 mParent,而且就是 ViewRootImpl,而且每個界面上的 View 的刷新,繪製,點擊事件的分發其實都是由 ViewRootImpl 作爲發起者的,由 ViewRootImpl 控制這些操作從 DecorView 開始遍歷 View 樹去分發處理。

在上一篇動畫分析的博客裏,分析 View#invalidate() 時,也可以看到內部其實是有一個 do{}while() 循環來不斷尋找 mParent,所以最終纔會走到 ViewRootImpl 裏去,那麼可能大夥就會疑問了,爲什麼 DecorView 的 mParent 會是 ViewRootImpl 呢?換個問法也就是,在什麼時候將 DevorView 和 ViewRootImpl 綁定起來?
Activity 的啓動是在 ActivityThread 裏完成的,handleLaunchActivity() 會依次間接的執行到 Activity 的 onCreate(), onStart(), onResume()。在執行完這些後 ActivityThread 會調用 WindowManager#addView(),而這個 addView() 最終其實是調用了 WindowManagerGlobal 的 addView() 方法,我們就從這裏開始看:
Android 屏幕刷新機制
WindowManager 維護着所有 Activity 的 DecorView 和 ViewRootImpl。這裏初始化了一個 ViewRootImpl,然後調用了它的 setView() 方法,將 DevorView 作爲參數傳遞了進去。所以看看 ViewRootImpl 中的 setView() 做了什麼:
Android 屏幕刷新機制
在 setView() 方法裏調用了 DecorView 的 assignParent() 方法,所以去看看 View 的這個方法:
Android 屏幕刷新機制
參數是 ViewParent,而 ViewRootImpl 是實現了 ViewParent 接口的,所以在這裏就將 DecorView 和 ViewRootImpl 綁定起來了。每個Activity 的根佈局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,所以在子 View 裏執行 invalidate() 之類的操作,循環找 parent 時,最後都會走到 ViewRootImpl 裏來。

跟界面刷新相關的方法裏應該都會有一個循環找 parent 的方法,或者是不斷調用 parent 的方法,這樣最終才都會走到 ViewRootImpl 裏,也就是說實際上 View 的刷新都是由 ViewRootImpl 來控制的。

即使是界面上一個小小的 View 發起了重繪請求時,都要層層走到 ViewRootImpl,由它來發起重繪請求,然後再由它來開始遍歷 View 樹,一直遍歷到這個需要重繪的 View 再調用它的 onDraw() 方法進行繪製。

我們重新看回 ViewRootImpl 的 setView() 這個方法,這個方法裏還調用了一個 requestLayout() 方法:
參數是 ViewParent,而 ViewRootImpl 是實現了 ViewParent 接口的,所以在這裏就將 DecorView 和 ViewRootImpl 綁定起來了。每個Activity 的根佈局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,所以在子 View 裏執行 invalidate() 之類的操作,循環找 parent 時,最後都會走到 ViewRootImpl 裏來。

跟界面刷新相關的方法裏應該都會有一個循環找 parent 的方法,或者是不斷調用 parent 的方法,這樣最終才都會走到 ViewRootImpl 裏,也就是說實際上 View 的刷新都是由 ViewRootImpl 來控制的。

即使是界面上一個小小的 View 發起了重繪請求時,都要層層走到 ViewRootImpl,由它來發起重繪請求,然後再由它來開始遍歷 View 樹,一直遍歷到這個需要重繪的 View 再調用它的 onDraw() 方法進行繪製。

我們重新看回 ViewRootImpl 的 setView() 這個方法,這個方法裏還調用了一個 requestLayout() 方法:
參數是 ViewParent,而 ViewRootImpl 是實現了 ViewParent 接口的,所以在這裏就將 DecorView 和 ViewRootImpl 綁定起來了。每個Activity 的根佈局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,所以在子 View 裏執行 invalidate() 之類的操作,循環找 parent 時,最後都會走到 ViewRootImpl 裏來。

跟界面刷新相關的方法裏應該都會有一個循環找 parent 的方法,或者是不斷調用 parent 的方法,這樣最終才都會走到 ViewRootImpl 裏,也就是說實際上 View 的刷新都是由 ViewRootImpl 來控制的。

即使是界面上一個小小的 View 發起了重繪請求時,都要層層走到 ViewRootImpl,由它來發起重繪請求,然後再由它來開始遍歷 View 樹,一直遍歷到這個需要重繪的 View 再調用它的 onDraw() 方法進行繪製。

我們重新看回 ViewRootImpl 的 setView() 這個方法,這個方法裏還調用了一個 requestLayout() 方法:
參數是 ViewParent,而 ViewRootImpl 是實現了 ViewParent 接口的,所以在這裏就將 DecorView 和 ViewRootImpl 綁定起來了。每個Activity 的根佈局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,所以在子 View 裏執行 invalidate() 之類的操作,循環找 parent 時,最後都會走到 ViewRootImpl 裏來。

跟界面刷新相關的方法裏應該都會有一個循環找 parent 的方法,或者是不斷調用 parent 的方法,這樣最終才都會走到 ViewRootImpl 裏,也就是說實際上 View 的刷新都是由 ViewRootImpl 來控制的。

即使是界面上一個小小的 View 發起了重繪請求時,都要層層走到 ViewRootImpl,由它來發起重繪請求,然後再由它來開始遍歷 View 樹,一直遍歷到這個需要重繪的 View 再調用它的 onDraw() 方法進行繪製。

我們重新看回 ViewRootImpl 的 setView() 這個方法,這個方法裏還調用了一個 requestLayout() 方法:
Android 屏幕刷新機制
這裏調用了一個 scheduleTraversals(),還記得當 View 發起重繪操作 invalidate() 時,最後也調用了 scheduleTraversals() 這個方法麼。其實這個方法就是屏幕刷新的關鍵,它是安排一次繪製 View 樹的任務等待執行,具體後面再說。

也就是說,其實打開一個 Activity,當它的 onCreate---onResume 生命週期都走完後,纔將它的 DecoView 與新建的一個 ViewRootImpl 對象綁定起來,同時開始安排一次遍歷 View 任務也就是繪製 View 樹的操作等待執行,然後將 DecoView 的 parent 設置成 ViewRootImpl 對象。
這也就是爲什麼在 onCreate---onResume 裏獲取不到 View 寬高的原因,因爲在這個時刻 ViewRootImpl 甚至都還沒創建,更不用說是否已經執行過測量操作了。

還可以得到一點信息是,一個 Activity 界面的繪製,其實是在 onResume() 之後纔開始的。

ViewRootImpl#scheduleTraversals

到這裏,我們梳理清楚了,調用一個 View 的 invalidate() 請求重繪操作,內部原來是要層層通知到 ViewRootImpl 的 scheduleTraversals() 裏去。而且打開一個新的 Activity,它的界面繪製原來是在 onResume() 之後也層層通知到 ViewRootImpl 的 scheduleTraversals() 裏去。雖然其他關於 View 的刷新操作,比如 requestLayout() 等等之類的方法我們還沒有去看,但我們已經可以大膽猜測,這些跟 View 刷新有關的操作最終也都會層層走到 ViewRootImpl 中的 scheduleTraversals() 方法裏去的。

那麼這個方法究竟幹了些什麼,我們就要好好來分析了:
Android 屏幕刷新機制

mTraversalScheduled 這個 boolean 變量的作用等會再來看,先看看 mChoreographer.postCallback() 這個方法,傳入了三個參數,第二個參數是一個 Runnable 對象,先來看看這個 Runnable:
Android 屏幕刷新機制
這個 Runnable 做的事很簡單,就調用了一個方法,doTraversal():
Android 屏幕刷新機制
看看這個方法做的事,跟 scheduleTraversals() 正好相反,一個將變量置成 true,這裏置成 false,一個是 postSyncBarrier(),這裏是 removeSyncBarrier(),具體作用等會再說,繼續先看看 performTraversals(),這個方法也是屏幕刷新的關鍵:
Android 屏幕刷新機制
View 的測量、佈局、繪製三大流程都是交由 ViewRootImpl 發起,而且還都是在 performTraversals() 方法中發起的,所以這個方法的邏輯很複雜,因爲每次都需要根據相應狀態判斷是否需要三個流程都走,有時可能只需要執行 performDraw() 繪製流程,有時可能只執行 performMeasure() 測量和 performLayout() 佈局流程(一般測量和佈局流程是一起執行的)。不管哪個流程都會遍歷一次 View 樹,所以其實界面的繪製是需要遍歷很多次的,如果頁面層次太過複雜,每一幀需要刷新的 View 又很多時,耗時就會長一點。

當然,測量、佈局、繪製這些流程在遍歷時並不一定會把整顆 View 樹都遍歷一遍,ViewGroup 在傳遞這些流程時,還會再根據相應狀態判斷是否需要繼續往下傳遞。
瞭解了 performTraversals() 是刷新界面的源頭後,接下去就需要了解下它是什麼時候執行的,和 scheduleTraversals() 又是什麼關係?
performTraversals() 是在 doTraversal() 中被調用的,而 doTraversal() 又被封裝到一個 Runnable 裏,那麼關鍵就是這個 Runnable 什麼時候被執行了?


Choreographer

scheduleTraversals() 裏調用了 Choreographer 的 postCallback() 將 Runnable 作爲參數傳了進去,所以跟進去看看:
Android 屏幕刷新機制
因爲 postCallback() 調用 postCallbackDelayed() 時傳了 delay = 0 進去,所以在 postCallbackDelayedInternal() 裏面會先根據當前時間戳將這個 Runnable 保存到一個 mCallbackQueue 隊列裏,這個隊列跟 MessageQueue 很相似,裏面待執行的任務都是根據一個時間戳來排序。然後走了 scheduleFrameLocked() 方法這邊,看看做了些什麼:
Android 屏幕刷新機制
如果代碼走了 else 這邊來發送一個消息,那麼這個消息做的事肯定很重要,因爲對這個 Message 設置了異步的標誌而且用了sendMessageAtFrontOfQueue() 方法,這個方法是將這個 Message 直接放到 MessageQueue 隊列裏的頭部,可以理解成設置了這個 Message 爲最高優先級,那麼先看看這個 Message 做了些什麼:
Android 屏幕刷新機制
所以這個 Message 最後做的事就是 scheduleVsyncLocked()。我們回到 scheduleFrameLocked() 這個方法裏,當走 if 裏的代碼時,直接調用了 scheduleVsyncLocked(),當走 else 裏的代碼時,發了一個最高優先級的 Message,這個 Message 也是執行 scheduleVsyncLocked()。既然兩邊最後調用的都是同一個方法,那麼爲什麼這麼做呢?

關鍵在於 if 條件裏那個方法,我的理解那個方法是用來判斷當前是否是在主線程的,我們知道主線程也是一直在執行着一個個的 Message,那麼如果在主線程的話,直接調用這個方法,那麼這個方法就可以直接被執行了,如果不是在主線程,那麼 post 一個最高優先級的 Message 到主線程去,保證這個方法可以第一時間得到處理。

那麼這個方法是幹嘛的呢,爲什麼需要在最短時間內被執行呢,而且只能在主線程?
Android 屏幕刷新機制
調用了 native 層的一個方法,那跟到這裏就跟不下去了。

那到這裏,我們先來梳理一下:
到這裏爲止,我們知道一個 View 發起刷新的操作時,會層層通知到 ViewRootImpl 的 scheduleTraversals() 裏去,然後這個方法會將遍歷繪製 View 樹的操作 performTraversals() 封裝到 Runnable 裏,傳給 Choreographer,以當前的時間戳放進一個 mCallbackQueue 隊列裏,然後調用了 native 層的一個方法就跟不下去了。所以這個 Runnable 什麼時候會被執行還不清楚。那麼,下去的重點就是搞清楚它什麼時候從隊列裏被拿出來執行了?

接下去只能換種方式繼續跟了,既然這個 Runnable 操作被放在一個 mCallbackQueue 隊列裏,那就從這個隊列着手,看看這個隊列的取操作在哪被執行了:
Android 屏幕刷新機制
Android 屏幕刷新機制
Android 屏幕刷新機制

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章