[TOC]
# VSync
在一個典型的顯示系統中,一般包括CPU、GPU、Display三個部分, CPU負責計算幀數據,把計算好的數據交給GPU,GPU會對圖形數據進行渲染,渲染好后放到buffer(圖像緩沖區)里存起來,然后Display(屏幕或顯示器)負責把buffer里的數據呈現到屏幕上。如下圖:

## 基礎概念
* **屏幕刷新頻率**
一秒內屏幕刷新的次數(一秒內顯示了多少幀的圖像),單位 Hz(赫茲),如常見的 60 Hz。**刷新頻率取決于硬件的固定參數**(不會變的)。
* **逐行掃描**
顯示器并不是一次性將畫面顯示到屏幕上,而是從左到右邊,從上到下逐行掃描,順序顯示整屏的一個個像素點,不過這一過程快到人眼無法察覺到變化。以 60 Hz 刷新率的屏幕為例,這一過程即 1000 / 60 ≈ 16ms。
* **幀率** (Frame Rate)
表示 **GPU 在一秒內繪制操作的幀數**,單位 fps。例如在電影界采用 24 幀的速度足夠使畫面運行的非常流暢。而 Android 系統則采用更加流程的 60 fps,即每秒鐘GPU最多繪制 60 幀畫面。幀率是動態變化的,例如當畫面靜止時,GPU 是沒有繪制操作的,屏幕刷新的還是buffer中的數據,即GPU最后操作的幀數據。
* **畫面撕裂**(tearing)
一個屏幕內的數據來自2個不同的幀,畫面會出現撕裂感,如下圖

## 雙緩存
### 畫面撕裂 原因
屏幕刷新頻是固定的,比如每16.6ms從buffer取數據顯示完一幀,理想情況下幀率和刷新頻率保持一致,即每繪制完成一幀,顯示器顯示一幀。但是CPU/GPU寫數據是不可控的,所以會出現buffer里有些數據根本沒顯示出來就被重寫了,即buffer里的數據可能是來自不同的幀的, 當屏幕刷新時,此時它并不知道buffer的狀態,因此從buffer抓取的幀并不是完整的一幀畫面,即出現畫面撕裂。
簡單說就是Display在顯示的過程中,buffer內數據被CPU/GPU修改,導致畫面撕裂。
### 雙緩存
那咋解決畫面撕裂呢? 答案是使用 雙緩存。
由于圖像繪制和屏幕讀取 使用的是同個buffer,所以屏幕刷新時可能讀取到的是不完整的一幀畫面。
**雙緩存**,讓繪制和顯示器擁有各自的buffer:GPU 始終將完成的一幀圖像數據寫入到 **Back Buffer**,而顯示器使用 **Frame Buffer**,當屏幕刷新時,Frame Buffer 并不會發生變化,當Back buffer準備就緒后,它們才進行交換。如下圖: 
### VSync
問題又來了:什么時候進行兩個buffer的交換呢?
假如是 Back buffer準備完成一幀數據以后就進行,那么如果此時屏幕還沒有完整顯示上一幀內容的話,肯定是會出問題的。看來只能是等到屏幕處理完一幀數據后,才可以執行這一操作了。
當掃描完一個屏幕后,設備需要重新回到第一行以進入下一次的循環,此時有一段時間空隙,稱為VerticalBlanking Interval(VBI)。那,這個時間點就是我們進行緩沖區交換的最佳時間。因為此時屏幕沒有在刷新,也就避免了交換過程中出現 screen tearing的狀況。
**VSync**(垂直同步)是VerticalSynchronization的簡寫,它利用VBI時期出現的vertical sync pulse(垂直同步脈沖)來保證雙緩沖在最佳時間點才進行交換。另外,交換是指各自的內存地址,可以認為該操作是瞬間完成。
所以說V-sync這個概念并不是Google首創的,它在早年的PC機領域就已經出現了。
# Android屏幕刷新機制
## Android4.1之前的問題
具體到Android中,在Android4.1之前,屏幕刷新也遵循 上面介紹的 雙緩存+VSync 機制。如下圖: 
以時間的順序來看下將會發生的過程:
1. Display顯示第0幀數據,此時CPU和GPU渲染第1幀畫面,且在Display顯示下一幀前完成
2. 因為渲染及時,Display在第0幀顯示完成后,也就是第1個VSync后,緩存進行交換,然后正常顯示第1幀
3. 接著第2幀開始處理,是直到第2個VSync快來前才開始處理的。
4. 第2個VSync來時,由于第2幀數據還沒有準備就緒,緩存沒有交換,顯示的還是第1幀。這種情況被Android開發組命名為“Jank”,即發生了**丟幀**。
5. 當第2幀數據準備完成后,它并不會馬上被顯示,而是要等待下一個VSync 進行緩存交換再顯示。
所以總的來說,就是屏幕平白無故地多顯示了一次第1幀。
原因是 第2幀的CPU/GPU計算 沒能在VSync信號到來前完成 。
我們知道,**雙緩存的交換 是在Vsyn到來時進行,交換后屏幕會取Frame buffer內的新數據,而實際 此時的Back buffer 就可以供GPU準備下一幀數據了。 如果 Vsyn到來時 CPU/GPU就開始操作的話,是有完整的16.6ms的,這樣應該會基本避免jank的出現了**(除非CPU/GPU計算超過了16.6ms)。 那如何讓 CPU/GPU計算在 Vsyn到來時進行呢?
## drawing with VSync
為了優化顯示性能,Google在Android 4.1系統中對Android Display系統進行了重構,實現了Project Butter(黃油工程):系統在收到VSync pulse后,將馬上開始下一幀的渲染。即**一旦收到VSync通知(16ms觸發一次),CPU和GPU 才立刻開始計算然后把數據寫入buffer**。如下圖:

