Android 屏幕刷新機制

本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈android

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

提問環節

閱讀源碼仍是得帶着問題或目的性的去閱讀,這樣閱讀過程當中比較有條理性,不會跟偏或太深刻,因此,仍是先來幾個問題吧:性能優化

大夥都清楚,Android 每隔 16.6ms 會刷新一次屏幕。微信

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

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

Q3:界面的顯示其實就是一個 Activity 的 View 樹裏全部的 View 都進行測量、佈局、繪製操做以後的結果呈現,那麼若是這部分工做都完成後,屏幕會立刻就刷新麼?異步

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

Q5:大夥都清楚,主線程耗時的操做會致使丟幀,可是耗時的操做爲何會致使丟幀?它是如何致使丟幀發生的?源碼分析

本篇主要就是搞清楚這幾個問題,分析的源碼基本只涉及 ViewRootImplChoreographer 這兩個類。佈局

源碼分析

ps:本篇分析的源碼均是 android-25 版本,版本不同,源碼可能會有些許差別,大夥過的時候注意一下。

基本概念

首先,先來過一下一些基本概念,摘抄自網上文章android屏幕刷新顯示機制

在一個典型的顯示系統中,通常包括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 裏的屏幕數據顯示出來。

若是還不清楚,那再看一張網上很常見的圖(摘自上面同一篇文章):

image.png

結合這張圖,再來說講 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 去計算下一幀畫面數據,可是底層仍然會以固定的頻率來切換每一幀的畫面,只是它後面切換的每一幀畫面都同樣,因此給咱們的感受就是屏幕沒刷新。

因此,我以爲上面那張圖還能夠再繼續延深幾幀的長度,這樣就更容易理解了:

屏幕刷新機制.png

我在那張圖的基礎上延長了幾幀,我想這樣應該能夠更容易理解點。

看我畫的這張圖,前三幀跟原圖同樣,從第三幀以後,由於咱們的 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() 是請求重繪的一個操做,因此咱們切入點能夠從這個方法開始一步步跟下去。咱們在上一篇博客View 動畫 Animation 運行原理解析已經分析過 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() 方法,咱們就從這裏開始看:

WindowManagerGlobal#addView

WindowManager 維護着全部 Activity 的 DecorView 和 ViewRootImpl。這裏初始化了一個 ViewRootImpl,而後調用了它的 setView() 方法,將 DevorView 做爲參數傳遞了進去。因此看看 ViewRootImpl 中的 setView() 作了什麼:

ViewRootImpl#setView

setView() 方法裏調用了 DecorView 的 assignParent() 方法,因此去看看 View 的這個方法:

View#assignParent

參數是 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() 方法:

ViewRootImpl#requestLayout

這裏調用了一個 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() 方法裏去的。

那麼這個方法究竟幹了些什麼,咱們就要好好來分析了:

ViewRootImpl#scheduleTraversals

mTraversalScheduled 這個 boolean 變量的做用等會再來看,先看看 mChoreographer.postCallback() 這個方法,傳入了三個參數,第二個參數是一個 Runnable 對象,先來看看這個 Runnable:

TraversalRunnable

這個 Runnable 作的事很簡單,就調用了一個方法,doTraversal():

ViewRootImpl#doTraversal

看看這個方法作的事,跟 scheduleTraversals() 正好相反,一個將變量置成 true,這裏置成 false,一個是 postSyncBarrier(),這裏是 removeSyncBarrier(),具體做用等會再說,繼續先看看 performTraversals(),這個方法也是屏幕刷新的關鍵:

ViewRootImpl#performTraversals

View 的測量、佈局、繪製三大流程都是交由 ViewRootImpl 發起,並且還都是在 performTraversals() 方法中發起的,因此這個方法的邏輯很複雜,由於每次都須要根據相應狀態判斷是否須要三個流程都走,有時可能只須要執行 performDraw() 繪製流程,有時可能只執行 performMeasure() 測量和 performLayout() 佈局流程(通常測量和佈局流程是一塊兒執行的)。無論哪一個流程都會遍歷一次 View 樹,因此其實界面的繪製是須要遍歷不少次的,若是頁面層次太過複雜,每一幀須要刷新的 View 又不少時,耗時就會長一點。

