說說Android的UI重新整理機制的實現
本文主要解決以下幾個問題:
- 我們都知道Android的重新整理頻率是60幀/秒,這是不是意味著每隔16ms就會呼叫一次onDraw方法?
- 如果介面不需要重繪,那麼16ms到後還會重新整理螢幕嗎?
- 我們呼叫invalidate()之後會馬上進行螢幕重新整理嗎?
- 我們說丟幀是因為主執行緒做了耗時操作,為什麼主執行緒做了耗時操作就會引起丟幀?
- 如果在螢幕快要重新整理的時候才去OnDraw()繪製,會丟幀嗎?
好了,帶著以上問題,我們進入原始碼來找尋答案。
一、螢幕繪製流程
螢幕繪製機制的基本原理可以概括如下:
整個螢幕繪製的基本流程是:
- 應用向系統服務申請buffer
- 系統服務返回buffer
- 應用繪製後提交buffer給系統服務
如果放到Android中來,那麼就是:
在Android中,一塊Surface對應一塊記憶體,當記憶體申請成功後,App端才有繪圖的地方。由於Android的view繪製不是今天的重點,所以這裡點到為止~
二、螢幕重新整理分析
螢幕重新整理的時機是當Vsync訊號到來的時候,具體如圖:
在Android端,是誰在控制 Vsync
的產生?又是誰來通知我們應用進行重新整理的呢? 在Android中, Vysnc
訊號的產生是由底層 HWComposer
負責的,而通知應用進行重新整理,是Java層的 Choreographer
,Android整個螢幕重新整理的核心就在於這個 Choreographer
下面我們結合程式碼一起來看一下。
每次當我們要進行ui重繪的時候,都會呼叫 requestLayout()
,所以,我們從這個方法入手:
2.1 requestLayout()
----》類名:ViewRootImpl @Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; //重點 scheduleTraversals(); } }
2.2 scheduleTraversals()
----》類名:ViewRootImpl void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL,mTraversalRunnable,null); ...... } }
可以看到,在這裡並沒有立即進行重繪,而是做了兩件事情:
- 往訊息佇列裡面插入一條SyncBarrier(同步屏障)
- 通過Cherographer post了一個callback
接下來,我們簡單說一下這個 SyncBarrier
(同步屏障)。
非同步屏障的作用在於:
- 阻止同步訊息的執行
- 優先執行非同步訊息
為什麼要設計這個 SyncBarrier
呢?主要原因在於,在Android中,有些訊息是十分緊急的,需要馬上執行,如果說訊息佇列裡面普通訊息太多的話,那等到執行它的時候可能早就過了時機了。
到這裡,可能有人會跟我一樣,覺得為什麼不乾脆在Message裡搞個優先順序,按照優先順序來進行排序呢?弄個 PriorityQueue
不就完了嗎?
我自己的理解是,在Android中,訊息佇列的設計是一個 單鏈表
,整個連結串列的排序是根據時間進行排序的,如果此時再加入一個優先順序的排序規則,一方面會複雜會排序規則,另一方面,也會使得訊息不可控。因為優先順序是可以使用者自己在外面填的,那樣不就亂套了嗎?如果使用者每次總填最高的優先順序,這樣就會導致系統訊息很久才會消費,整個系統運作就會出問題,最後影響使用者體驗,所以,我自己覺得Android的同步屏障這個設計還是挺巧妙的~
好了,總結一下,執行 scheduleTraversals()
後,會插入一個屏障,保證非同步訊息的優先執行。
插入一個小小的思考題: 如果說我們在一個方法裡連續呼叫了 requestLayout()
多次,那麼請問:系統會插入多條屏障或者 post
多個 Callback
嗎? 答案是不會,為什麼呢?看到 mTraversalScheduled
這個變量了嗎?它就是答案~
2.3 Choreographer.postCallback()
先來簡單說一下 Choreographer
, Choreographer
中文翻譯叫 編舞者
,它的主要作用是進行系統協調的。(大家可以上網google下實際工作中的編舞者,這個類名真的起的很貼切了~)
Choreographer
這個類是應用怎麼初始化的呢?是通過 getInstance()
方法:
public static Choreographer getInstance() { return sThreadInstance.get(); } // Thread local storage for the choreographer. private static final ThreadLocal<Choreographer> sThreadInstance = new ThreadLocal<Choreographer>() { @Override protected Choreographer initialValue() { Looper looper = Looper.myLooper(); if (looper == null) { 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; } };
這裡貼出來是為了提醒大家, Choreographer
不是單例,而是每個執行緒都有單獨的一份。
好了,回到我們的程式碼:
----》類名:Choreographer //1 public void postCallback(int callbackType,Runnable action,Object token) { postCallbackDelayed(callbackType,action,token,0); } //2 public void postCallbackDelayed(int callbackType,Object token,long delayMillis) { .... postCallbackDelayedInternal(callbackType,delayMillis); } //3 private void postCallbackDelayedInternal(int callbackType,Object action,long delayMillis) { ... mCallbackQueues[callbackType].addCallbackLocked(dueTime,token); if (dueTime <= now) { scheduleFrameLocked(now); } else { ... } }
Choreographer
post的callback會放入 CallbackQueue
裡面,這個 CallbackQueue
是一個單鏈表。
首先會根據callbackType得到一條 CallbackQueue
單鏈表,之後會根據時間順序,將這個callback插入到單鏈表中;
2.4 scheduleFrameLocked()
----》類名:Choreographer private void scheduleFrameLocked(long now) { ... // If running on the Looper thread,then schedule the vsync immediately,// otherwise post a message to schedule the vsync from the UI thread // as soon as possible. if (isRunningOnLooperThreadLocked()) { scheduleVsyncLocked(); } else { Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); } } else { ... } } }
scheduleFrameLocked
的作用是:
- 如果當前執行緒就是
Cherographer
的工作執行緒的話,那麼就直接執行scheduleVysnLocked
- 否則,就傳送一個非同步訊息到訊息佇列裡面去 ,這個非同步訊息是不受同步屏障影響的,而且這個訊息還要插入到訊息佇列的頭部,可見這個訊息是非常緊急的
跟蹤原始碼,我們發現,其實 MSG_DO_SCHEDULE_VSYNC
這條訊息,最終執行的也是 scheduleFrameLocked
這個方法,所以我們直接跟蹤 scheduleVsyncLocked()
這個方法。
2.5 scheduleVsyncLocked()
----》類名:Choreographer private void scheduleVsyncLocked() { mDisplayEventReceiver.scheduleVsync(); } ----》類名:DisplayEventReceiver 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 { //mReceiverPtr是Native層一個類的指標地址 //這裡這個類指的是底層NativeDisplayEventReceiver這個類 //nativeScheduleVsync底層會呼叫到requestNextVsync()去請求下一個Vsync, //具體不跟蹤了,native層程式碼更長,還涉及到各種描述符監聽以及跨程序資料傳輸 nativeScheduleVsync(mReceiverPtr); } }
這裡我們可以看到一個新的類: DisplayEventReceiver
,這個類的作用是註冊Vsync訊號的監聽,當下個Vsync訊號到來的時候就會通知到這個 DisplayEventReceiver
了。
在哪裡通知呢?原始碼裡註釋寫的非常清楚了:
----》類名:DisplayEventReceiver // Called from native code. <---註釋還是很良心的 private void dispatchVsync(long timestampNanos,int builtInDisplayId,int frame) { onVsync(timestampNanos,builtInDisplayId,frame); }
當下一個Vysnc訊號到來的時候,會最終呼叫 onVsync
方法:
public void onVsync(long timestampNanos,int frame) { }
點進去一看,是個空實現,回到類定義,原來是個抽象類,它的實現類是: FrameDisplayEventReceiver
,定義在 Cherographer
裡面:
----》類名:Choreographer private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { .... }
2.6 FrameDisplayEventReceiver.onVysnc()
----》類名:Choreographer private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { @Override public void onVsync(long timestampNanos,int frame) { .... mTimestampNanos = timestampNanos; mFrame = frame; Message msg = Message.obtain(mHandler,this); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg,timestampNanos / TimeUtils.NANOS_PER_MS); } @Override public void run() { .... doFrame(mTimestampNanos,mFrame); } }
onVsync
方法往 Cherographer
所線上程的訊息佇列中傳送的一個訊息,這個訊息是就是它自己(它實現了Runnable),所以最終會呼叫到 doFrame()
方法。
2.7 doFrame(mTimestampNanos,mFrame)
doFrame()的處理分為兩個階段:
void doFrame(long frameTimeNanos,int frame) { final long startNanos; synchronized (mLock) { //1、階段一 long intendedFrameTimeNanos = frameTimeNanos; startNanos = System.nanoTime(); final long jitterNanos = startNanos - frameTimeNanos; if (jitterNanos >= mFrameIntervalNanos) { final long skippedFrames = jitterNanos / mFrameIntervalNanos; if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) { Log.i(TAG,"Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread."); } ... } ... }
frameTimeNanos
是當前的時間戳,將當前的時間和開始時間相減,得到這一幀處理花費了多長,如果大於 mFrameIntervalNano
,說明處理耗時了,之後就打印出我們日常見到的 The application may be doing too much work on its main thread
。
階段二:
void doFrame(long frameTimeNanos,int frame) { ... try { //階段2 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); mFrameInfo.markPerformTraversalsStart(); doCallbacks(Choreographer.CALLBACK_TRAVERSAL,frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT,frameTimeNanos); } ... }
doFrame()
的第二個階段做的是處理各種callback,從CallbackQueue裡面取出到執行時間的callback進行處理,那這個callback是怎麼樣呢?
這裡要回憶一下之前的 postCallback()
操作:
這個 Callback
其實就一個 mTraversalRunnable
,它是一個 Runnable
,最終會呼叫到 run()
方法,實現介面的真正重新整理:
----》類名:ViewRootImpl final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } } void doTraversal() { if (mTraversalScheduled) { ... performTraversals(); ... } } private void performTraversals() { ... //開始真正的介面繪製 performDraw(); ... }
三、總結
經過漫長的程式碼跟蹤,整個介面重新整理流程算是跟蹤完了,下面我們來總結一下:
四、問題解答
我們都知道Android的重新整理頻率是60幀/秒,這是不是意味著每隔16ms就會呼叫一次onDraw方法?
這裡60幀/秒是螢幕重新整理頻率,但是是否會呼叫onDraw()方法要看應用是否呼叫requestLayout()進行註冊監聽。
如果介面不需要重繪,那麼還16ms到後還會重新整理螢幕嗎?
如果不需要重繪,那麼應用就不會受到Vsync訊號,但是還是會進行重新整理,只不過繪製的資料不變而已;
我們呼叫invalidate()之後會馬上進行螢幕重新整理嗎?
不會,到等到下一個Vsync訊號到來
我們說丟幀是因為主執行緒做了耗時操作,為什麼主執行緒做了耗時操作就會引起丟幀
原因是,如果在主執行緒做了耗時操作,就會影響下一幀的繪製,導致介面無法在這個Vsync時間進行重新整理,導致丟幀了。
如果在螢幕快要重新整理的時候才去OnDraw()繪製,會丟幀嗎?
這個沒有太大關係,因為Vsync訊號是週期的,我們什麼時候發起onDraw()不會影響介面重新整理;
五、參考文件
gityuan大神的 Cherographer原理
視訊
到此這篇關於說說Android的UI重新整理機制的實現的文章就介紹到這了,更多相關Android UI重新整理機制內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!