<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                [TOC] # VSync 在一個典型的顯示系統中,一般包括CPU、GPU、Display三個部分, CPU負責計算幀數據,把計算好的數據交給GPU,GPU會對圖形數據進行渲染,渲染好后放到buffer(圖像緩沖區)里存起來,然后Display(屏幕或顯示器)負責把buffer里的數據呈現到屏幕上。如下圖: ![](https://img.kancloud.cn/3d/dd/3ddd743baffbcd7592e94945cccc5f2d_1200x673.png) ## 基礎概念 * **屏幕刷新頻率** 一秒內屏幕刷新的次數(一秒內顯示了多少幀的圖像),單位 Hz(赫茲),如常見的 60 Hz。**刷新頻率取決于硬件的固定參數**(不會變的)。 * **逐行掃描** 顯示器并不是一次性將畫面顯示到屏幕上,而是從左到右邊,從上到下逐行掃描,順序顯示整屏的一個個像素點,不過這一過程快到人眼無法察覺到變化。以 60 Hz 刷新率的屏幕為例,這一過程即 1000 / 60 ≈ 16ms。 * **幀率** (Frame Rate) 表示 **GPU 在一秒內繪制操作的幀數**,單位 fps。例如在電影界采用 24 幀的速度足夠使畫面運行的非常流暢。而 Android 系統則采用更加流程的 60 fps,即每秒鐘GPU最多繪制 60 幀畫面。幀率是動態變化的,例如當畫面靜止時,GPU 是沒有繪制操作的,屏幕刷新的還是buffer中的數據,即GPU最后操作的幀數據。 * **畫面撕裂**(tearing) 一個屏幕內的數據來自2個不同的幀,畫面會出現撕裂感,如下圖 ![](https://img.kancloud.cn/0b/9a/0b9ad3a50c52a03aa259afcb4bf2f947_640x356.png) ## 雙緩存 ### 畫面撕裂 原因 屏幕刷新頻是固定的,比如每16.6ms從buffer取數據顯示完一幀,理想情況下幀率和刷新頻率保持一致,即每繪制完成一幀,顯示器顯示一幀。但是CPU/GPU寫數據是不可控的,所以會出現buffer里有些數據根本沒顯示出來就被重寫了,即buffer里的數據可能是來自不同的幀的, 當屏幕刷新時,此時它并不知道buffer的狀態,因此從buffer抓取的幀并不是完整的一幀畫面,即出現畫面撕裂。 簡單說就是Display在顯示的過程中,buffer內數據被CPU/GPU修改,導致畫面撕裂。 ### 雙緩存 那咋解決畫面撕裂呢? 答案是使用 雙緩存。 由于圖像繪制和屏幕讀取 使用的是同個buffer,所以屏幕刷新時可能讀取到的是不完整的一幀畫面。 **雙緩存**,讓繪制和顯示器擁有各自的buffer:GPU 始終將完成的一幀圖像數據寫入到 **Back Buffer**,而顯示器使用 **Frame Buffer**,當屏幕刷新時,Frame Buffer 并不會發生變化,當Back buffer準備就緒后,它們才進行交換。如下圖: ![雙緩存,CPU/GPU寫數據到Back Buffer,顯示器從Frame Buffer取數據](https://img-blog.csdnimg.cn/202008192024268.png#pic_center) ### 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 機制。如下圖: ![](https://img.kancloud.cn/fc/c5/fcc5184f615660c25960404042c5b22f_2278x1274.png) 以時間的順序來看下將會發生的過程: 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**。如下圖: ![](https://img.kancloud.cn/99/db/99db944edbf948ae9f375597ea5174af_2282x1272.png) CPU/GPU根據VSYNC信號同步處理數據,可以讓CPU/GPU有完整的16ms時間來處理數據,減少了jank。 一句話總結,**VSync同步使得CPU/GPU充分利用了16.6ms時間,減少jank。** 問題又來了,如果界面比較復雜,CPU/GPU的處理時間較長 超過了16.6ms呢?如下圖: ![](https://img.kancloud.cn/a0/f3/a0f30e0a28d19e3ec511f149ffe50a8f_2284x1278.png) 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 所占用的內存。 ![](https://img.kancloud.cn/ff/c4/ffc4670c149c910482f7f80e95118951_2278x1274.png) 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")的流程圖: ![](https://img.kancloud.cn/09/b8/09b8b61e6d684a698be00b96f87ac516_1012x1466.png) # 五、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到來時開始"。 ![](https://img.kancloud.cn/08/5f/085f668f334b909e66e70301bbfee5d5_3114x1668.png) 好了,就到這里了。以上問題都理解的話,會對Android屏幕刷新、UI優化、卡頓優化 有更加全面和清晰的認識。其中涉及的知識點也較多,需要把這些都串起來。 # 參考鏈接 [“終于懂了” 系列:Android屏幕刷新機制—VSync、Choreographer 全面理解!](https://juejin.cn/post/6863756420380196877)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看