固然,測量、佈局、繪製這些流程在遍歷時並不必定會把整顆 View 樹都遍歷一遍,ViewGroup 在傳遞這些流程時,還會再根據相應狀態判斷是否須要繼續往下傳遞。

瞭解了 performTraversals() 是刷新界面的源頭後,接下去就須要瞭解下它是何時執行的,和 scheduleTraversals() 又是什麼關係?

performTraversals() 是在 doTraversal() 中被調用的,而 doTraversal() 又被封裝到一個 Runnable 裏,那麼關鍵就是這個 Runnable 何時被執行了?

Choreographer

scheduleTraversals() 裏調用了 Choreographer 的 postCallback() 將 Runnable 做爲參數傳了進去,因此跟進去看看:

Choreographer#postCallback
Choreographer#postCallbackDelayedInternal

由於 postCallback() 調用 postCallbackDelayed() 時傳了 delay = 0 進去,因此在 postCallbackDelayedInternal() 裏面會先根據當前時間戳將這個 Runnable 保存到一個 mCallbackQueue 隊列裏,這個隊列跟 MessageQueue 很類似,裏面待執行的任務都是根據一個時間戳來排序。而後走了 scheduleFrameLocked() 方法這邊,看看作了些什麼:

Choreographer#scheduleFrameLocked

若是代碼走了 else 這邊來發送一個消息,那麼這個消息作的事確定很重要,由於對這個 Message 設置了異步的標誌並且用了sendMessageAtFrontOfQueue() 方法,這個方法是將這個 Message 直接放到 MessageQueue 隊列裏的頭部,能夠理解成設置了這個 Message 爲最高優先級,那麼先看看這個 Message 作了些什麼:

Choreograhper$FrameHandler#handleMessage
Choreographer#doScheduleVsync

因此這個 Message 最後作的事就是 scheduleVsyncLocked()。咱們回到 scheduleFrameLocked() 這個方法裏,當走 if 裏的代碼時,直接調用了 scheduleVsyncLocked(),當走 else 裏的代碼時,發了一個最高優先級的 Message,這個 Message 也是執行 scheduleVsyncLocked()。既然兩邊最後調用的都是同一個方法,那麼爲何這麼作呢?

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

那麼這個方法是幹嗎的呢,爲何須要在最短期內被執行呢,並且只能在主線程?

Choreographer#scheduleVsyncLocked
DisplayEventReceiver#scheduleVsync

調用了 native 層的一個方法,那跟到這裏就跟不下去了。

那到這裏,咱們先來梳理一下:

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

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

Choreographer$CallbackQueue

Choreographer#doCallbacks

Choreographer#doFrame

還記得咱們說過在 ViewRootImpl 的 scheduleTraversals() 裏會將遍歷 View 樹繪製的操做封裝到 Runnable 裏,而後調用 Choreographer 的 postCallback() 將這個 Runnable 放進隊列裏麼,而當時調用 postCallback() 時傳入了多個參數,這是由於 Choreographer 裏有多個隊列,而第一個參數 Choreographer.CALLBACK_TRAVERSAL 這個參數是用來區分隊列的,能夠理解成各個隊列的 key 值。

那麼這樣一來,就找到關鍵的方法了:doFrame(),這個方法裏會根據一個時間戳去隊列裏取任務出來執行,而這個任務就是 ViewRootImpl 封裝起來的 doTraversal() 操做,而 doTraversal() 會去調用 performTraversals() 開始根據須要測量、佈局、繪製整顆 View 樹。因此剩下的問題就是 doFrame() 這個方法在哪裏被調用了。

有幾個調用的地方,但有個地方很關鍵:

Choreographer$FrameDisplayEventReceiver

關鍵的地方來了,這個繼承自 DisplayEventReceiver 的 FrameDisplayEventReceiver 類的做用很重要。跟進去看註釋,我只能理解它是用來接收底層信號用的。但看了網上的解釋後,全部的都理解過來了:

FrameDisplayEventReceiver繼承自DisplayEventReceiver接收底層的VSync信號開始處理UI過程。VSync信號由SurfaceFlinger實現並定時發送。FrameDisplayEventReceiver收到信號後,調用onVsync方法組織消息發送到主線程處理。這個消息主要內容就是run方法裏面的doFrame了,這裏mTimestampNanos是信號到來的時間參數。