CPU/GPU根據VSYNC信號同步處理數據,可以讓CPU/GPU有完整的16ms時間來處理數據,減少了jank。
一句話總結,**VSync同步使得CPU/GPU充分利用了16.6ms時間,減少jank。**
問題又來了,如果界面比較復雜,CPU/GPU的處理時間較長 超過了16.6ms呢?如下圖:

1. 在第二個時間段內,但卻因 GPU 還在處理 B 幀,緩存沒能交換,導致 A 幀被重復顯示。
2. 而B完成后,又因為缺乏VSync pulse信號,它只能等待下一個signal的來臨。于是在這一過程中,有一大段時間是被浪費的。
3. 當下一個VSync出現時,CPU/GPU馬上執行操作(A幀),且緩存交換,相應的顯示屏對應的就是B。這時看起來就是正常的。只不過由于執行時間仍然超過16ms,導致下一次應該執行的緩沖區交換又被推遲了——如此循環反復,便出現了越來越多的“Jank”。
**為什么 CPU 不能在第二個 16ms 處理繪制工作呢?**
原因是只有兩個 buffer,Back buffer正在被GPU用來處理B幀的數據, Frame buffer的內容用于Display的顯示,這樣兩個buffer都被占用,CPU 則無法準備下一幀的數據。 那么,如果再提供一個buffer,CPU、GPU 和顯示設備都能使用各自的buffer工作,互不影響。
## 三緩存
**三緩存**就是在雙緩沖機制基礎上增加了一個 Graphic Buffer 緩沖區,這樣可以最大限度的利用空閑時間,帶來的壞處是多使用的一個 Graphic Buffer 所占用的內存。

