基本概念
首先,先來過一下一些基本概念,摘抄自網上文章android屏幕刷新顯示機制:
在一個典型的顯示系統中,一般包括CPU、GPU、display三個部分, CPU負責計算數據,把計算好數據交給GPU,GPU會對圖形數據進行渲染,渲染好后放到buffer里存起來,然后display(有的文章也叫屏幕或者顯示器)負責把buffer里的數據呈現到屏幕上。
顯示過程,簡單的說就是CPU/GPU準備好數據,存入buffer,display每隔一段時間去buffer里取數據,然后顯示出來。display讀取的頻率是固定的,比如每個16ms讀一次,但是CPU/GPU寫數據是完全無規律的。
上述內容概括一下,大體意思就是說,屏幕的刷新包括三個步驟:CPU 計算屏幕數據、GPU 進一步處理和緩存、最后 display 再將緩存中(buffer)的屏幕數據顯示出來。
對于 Android 而言,第一個步驟:CPU 計算屏幕數據指的也就是 View 樹的繪制過程,也就是 Activity 對應的視圖樹從根布局 DecorView 開始層層遍歷每個 View,分別執行測量、布局、繪制三個操作的過程。
Display 這一行可以理解成屏幕,所以可以看到,底層是以固定的頻率發出 VSync 信號的,而這個固定頻率就是我們常說的每 16.6ms 發送一個 VSync 信號,至于什么叫 VSync 信號,我們可以不用深入去了解,只要清楚這個信號就是屏幕刷新的信號就可以了。
看圖,CPU 藍色的這行,CPU 這塊的耗時其實就是我們 app 繪制當前 View 樹的時間,而這段時間就跟我們自己寫的代碼有關系了,如果你的布局很復雜,層次嵌套很多,每一幀內需要刷新的 View 又很多時,那么每一幀的繪制耗時自然就會多一點。
看圖,CPU 藍色這行里也有一些數字,其實這些數字跟 Display 黃色的那一行里的數字是對應的,在 Display 里這些數字表示的是每一幀的畫面,那么在 CPU 這一行里,其實就是在計算對應幀的畫面數據,也叫屏幕數據。也就是說,在當前幀內,CPU 是在計算下一幀的屏幕畫面數據,當屏幕刷新信號到的時候,屏幕就去將 CPU 計算的屏幕畫面數據顯示出來;同時 CPU 也接收到屏幕刷新信號,所以也開始去計算下一幀的屏幕畫面數據。
CPU 跟 Display 是不同的硬件,它們是可以并行工作的。要理解的一點是,我們寫的代碼,只是控制讓 CPU 在接收到屏幕刷新信號的時候開始去計算下一幀的畫面工作。而底層在每一次屏幕刷新信號來的時候都會去切換這一幀的畫面,這點我們是控制不了的,是底層的工作機制。
當我們的 app 界面沒有必要再刷新時(比如用戶不操作了,當前界面也沒動畫),這個時候,我們 app 是接收不到屏幕刷新信號的,所以也就不會讓 CPU 去計算下一幀畫面數據,但是底層仍然會以固定的頻率來切換每一幀的畫面,只是它后面切換的每一幀畫面都一樣,所以給我們的感覺就是屏幕沒刷新。
ViewRootImpl 和DecorView的綁定
分析 View#invalidate()
時,也可以看到內部其實是有一個 do{}while() 循環來不斷尋找 mParent,所以最終才會走到 ViewRootImpl 里去,那么可能大伙就會疑問了,為什么 DecorView 的 mParent 會是 ViewRootImpl 呢?換個問法也就是,在什么時候將 DevorView 和 ViewRootImpl 綁定起來?
Activity 的啟動是在 ActivityThread 里完成的,handleLaunchActivity()
會依次間接的執行到 Activity 的 onCreate()
, onStart()
, onResume()
。在執行完這些后 ActivityThread 會調用 WindowManager#addView()
,而這個 addView()
最終其實是調用了 WindowManagerGlobal 的 addView()
方法。
這里初始化了一個 ViewRootImpl,然后調用了它的 setView()
方法,將 DevorView 作為參數傳遞了進去,這個方法里還調用了一個 requestLayout()
方法,進而又調用了一個 scheduleTraversals()
。
在 setView()
方法里調用了 DecorView 的 assignParent()
方法,參數是 ViewParent,而 ViewRootImpl 是實現了 ViewParent 接口的,所以在這里就將 DecorView 和 ViewRootImpl 綁定起來了。
每個Activity 的根布局都是 DecorView,而 DecorView 的 parent 又是 ViewRootImpl,所以在子 View 里執行 invalidate()
之類的操作,循環找 parent 時,最后都會走到 ViewRootImpl 里來。
即使是界面上一個小小的 View 發起了重繪請求時,都要層層走到 ViewRootImpl,由它來發起重繪請求,然后再由它來開始遍歷 View 樹,一直遍歷到這個需要重繪的 View 再調用它的 onDraw()
方法進行繪制。
總結一下:其實打開一個 Activity,當它的 onCreate---onResume 生命周期都走完后,才將它的 DecoView 與新建的一個 ViewRootImpl 對象綁定起來,同時開始安排一次遍歷 View 任務也就是繪制 View 樹的操作等待執行,然后將 DecoView 的 parent 設置成 ViewRootImpl 對象。
ViewRootImpl#scheduleTraversals
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//往主線程的消息隊列中發送一個同步屏障,攔截這個時刻之后所有的同步消息的執行,但不會攔截異步消息,以此來盡可能的保證當接收到屏幕刷新信號時可以盡可能第一時間處理遍歷繪制 View 樹的工作;
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);//把 performTraversals() 封裝到 Runnable 里面,然后調用 Choreographer 的 postCallback() 方法;postCallback() 方法會先將這個 Runnable 任務以當前時間戳放進一個待執行的隊列里,然后如果當前是在主線程就會直接調用一個native 層方法,如果不是在主線程,會發一個最高優先級的 message 到主線程,讓主線程第一時間調用這個 native 層的方法;
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
- 界面上任何一個 View 的刷新請求最終都會走到 ViewRootImpl 中的 scheduleTraversals() 里來安排一次遍歷繪制 View 樹的任務;
- scheduleTraversals() 會先過濾掉同一幀內的重復調用,在同一幀內只需要安排一次遍歷繪制 View 樹的任務即可,這個任務會在下一個屏幕刷新信號到來時調用 performTraversals() 遍歷 View 樹,遍歷過程中會將所有需要刷新的 View 進行重繪;
- 接著 scheduleTraversals() 會往主線程的消息隊列中發送一個同步屏障,攔截這個時刻之后所有的同步消息的執行,但不會攔截異步消息,以此來盡可能的保證當接收到屏幕刷新信號時可以盡可能第一時間處理遍歷繪制 View 樹的工作;
- 發完同步屏障后 scheduleTraversals() 才會開始安排一個遍歷繪制 View 樹的操作,作法是把 performTraversals() 封裝到 Runnable 里面,然后調用 Choreographer 的 postCallback() 方法;
- postCallback() 方法會先將這個 Runnable 任務以當前時間戳放進一個待執行的隊列里,然后如果當前是在主線程就會直接調用一個native 層方法,如果不是在主線程,會發一個最高優先級的 message 到主線程,讓主線程第一時間調用這個 native 層的方法;
- native 層的這個方法是用來向底層注冊監聽下一個屏幕刷新信號,當下一個屏幕刷新信號發出時,底層就會回調 Choreographer 的onVsync() 方法來通知上層 app;
- onVsync() 方法被回調時,會往主線程的消息隊列中發送一個執行 doFrame() 方法的消息,這個消息是異步消息,所以不會被同步屏障攔截住;
- doFrame() 方法會去取出之前放進待執行隊列里的任務來執行,取出來的這個任務實際上是 ViewRootImpl 的 doTraversal() 操作;
- 上述第4步到第8步涉及到的消息都手動設置成了異步消息,所以不會受到同步屏障的攔截;
- doTraversal() 方法會先移除主線程的同步屏障,然后調用 performTraversals() 開始根據當前狀態判斷是否需要執行performMeasure() 測量、perfromLayout() 布局、performDraw() 繪制流程,在這幾個流程中都會去遍歷 View 樹來刷新需要更新的View;