也就是說,onVsync() 是底層會回調的,能夠理解成每隔 16.6ms 一個幀信號來的時候,底層就會回調這個方法,固然前提是咱們得先註冊,這樣底層才能找到咱們 app 並回調。當這個方法被回調時,內部發起了一個 Message,注意看代碼對這個 Message 設置了 callback 爲 this,Handler 在處理消息時會先查看 Message 是否有 callback,有則優先交由 Message 的 callback 處理消息,沒有的話再去看看Handler 有沒有 callback,若是也沒有才會交由 handleMessage() 這個方法執行。

這裏這麼作的緣由,我猜想可能 onVsync() 是由底層回調的,那麼它就不是運行在咱們 app 的主線程上,畢竟上層 app 對底層是隱藏的。但這個 doFrame() 是個 ui 操做,它須要在主線程中執行,因此才經過 Handler 切到主線程中。

還記得咱們前面分析 scheduleTraversals() 方法時,最後跟到了一個 native 層方法就跟不下去了麼,如今再回過來想一想這個 native 層方法的做用是什麼,應該就比較好猜想了。

DisplayEventReceiver#scheduleVsync

英文不大理解,大致上多是說安排接收一個 vsync 信號。而根據咱們的分析,若是這個 vsync 信號發出的話,底層就會回調 DisplayEventReceiver 的 onVsync() 方法。

那若是隻是這樣的話,就有一點說不通了,首先上層 app 對於這些發送 vsync 信號的底層來講確定是隱藏的,也就是說底層它根本不知道上層 app 的存在,那麼在它的每 16.6ms 的幀信號來的時候,它是怎麼找到咱們的 app,並回調它的方法呢?

這就有點相似於觀察者模式,或者說發佈-訂閱模式。既然上層 app 須要知道底層每隔 16.6ms 的幀信號事件,那麼它就須要先註冊監聽纔對,這樣底層在發信號的時候,直接去找這些觀察者通知它們就好了。

這是個人理解,因此,這樣一來,scheduleVsync() 這個調用到了 native 層方法的做用大致上就能夠理解成註冊監聽了,這樣底層也才找獲得上層 app,並在每 16.6ms 刷新信號發出的時候回調上層 app 的 onVsync() 方法。這樣一來,應該就說得通了。

還有一點,scheduleVsync() 註冊的監聽應該只是監聽下一個屏幕刷新信號的事件而已,而不是監聽全部的屏幕刷新信號。好比說當前監聽了第一幀的刷新信號事件,那麼當第一幀的刷新信號來的時候,上層 app 就能接收到事件並做出反應。但若是還想監聽第二幀的刷新信號,那麼只能等上層 app 接收到第一幀的刷新信號以後再去監聽下一幀。

雖然如今能力還不足以跟蹤到 native 層,這些結論雖然是猜想的,但都通過調試,對註釋、代碼理解以後梳理出來的結論,跟原理應該不會誤差太多,這樣子的理解應該是能夠的。

本篇內容確實有點多,因此到這裏仍是繼續來先來梳理一下目前的信息,防止都忘記上面講了些什麼:

  1. 咱們知道一個 View 發起刷新的操做時,最終是走到了 ViewRootImpl 的 scheduleTraversals() 裏去,而後這個方法會將遍歷繪製 View 樹的操做 performTraversals() 封裝到 Runnable 裏,傳給 Choreographer,以當前的時間戳放進一個 mCallbackQueue 隊列裏,而後調用了 native 層的方法向底層註冊監聽下一個屏幕刷新信號事件。

  2. 當下一個屏幕刷新信號發出的時候,若是咱們 app 有對這個事件進行監聽,那麼底層它就會回調咱們 app 層的 onVsync() 方法來通知。當 onVsync() 被回調時,會發一個 Message 到主線程,將後續的工做切到主線程來執行。

  3. 切到主線程的工做就是去 mCallbackQueue 隊列里根據時間戳將以前放進去的 Runnable 取出來執行,而這些 Runnable 有一個就是遍歷繪製 View 樹的操做 performTraversals()。在此次的遍歷操做中,就會去繪製那些須要刷新的 View。

  4. 因此說,當咱們調用了 invalidate(),requestLayout(),等之類刷新界面的操做時,並非立刻就會執行這些刷新的操做,而是經過 ViewRootImpl 的 scheduleTraversals() 先向底層註冊監聽下一個屏幕刷新信號事件,而後等下一個屏幕刷新信號來的時候,纔會去經過 performTraversals() 遍歷繪製 View 樹來執行這些刷新操做。