1. 第一個Jank,是不可避免的。但是在第二個 16ms 時間段,CPU/GPU 使用 **第三個 Buffer** 完成C幀的計算,雖然還是會多顯示一次 A 幀,但后續顯示就比較順暢了,有效避免 Jank 的進一步加劇。
2. 注意在第3段中,A幀的計算已完成,但是在第4個vsync來的時候才顯示,如果是雙緩沖,那在第三個vynsc就可以顯示了。
**三緩沖有效利用了等待vysnc的時間,減少了jank,但是帶來了延遲。** 所以,是不是 Buffer 越多越好呢?這個是否定的,Buffer 正常還是兩個,當出現 Jank 后三個足以。
以上就是Android屏幕刷新的原理了。
# Choreographer
## 概述
上面講到,Google在Android 4.1系統中對Android Display系統進行了優化:在收到VSync pulse后,將馬上開始下一幀的渲染。即**一旦收到VSync通知,CPU和GPU就立刻開始計算然后把數據寫入buffer**。本節就來講 "drawing with VSync" 的實現——**Choreographer**。
* Choreographer,意為 舞蹈編導、編舞者。在這里就是指 對CPU/GPU繪制的指導—— 收到VSync信號 才開始繪制,保證繪制擁有完整的16.6ms,避免繪制的隨機性。
* Choreographer,是一個Java類,包路徑android.view.Choreographer。類注釋是“協調動畫、輸入和繪圖的計時”。
* 通常 應用層不會直接使用Choreographer,而是使用更高級的API,例如動畫和View繪制相關的ValueAnimator.start()、View.invalidate()等。
* 業界一般通過Choreographer來監控應用的幀率。
## 源碼分析
學習 Choreographer 可以幫助理解 每幀運行的原理,也可加深對 Handler機制、View繪制流程的理解,這樣再去做UI優化、卡頓優化,思路會更清晰。
好了,下面開始源碼分析了~
### 入口 和 實例創建
在[《Window和WindowManager》](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fhfy8971613%2Farticle%2Fdetails%2F103241153 "https://blog.csdn.net/hfy8971613/article/details/103241153")、[《Activity的啟動過程詳解》](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fhfy8971613%2Farticle%2Fdetails%2F107201238 "https://blog.csdn.net/hfy8971613/article/details/107201238")中介紹過,Activity啟動 走完onResume方法后,會進行**window的添加**。window添加過程會 調用ViewRootImpl的setView()方法,setView()方法會調用requestLayout()方法來請求繪制布局,requestLayout()方法內部又會走到scheduleTraversals()方法,最后會走到performTraversals()方法,接著到了我們熟知的測量、布局、繪制三大流程了。
另外,查看源碼發現,當我們使用 ValueAnimator.start()、View.invalidate()時,最后也是走到ViewRootImpl的scheduleTraversals()方法。(View.invalidate()內部會循環獲取ViewParent直到ViewRootImpl的invalidateChildInParent()方法,然后走到scheduleTraversals(),可自行查看源碼 )
即 **所有UI的變化都是走到ViewRootImpl的scheduleTraversals()方法。**
那么問題又來了,scheduleTraversals() 到 performTraversals() 中間 經歷了什么呢?是立刻執行嗎?答案很顯然是否定的,根據我們上面的介紹,在VSync信號到來時才會執行繪制,即performTraversals()方法。 下面來瞅瞅這是如何實現的:
~~~java
//ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
//此字段保證同時間多次更改只會刷新一次,例如TextView連續兩次setText(),也只會走一次繪制流程
mTraversalScheduled = true;
//添加同步屏障,屏蔽同步消息,保證VSync到來立即執行繪制
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//mTraversalRunnable是TraversalRunnable實例,最終走到run(),也即doTraversal();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
...
//開始三大繪制流程
performTraversals();
...
}
}
復制代碼
~~~
主要有以下邏輯:
1. 首先使用mTraversalScheduled字段保證同時間多次更改只會刷新一次,例如TextView連續兩次setText(),也只會走一次繪制流程。
2. 然后把當前線程的消息隊列Queue添加了**同步屏障**,這樣就屏蔽了正常的同步消息,保證VSync到來后立即執行繪制,而不是要等前面的同步消息。后面會具體分析同步屏障和異步消息的代碼邏輯。
3. 調用了mChoreographer.postCallback()方法,發送一個會在下一幀執行的回調,即**在下一個VSync到來時會執行TraversalRunnable-->doTraversal()--->performTraversals()-->繪制流程**。
接下來,就是分析的重點——Choreographer。我們先看它的實例mChoreographer,是在ViewRootImpl的構造方法內使用Choreographer.getInstance()創建:
~~~java
Choreographer mChoreographer;
//ViewRootImpl實例是在添加window時創建
public ViewRootImpl(Context context, Display display) {
...
mChoreographer = Choreographer.getInstance();
...
}
復制代碼
~~~
我們先來看看Choreographer.getInstance():
~~~java
public static Choreographer getInstance() {
return sThreadInstance.get();
}
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
//當前線程要有looper,Choreographer實例需要傳入
throw new IllegalStateException("The current thread must have a looper!");
}
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};
復制代碼
~~~
看到這里 如你對[Handler機制](https://link.juejin.cn?target=https%3A%2F%2Fblog.csdn.net%2Fhfy8971613%2Farticle%2Fdetails%2F103881609 "https://blog.csdn.net/hfy8971613/article/details/103881609")中looper比較熟悉的話,應該知道 Choreographer和Looper一樣 是線程單例的。且當前線程要有looper,Choreographer實例需要傳入。接著看看Choreographer構造方法:
~~~java
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
//使用當前線程looper創建 mHandler
mHandler = new FrameHandler(looper);
//USE_VSYNC 4.1以上默認是true,表示 具備接受VSync的能力,這個接受能力就是FrameDisplayEventReceiver
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;
// 計算一幀的時間,Android手機屏幕是60Hz的刷新頻率,就是16ms
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
// 創建一個鏈表類型CallbackQueue的數組,大小為5,
//也就是數組中有五個鏈表,每個鏈表存相同類型的任務:輸入、動畫、遍歷繪制等任務(CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL)
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// b/68769804: For low FPS experiments.
setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}
復制代碼
~~~
代碼中都有注釋,創建了一個mHandler、VSync事件接收器mDisplayEventReceiver、任務鏈表數組mCallbackQueues。FrameHandler、FrameDisplayEventReceiver、CallbackQueue后面會一一說明。
### 安排任務—postCallback
回頭看mChoreographer.postCallback(Choreographer.CALLBACK\_TRAVERSAL, mTraversalRunnable, null)方法,注意到第一個參數是CALLBACK\_TRAVERSAL,表示回調任務的類型,共有以下5種類型:
~~~java
//輸入事件,首先執行
public static final int CALLBACK_INPUT = 0;
//動畫,第二執行
public static final int CALLBACK_ANIMATION = 1;
//插入更新的動畫,第三執行
public static final int CALLBACK_INSETS_ANIMATION = 2;
//繪制,第四執行
public static final int CALLBACK_TRAVERSAL = 3;
//提交,最后執行,
public static final int CALLBACK_COMMIT = 4;
復制代碼
~~~
五種類型任務對應存入對應的CallbackQueue中,每當收到 VSYNC 信號時,Choreographer 將首先處理 INPUT 類型的任務,然后是 ANIMATION 類型,最后才是 TRAVERSAL 類型。
postCallback()內部調用postCallbackDelayed(),接著又調用postCallbackDelayedInternal(),來瞅瞅:
~~~java
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
...
synchronized (mLock) {
// 當前時間
final long now = SystemClock.uptimeMillis();
// 加上延遲時間
final long dueTime = now + delayMillis;
//取對應類型的CallbackQueue添加任務
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//立即執行
scheduleFrameLocked(now);
} else {
//延遲運行,最終也會走到scheduleFrameLocked()
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
復制代碼
~~~
首先取對應類型的CallbackQueue添加任務,action就是mTraversalRunnable,token是null。**CallbackQueue的addCallbackLocked()就是把 dueTime、action、token組裝成CallbackRecord后 存入CallbackQueue的下一個節點**,具體代碼比較簡單,不再跟進。
然后注意到如果沒有延遲會執行scheduleFrameLocked()方法,有延遲就會使用 mHandler發送MSG\_DO\_SCHEDULE\_CALLBACK消息,并且注意到 **使用msg.setAsynchronous(true)把消息設置成異步**,這是因為前面設置了同步屏障,只有異步消息才會執行。我們看下mHandler的對這個消息的處理:
~~~java
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
// 執行doFrame,即繪制過程
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
//申請VSYNC信號,例如當前需要繪制任務時
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK:
//需要延遲的任務,最終還是執行上述兩個事件
doScheduleCallback(msg.arg1);
break;
}
}
}
復制代碼
~~~
直接使用doScheduleCallback方法,看看:
~~~java
void doScheduleCallback(int callbackType) {
synchronized (mLock) {
if (!mFrameScheduled) {
final long now = SystemClock.uptimeMillis();
if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
scheduleFrameLocked(now);
}
}
}
}
復制代碼
~~~
發現也是走到這里,即延遲運行最終也會走到scheduleFrameLocked(),跟進看看:
~~~java
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
//開啟了VSYNC
if (USE_VSYNC) {
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame on vsync.");
}
//當前執行的線程,是否是mLooper所在線程
if (isRunningOnLooperThreadLocked()) {
//申請 VSYNC 信號
scheduleVsyncLocked();
} else {
// 若不在,就用mHandler發送消息到原線程,最后還是調用scheduleVsyncLocked方法
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);//異步
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
// 如果未開啟VSYNC則直接doFrame方法(4.1后默認開啟)
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
if (DEBUG_FRAMES) {
Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
}
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);//異步
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
復制代碼
~~~
1. 如果系統未開啟 VSYNC 機制,此時直接發送 MSG\_DO\_FRAME 消息到 FrameHandler。注意查看上面貼出的 FrameHandler 代碼,此時直接執行 doFrame 方法。
2. Android 4.1 之后系統默認開啟 VSYNC,在 Choreographer 的構造方法會創建一個 FrameDisplayEventReceiver,scheduleVsyncLocked 方法將會通過它申請 VSYNC 信號。
3. isRunningOnLooperThreadLocked 方法,其內部根據 Looper 判斷是否在原線程,否則發送消息到 FrameHandler。最終還是會調用 scheduleVsyncLocked 方法申請 VSYNC 信號。
到這里,**FrameHandler的作用很明顯里了:發送異步消息(因為前面設置了同步屏障)。有延遲的任務發延遲消息、不在原線程的發到原線程、沒開啟VSYNC的直接走 doFrame 方法取執行繪制。**
### 申請和接受VSync
好了, 接著就看 scheduleVsyncLocked 方法是如何申請 VSYNC 信號的。猜測肯定申請 VSYNC 信號后,信號到來時也是走doFrame() 方法,doFrame()后面再看。先跟進scheduleVsyncLocked():
~~~java
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
復制代碼
~~~
很簡單,調用mDisplayEventReceiver的scheduleVsync()方法,mDisplayEventReceiver是Choreographer構造方法中創建,是FrameDisplayEventReceiver 的實例。 FrameDisplayEventReceiver是 DisplayEventReceiver 的子類,DisplayEventReceiver 是一個 abstract class:
~~~java
public DisplayEventReceiver(Looper looper, int vsyncSource) {
if (looper == null) {
throw new IllegalArgumentException("looper must not be null");
}
mMessageQueue = looper.getQueue();
// 注冊VSYNC信號監聽者
mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
vsyncSource);
mCloseGuard.open("dispose");
}
復制代碼
~~~
在 DisplayEventReceiver 的構造方法會通過 JNI 創建一個 IDisplayEventConnection 的 VSYNC 的監聽者。
FrameDisplayEventReceiver的scheduleVsync()就是在 DisplayEventReceiver中:
~~~java
public void scheduleVsync() {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
+ "receiver has already been disposed.");
} else {
// 申請VSYNC中斷信號,會回調onVsync方法
nativeScheduleVsync(mReceiverPtr);
}
}
復制代碼
~~~
那么scheduleVsync()就是使用native方法nativeScheduleVsync()去申請VSYNC信號。這個native方法就看不了了,只需要知道**VSYNC信號的接受回調是onVsync()**,我們直接看onVsync():
~~~java
/**
* 接收到VSync脈沖時 回調
* @param timestampNanos VSync脈沖的時間戳
* @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
* @param frame 幀號碼,自增
*/
@UnsupportedAppUsage
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
}
復制代碼
~~~
具體實現是在FrameDisplayEventReceiver中:
~~~java
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
super(looper, vsyncSource);
}
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
// Post the vsync event to the Handler.
// The idea is to prevent incoming vsync events from completely starving
// the message queue. If there are no messages in the queue with timestamps
// earlier than the frame time, then the vsync event will be processed immediately.
// Otherwise, messages that predate the vsync event will be handled first.
long now = System.nanoTime();
if (timestampNanos > now) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
timestampNanos = now;
}
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
mTimestampNanos = timestampNanos;
mFrame = frame;
//將本身作為runnable傳入msg, 發消息后 會走run(),即doFrame(),也是異步消息
Message msg = Message.obtain(mHandler, this);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
復制代碼
~~~
onVsync()中,將接收器本身作為runnable傳入異步消息msg,并使用mHandler發送msg,最終執行的就是doFrame()方法了。
注意一點是,**onVsync()方法中只是使用mHandler發送消息到MessageQueue中,不一定是立刻執行,如何MessageQueue中前面有較為耗時的操作,那么就要等完成,才會執行本次的doFrame()**。
### doFrame
和上面猜測一樣,申請VSync信號接收到后確實是走 doFrame()方法,那么就來看看Choreographer的doFrame():
~~~java
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
...
// 預期執行時間
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
// 超時時間是否超過一幀的時間(這是因為MessageQueue雖然添加了同步屏障,但是還是有正在執行的同步任務,導致doFrame延遲執行了)
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
// 計算掉幀數
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
// 掉幀超過30幀打印Log提示
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
...
frameTimeNanos = startNanos - lastFrameOffset;
}
...
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
// Frame標志位恢復
mFrameScheduled = false;
// 記錄最后一幀時間
mLastFrameTimeNanos = frameTimeNanos;
}
try {
// 按類型順序 執行任務
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
復制代碼
~~~
上面都有注釋了很好理解,接著看任務的具體執行doCallbacks 方法:
~~~java
void doCallbacks(int callbackType, long frameTimeNanos) {
CallbackRecord callbacks;
synchronized (mLock) {
final long now = System.nanoTime();
// 根據指定的類型CallbackkQueue中查找到達執行時間的CallbackRecord
callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(now / TimeUtils.NANOS_PER_MS);
if (callbacks == null) {
return;
}
mCallbacksRunning = true;
//提交任務類型
if (callbackType == Choreographer.CALLBACK_COMMIT) {
final long jitterNanos = now - frameTimeNanos;
if (jitterNanos >= 2 * mFrameIntervalNanos) {
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
+ mFrameIntervalNanos;
if (DEBUG_JANK) {
Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
+ " ms which is more than twice the frame interval of "
+ (mFrameIntervalNanos * 0.000001f) + " ms! "
+ "Setting frame time to " + (lastFrameOffset * 0.000001f)
+ " ms in the past.");
mDebugPrintNextFrameTimeDelta = true;
}
frameTimeNanos = now - lastFrameOffset;
mLastFrameTimeNanos = frameTimeNanos;
}
}
}
try {
// 迭代執行隊列所有任務
for (CallbackRecord c = callbacks; c != null; c = c.next) {
// 回調CallbackRecord的run,其內部回調Callback的run
c.run(frameTimeNanos);
}
} finally {
synchronized (mLock) {
mCallbacksRunning = false;
do {
final CallbackRecord next = callbacks.next;
//回收CallbackRecord
recycleCallbackLocked(callbacks);
callbacks = next;
} while (callbacks != null);
}
}
}
復制代碼
~~~
主要內容就是取對應任務類型的隊列,遍歷隊列執行所有任務,執行任務是 CallbackRecord的 run 方法:
~~~java
private static final class CallbackRecord {
public CallbackRecord next;
public long dueTime;
public Object action; // Runnable or FrameCallback
public Object token;
@UnsupportedAppUsage
public void run(long frameTimeNanos) {
if (token == FRAME_CALLBACK_TOKEN) {
// 通過postFrameCallback 或 postFrameCallbackDelayed,會執行這里
((FrameCallback)action).doFrame(frameTimeNanos);
} else {
//取出Runnable執行run()
((Runnable)action).run();
}
}
}
復制代碼
~~~
前面看到mChoreographer.postCallback傳的token是null,所以取出action,就是Runnable,執行run(),這里的action就是 ViewRootImpl 發起的繪制任務mTraversalRunnable了,那么**這樣整個邏輯就閉環了**。
那么 啥時候 token == FRAME\_CALLBACK\_TOKEN 呢?答案是Choreographer的postFrameCallback()方法:
~~~java
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
//也是走到是postCallbackDelayedInternal,并且注意是CALLBACK_ANIMATION類型,
//token是FRAME_CALLBACK_TOKEN,action就是FrameCallback
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
public interface FrameCallback {
public void doFrame(long frameTimeNanos);
}
復制代碼
~~~
可以看到postFrameCallback()傳入的是FrameCallback實例,接口FrameCallback只有一個doFrame()方法。并且也是走到postCallbackDelayedInternal,FrameCallback實例作為action傳入,token則是FRAME\_CALLBACK\_TOKEN,并且任務是CALLBACK\_ANIMATION類型。
**Choreographer的postFrameCallback()通常用來計算丟幀情況**,使用方式如下:
~~~java
//Application.java
public void onCreate() {
super.onCreate();
//在Application中使用postFrameCallback
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
}
public class FPSFrameCallback implements Choreographer.FrameCallback {
private static final String TAG = "FPS_TEST";
private long mLastFrameTimeNanos = 0;
private long mFrameIntervalNanos;
public FPSFrameCallback(long lastFrameTimeNanos) {
mLastFrameTimeNanos = lastFrameTimeNanos;
mFrameIntervalNanos = (long)(1000000000 / 60.0);
}
@Override
public void doFrame(long frameTimeNanos) {
//初始化時間
if (mLastFrameTimeNanos == 0) {
mLastFrameTimeNanos = frameTimeNanos;
}
final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if(skippedFrames>30){
//丟幀30以上打印日志
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
mLastFrameTimeNanos=frameTimeNanos;
//注冊下一幀回調
Choreographer.getInstance().postFrameCallback(this);
}
}
復制代碼
~~~
### 小結
使用Choreographer的postCallback()、postFrameCallback() 作用理解:發送任務 存隊列中,監聽VSync信號,當前VSync到來時 會使用mHandler發送異步message,這個message的Runnable就是隊列中的所有任務。
好了,Choreographer整個代碼邏輯都講完了,引用[《Android 之 Choreographer 詳細分析》](https://link.juejin.cn?target=https%3A%2F%2Fwww.jianshu.com%2Fp%2F86d00bbdaf60 "https://www.jianshu.com/p/86d00bbdaf60")的流程圖:

# 五、Handler異步消息與同步屏障
最后來介紹下異步消息與同步屏障。
在Handler中,Message分為3種:同步消息、異步消息、同步屏障消息,他們三者都是Message,只是屬性有些區別。
## 5.1異步消息
通常我們使用創建Handler方式如下:
~~~java
public Handler() {
this(null, false);
}
復制代碼
~~~
注意到內部使用了兩個兩個參數的構造方法,其中第二個是false:
~~~java
public Handler(@Nullable Callback callback, boolean async) {
...
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
//異步標志
mAsynchronous = async;
}
復制代碼
~~~
這個false就表示 非異步,即使用的是同步消息,mAsynchronous使用是在enqueueMessage()中:
~~~java
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
//將Handler賦值給Message的target變量
msg.target = this;
//mAsynchronous為false,為同步消息
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
復制代碼
~~~
這里如果mAsynchronous是true,就會使用msg.setAsynchronous(true)設置為異步消息。所以上面Choreographer中使用的都是異步消息。
## 同步屏障消息
postSyncBarrier()方法就是用來插入一個屏障到消息隊列的,
~~~java
//MessageQueue
public int postSyncBarrier() {
return postSyncBarrier(SystemClock.uptimeMillis());
}
private int postSyncBarrier(long when) {
synchronized (this) {
final int token = mNextBarrierToken++;
//注意這里 沒有tartget賦值
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
復制代碼
~~~
可以看到它很簡單,從這個方法我們可以知道如下:
* 屏障消息和普通消息的區別在于**屏障沒有tartget**,普通消息有target是因為它需要將消息分發給對應的target,而屏障不需要被分發,它就是**用來擋住普通消息來保證異步消息優先處理的**。
* **屏障和普通消息一樣可以根據時間來插入到消息隊列中的適當位置,并且只會擋住它后面的同步消息的分發**
* postSyncBarrier()返回一個int類型的數值,通過這個數值可以撤銷屏障即removeSyncBarrier()。
* postSyncBarrier()是私有的,如果我們想調用它就得使用反射。插入普通消息會喚醒消息隊列,但是插入屏障不會。
## 原理
同步屏障消息 是如何 擋住普通消息來保證異步消息優先處理的?我們看看MessageQueue的next()方法:
~~~java
//MessageQueue.java
Message next() {
...
for (;;) {
...
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// msg.target == null 就是同步屏障消息,那么只取異步消息
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
if (msg != null) {
if (now < msg.when) {
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
...
}
}
復制代碼
~~~
很簡單,遍歷消息隊列時,發現了同步屏障消息,那么就只取異步消息了。
好了,相關知識終于講完了。
# 六、疑問解答
1. **丟幀**(掉幀) ,是說 這一幀延遲顯示 還是丟棄不再顯示 ?
答:延遲顯示,因為緩存交換的時機只能等下一個VSync了。
2. 布局層級較多/主線程耗時 是如何造成 丟幀的呢?
答:布局層級較多/主線程耗時 會影響CPU/GPU的執行時間,大于16.6ms時只能等下一個VSync了。
3. 16.6ms刷新一次 是啥意思?是每16.6ms都走一次 measure/layout/draw ? 答:屏幕的固定刷新頻率是60Hz,即16.6ms。不是每16.6ms都走一次 measure/layout/draw,而是有繪制任務才會走,并且繪制時間間隔是取決于布局復雜度及主線程耗時。
4. measure/layout/draw 走完,界面就立刻刷新了嗎?
答:不是。measure/layout/draw 走完后 會在VSync到來時進行緩存交換和刷新。
5. 如果界面沒動靜止了,還會刷新嗎?
答:屏幕會固定沒16.6ms刷新,但CPU/GPU不走繪制流程。見下面的SysTrace圖。 6. 可能你知道**VSYNC**,這個具體指啥?在屏幕刷新中如何工作的? 答:當掃描完一個屏幕后,設備需要重新回到第一行以進入下一次的循環,此時會出現的vertical sync pulse(垂直同步脈沖)來保證雙緩沖在最佳時間點才進行交換。并且Android4.1后 CPU/GPU的繪制是在VSYNC到來時開始。 7. 可能你還聽過屏幕刷新使用 **雙緩存**、**三緩存**,這又是啥意思呢?
答:雙緩存是Back buffer、Frame buffer,用于解決畫面撕裂。三緩存增加一個Back buffer,用于減少Jank。
8. 可能你還聽過神秘的**Choreographer**,這又是干啥的? 答:用于實現——"CPU/GPU的繪制是在VSYNC到來時開始"。

好了,就到這里了。以上問題都理解的話,會對Android屏幕刷新、UI優化、卡頓優化 有更加全面和清晰的認識。其中涉及的知識點也較多,需要把這些都串起來。
# 參考鏈接
[“終于懂了” 系列:Android屏幕刷新機制—VSync、Choreographer 全面理解!](https://juejin.cn/post/6863756420380196877)
- Android
- 四大組件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介紹
- MessageQueue詳細
- 啟動流程
- 系統啟動流程
- 應用啟動流程
- Activity啟動流程
- View
- view繪制
- view事件傳遞
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大數據
- Binder小結
- Android組件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 遷移與修復
- Sqlite內核
- Sqlite優化v2
- sqlite索引
- sqlite之wal
- sqlite之鎖機制
- 網絡
- 基礎
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP進化圖
- HTTP小結
- 實踐
- 網絡優化
- Json
- ProtoBuffer
- 斷點續傳
- 性能
- 卡頓
- 卡頓監控
- ANR
- ANR監控
- 內存
- 內存問題與優化
- 圖片內存優化
- 線下內存監控
- 線上內存監控
- 啟動優化
- 死鎖監控
- 崩潰監控
- 包體積優化
- UI渲染優化
- UI常規優化
- I/O監控
- 電量監控
- 第三方框架
- 網絡框架
- Volley
- Okhttp
- 網絡框架n問
- OkHttp原理N問
- 設計模式
- EventBus
- Rxjava
- 圖片
- ImageWoker
- Gilde的優化
- APT
- 依賴注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 協程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 運行期Java-hook技術
- 編譯期hook
- ASM
- Transform增量編譯
- 運行期Native-hook技術
- 熱修復
- 插件化
- AAB
- Shadow
- 虛擬機
- 其他
- UI自動化
- JavaParser
- Android Line
- 編譯
- 疑難雜癥
- Android11滑動異常
- 方案
- 工業化
- 模塊化
- 隱私合規
- 動態化
- 項目管理
- 業務啟動優化
- 業務架構設計
- 性能優化case
- 性能優化-排查思路
- 性能優化-現有方案
- 登錄
- 搜索
- C++
- NDK入門
- 跨平臺
- H5
- Flutter
- Flutter 性能優化
- 數據跨平臺