過濾一幀內重複的刷新請求

總體上的流程咱們已經梳理出來的,但還有幾點問題須要解決。咱們在一個 16.6ms 的一幀內,代碼裏可能會有多個 View 發起了刷新請求,這是很是常見的場景了,好比某個動畫是有多個 View 一塊兒完成,好比界面發生了滑動等等。

按照咱們上面梳理的流程,只要 View 發起了刷新請求最終都會走到 ViewRootImpl 中的 scheduleTraversals() 裏去,是吧。而這個方法又會封裝一個遍歷繪製 View 樹的操做 performTraversals() 到 Runnable 而後扔到隊列裏等刷新信號來的時候取出來執行,沒錯吧。

那若是多個 View 發起了刷新請求,豈不是意味着會有屢次遍歷繪製 View 樹的操做?

其實,這點不用擔憂,還記得咱們在最開始分析 scheduleTraverslas() 的時候先跳過了一些代碼麼?如今咱們回過來繼續看看這些代碼:

ViewRootImpl#scheduleTraversals2

咱們上面分析的 scheduleTraversals() 乾的那一串工做,前提是 mTraversalScheduled 這個 boolean 類型變量等於 false 纔會去執行。那這個變量在何時被賦值被 false 了呢:

ViewRootImpl#doTraversal2

只有三個被賦值爲 false 的地方,一個是上圖的 doTraversal(),還有就是聲明時默認爲 false,剩下一個是在取消遍歷繪製 View 操做 unscheduleTraversals() 裏。這兩個能夠先不去看,就看看 doTraversal()。還記得這個方法吧,就是在 scheduleTraversals() 中封裝到 Runnable 裏的那個方法。

也就是說,當咱們調用了一次 scheduleTraversals()以後,直到下一個屏幕刷新信號來的時候,doTraversal() 被取出來執行。在這期間重複調用 scheduleTraversals() 都會被過濾掉的。那麼爲何須要這樣呢?

其實,想一想就能明白了。View 最終是怎麼刷新的呢,就是在執行 performTraversals() 遍歷繪製 View 樹過程當中層層遍歷到須要刷新的 View,而後去繪製它的吧。既然是遍歷,那麼無論上一幀內有多少個 View 發起了刷新的請求,在這一次的遍歷過程當中所有都會去處理的吧。這也是咱們從代碼上看到的,每個屏幕刷新信號來的時候,只會去執行一次 performTraversals(),由於只需遍歷一遍,就可以刷新全部的 View 了。

performTraversals() 會被執行的前提是調用了 scheduleTraversals() 來向底層註冊監聽了下一個屏幕刷新信號事件,因此在同一個 16.6ms 的一幀內,只須要第一個發起刷新請求的 View 來走一遍 scheduleTraversals() 乾的事就能夠了,其餘無論還有多少 View 發起了刷新請求,不必再去重複向底層註冊監聽下一個屏幕刷新信號事件了,反正只要有一次遍歷繪製 View 樹的操做就能夠對它們進行刷新了。

postSyncBarrier()---同步屏障消息

還剩最後一個問題,scheduleTraversals() 裏咱們還有一行代碼沒分析。這個問題是這樣的:

咱們清楚主線程實際上是一直在處理 MessageQueue 消息隊列裏的 Message,每一個操做都是一個 Message,打開 Activity 是一個 Message,遍歷繪製 View 樹來刷新屏幕也是一個 Message。

並且,上面梳理完咱們也清楚,遍歷繪製 View 樹的操做是在屏幕刷新信號到的時候,底層回調咱們 app 的 onVsync(),這個方法再去將遍歷繪製 View 樹的操做 post 到主線程的 MessageQueue 中去等待執行。主線程同一時間只能處理一個 Message,這些 Message 就確定有前後的問題,那麼會不會出現下面這種狀況呢:

同步分隔欄.png

也就是說,當咱們的 app 接收到屏幕刷新信號時,來不及第一時間就去執行刷新屏幕的操做,這樣一來,即便咱們將佈局優化得很完全,保證繪製當前 View 樹不會超過 16ms,但若是不能第一時間優先處理繪製 View 的工做,那等 16.6 ms 過了,底層須要去切換下一幀的畫面了,咱們 app 卻還沒處理完,這樣也照樣會出現丟幀了吧。並且這種場景是很是有可能出現的吧,畢竟主線程須要處理的事確定不只僅是刷新屏幕的事而已,那麼這個問題是怎麼處理的呢?

因此咱們繼續回來看 scheduleTraversals()

ViewRootImpl#scheduleTraversals3
ViewRootImpl#doTraversal2

在邏輯走進 Choreographer 前會先往隊列裏發送一個同步屏障,而當 doTraversal() 被調用時纔將同步屏障移除。這個同步屏障又涉及到消息機制了,不深刻了,這裏就只給出結論。

這個同步屏障的做用能夠理解成攔截同步消息的執行,主線程的 Looper 會一直循環調用 MessageQueue 的 next() 來取出隊頭的 Message 執行,當 Message 執行完後再去取下一個。當 next() 方法在取 Message 時發現隊頭是一個同步屏障的消息時,就會去遍歷整個隊列,只尋找設置了異步標誌的消息,若是有找到異步消息,那麼就取出這個異步消息來執行,不然就讓 next() 方法陷入阻塞狀態。若是 next() 方法陷入阻塞狀態,那麼主線程此時就是處於空閒狀態的,也就是沒在幹任何事。因此,若是隊頭是一個同步屏障的消息的話,那麼在它後面的全部同步消息就都被攔截住了,直到這個同步屏障消息被移除出隊列,不然主線程就一直不會去處理同步屏幕後面的同步消息。

而全部消息默認都是同步消息,只有手動設置了異步標誌,這個消息纔會是異步消息。另外,同步屏障消息只能由內部來發送,這個接口並無公開給咱們使用。

最後,仔細看上面 Choreographer 裏全部跟 message 有關的代碼,你會發現,都手動設置了異步消息的標誌,因此這些操做是不受到同步屏障影響的。這樣作的緣由可能就是爲了儘量保證上層 app 在接收到屏幕刷新信號時,能夠在第一時間執行遍歷繪製 View 樹的工做。

由於主線程中若是有太多消息要執行,而這些消息又是根據時間戳進行排序,若是不加一個同步屏障的話,那麼遍歷繪製 View 樹的工做就可能被迫延遲執行,由於它也須要排隊,那麼就有可能出現當一幀都快結束的時候纔開始計算屏幕數據,那即便此次的計算少於 16.6ms,也一樣會形成丟幀現象。

那麼,有了同步屏障消息的控制就能保證每次一接收到屏幕刷新信號就第一時間處理遍歷繪製 View 樹的工做麼?

只能說,同步屏障是儘量去作到,但並不能保證必定能夠第一時間處理。由於,同步屏障是在 scheduleTraversals() 被調用時才發送到消息隊列裏的,也就是說,只有當某個 View 發起了刷新請求時,在這個時刻後面的同步消息纔會被攔截掉。若是在 scheduleTraversals() 以前就發送到消息隊列裏的工做仍然會按順序依次被取出來執行。

界面刷新控制者--ViewRootImpl

最後,就是上文常常說的一點,全部跟界面刷新相關的操做,其實最終都會走到 ViewRootImpl 中的 scheduleTraversals() 去的。

大夥能夠想一想,跟界面刷新有關的操做有哪些,大概就是下面幾種場景吧:

  1. invalidate(請求重繪)
  2. requestLayout(從新佈局)
  3. requestFocus(請求焦點)
  4. startActivity(打開新界面)
  5. onRestart(從新打開界面)
  6. KeyEvent(遙控器事件,本質上是焦點致使的刷新)
  7. Animation(各類動畫,本質上是請求重繪致使的刷新)
  8. RecyclerView滑動(頁面滑動,本質上是動畫致使的刷新)
  9. setAdapter(各類adapter的更新)
  10. ...

在上一篇分析動畫的博客裏,咱們跟蹤了 invalidate(),確實也是這樣,至於其餘的我並無一一去驗證,大夥有興趣能夠看看,我猜想,這些跟界面刷新有關的方法內部要麼就是一個 do{}while() 循環尋找 mParent,要麼就是直接不斷的調用 mParent 的方法。而一顆 View 樹最頂端的 mParent 就是 ViewRootImpl,因此這些跟界面刷新相關的方法,在 ViewRootImpl 確定也是能夠找到的:

ViewRootImpl#requestChildFocus

ViewRootImpl#clearChildFocus

ViewRootImpl#requestLayout

其實,之前我一直覺得若是界面上某個小小的 View 發起了 invalidate() 重繪之類的操做,那麼應該就只是它本身的 onLayout(), onDraw() 被調用來重繪而已。最後才清楚,原來,即便再小的 View,若是發起了重繪的請求,那麼也須要先層層走到 ViewRootImpl 裏去,並且還不是立刻就執行重繪操做,而是須要等待下一個屏幕刷新信號來的時候,再從 DecorView 開始層層遍歷到這些須要刷新的 View 裏去重繪它們。

總結

本篇篇幅確實很長,由於這部份內容要理清楚不容易,要講清楚更不容易,大夥若是有時間,能夠靜下心來慢慢看,從頭看下來,我相信,多少會有些收穫的。若是沒時間,那麼也能夠直接看看總結。

  1. 界面上任何一個 View 的刷新請求最終都會走到 ViewRootImpl 中的 scheduleTraversals() 裏來安排一次遍歷繪製 View 樹的任務;
  2. scheduleTraversals() 會先過濾掉同一幀內的重複調用,在同一幀內只須要安排一次遍歷繪製 View 樹的任務便可,這個任務會在下一個屏幕刷新信號到來時調用 performTraversals() 遍歷 View 樹,遍歷過程當中會將全部須要刷新的 View 進行重繪;
  3. 接着 scheduleTraversals() 會往主線程的消息隊列中發送一個同步屏障,攔截這個時刻以後全部的同步消息的執行,但不會攔截異步消息,以此來儘量的保證當接收到屏幕刷新信號時能夠儘量第一時間處理遍歷繪製 View 樹的工做;
  4. 發完同步屏障後 scheduleTraversals() 纔會開始安排一個遍歷繪製 View 樹的操做,做法是把 performTraversals() 封裝到 Runnable 裏面,而後調用 Choreographer 的 postCallback() 方法;
  5. postCallback() 方法會先將這個 Runnable 任務以當前時間戳放進一個待執行的隊列裏,而後若是當前是在主線程就會直接調用一個native 層方法,若是不是在主線程,會發一個最高優先級的 message 到主線程,讓主線程第一時間調用這個 native 層的方法;
  6. native 層的這個方法是用來向底層註冊監聽下一個屏幕刷新信號,當下一個屏幕刷新信號發出時,底層就會回調 Choreographer 的onVsync() 方法來通知上層 app;
  7. onVsync() 方法被回調時,會往主線程的消息隊列中發送一個執行 doFrame() 方法的消息,這個消息是異步消息,因此不會被同步屏障攔截住;
  8. doFrame() 方法會去取出以前放進待執行隊列裏的任務來執行,取出來的這個任務其實是 ViewRootImpl 的 doTraversal() 操做;
  9. 上述第4步到第8步涉及到的消息都手動設置成了異步消息,因此不會受到同步屏障的攔截;
  10. doTraversal() 方法會先移除主線程的同步屏障,而後調用 performTraversals() 開始根據當前狀態判斷是否須要執行performMeasure() 測量、perfromLayout() 佈局、performDraw() 繪製流程,在這幾個流程中都會去遍歷 View 樹來刷新須要更新的View;

再來一張時序圖結尾,大夥想本身過源碼時能夠跟着時序圖來,建議在電腦上閱讀:

View刷新流程時序圖.png

QA

Q1:Android 每隔 16.6 ms 刷新一次屏幕到底指的是什麼意思?是指每隔 16.6ms 調用 onDraw() 繪製一次麼?
Q2:若是界面一直保持沒變的話,那麼還會每隔 16.6ms 刷新一次屏幕麼?
答:咱們常說的 Android 每隔 16.6 ms 刷新一次屏幕實際上是指底層會以這個固定頻率來切換每一幀的畫面,而這個每一幀的畫面數據就是咱們 app 在接收到屏幕刷新信號以後去執行遍歷繪製 View 樹工做所計算出來的屏幕數據。而 app 並非每隔 16.6ms 的屏幕刷新信號均可以接收到,只有當 app 向底層註冊監聽下一個屏幕刷新信號以後,才能接收到下一個屏幕刷新信號到來的通知。而只有當某個 View 發起了刷新請求時,app 纔會去向底層註冊監聽下一個屏幕刷新信號。

也就是說,只有當界面有刷新的須要時,咱們 app 纔會在下一個屏幕刷新信號來時,遍歷繪製 View 樹來從新計算屏幕數據。若是界面沒有刷新的須要,一直保持不變時,咱們 app 就不會去接收每隔 16.6ms 的屏幕刷新信號事件了,但底層仍然會以這個固定頻率來切換每一幀的畫面,只是後面這些幀的畫面都是相同的而已。

Q3:界面的顯示其實就是一個 Activity 的 View 樹裏全部的 View 都進行測量、佈局、繪製操做以後的結果呈現,那麼若是這部分工做都完成後,屏幕會立刻就刷新麼?
答:咱們 app 只負責計算屏幕數據而已,接收到屏幕刷新信號就去計算,計算完畢就計算完畢了。至於屏幕的刷新,這些是由底層以固定的頻率來切換屏幕每一幀的畫面。因此即便屏幕數據都計算完畢,屏幕會不會立刻刷新就取決於底層是否到了要切換下一幀畫面的時機了。

Q4:網上都說避免丟幀的方法之一是保證每次繪製界面的操做要在 16.6ms 內完成,但若是這個 16.6ms 是一個固定的頻率的話,請求繪製的操做在代碼裏被調用的時機是不肯定的啊,那麼若是某次用戶點擊屏幕致使的界面刷新操做是在某一個 16.6ms 幀快結束的時候,那麼即便此次繪製操做小於 16.6 ms,按道理不也會形成丟幀麼?這又該如何理解?
答:之因此提了這個問題,是由於以前是覺得若是某個 View 發起了刷新請求,好比調用了 invalidte(),那麼它的重繪工做就立刻開始執行了,因此之前在看網上那些介紹屏幕刷新機制的博客時,常常看見下面這張圖:

image.png

那個時候就是不大理解,爲何每一次 CPU 計算的工做都剛恰好是在每個信號到來的那個瞬間開始的呢?畢竟代碼裏發起刷新屏幕的操做是動態的,不可能每次都剛恰好那麼巧。

梳理完屏幕刷新機制後就清楚了,代碼裏調用了某個 View 發起的刷新請求,這個重繪工做並不會立刻就開始,而是須要等到下一個屏幕刷新信號來的時候纔開始,因此如今回過頭來看這些圖就清楚多了。

Q5:大夥都清楚,主線程耗時的操做會致使丟幀,可是耗時的操做爲何會致使丟幀?它是如何致使丟幀發生的?
答:形成丟幀大致上有兩類緣由,一是遍歷繪製 View 樹計算屏幕數據的時間超過了 16.6ms;二是,主線程一直在處理其餘耗時的消息,致使遍歷繪製 View 樹的工做遲遲不能開始,從而超過了 16.6 ms 底層切換下一幀畫面的時機。

第一個緣由就是咱們寫的佈局有問題了,須要進行優化了。而第二個緣由則是咱們常說的避免在主線程中作耗時的任務。

針對第二個緣由,系統已經引入了同步屏障消息的機制,儘量的保證遍歷繪製 View 樹的工做可以及時進行,但仍沒辦法徹底避免,因此咱們仍是得儘量避免主線程耗時工做。

其實第二個緣由,能夠拿出來細講的,好比有這種狀況, message 不怎麼耗時,但數量太多,這一樣可能會形成丟幀。若是有使用一些圖片框架的,它內部下載圖片都是開線程去下載,但當下載完成後須要把圖片加載到綁定的 view 上,這個工做就是發了一個 message 切到主線程來作,若是一個界面這種 view 特別多的話,隊列裏就會有很是多的 message,雖然每一個都 message 並不怎麼耗時,但經不起量多啊。後面有時間的話,看看要不要專門整理一篇文章來說卡頓和丟幀的事。

推薦閱讀(大神博客)

破譯Android性能優化中的16ms問題 android屏幕刷新顯示機制 Android Choreographer 源碼分析


QQ圖片20180316094923.jpg
最近剛開通了公衆號,想激勵本身堅持寫做下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的能夠點一波關注,謝謝支持~~
相關文章
相關標籤/搜索