APP開發中,卡頓絕對優化的大頭,Google為了幫助開發者更好的定位問題,提供了不少工具,如Systrace、GPU呈現模式分析工具、Android Studio自帶的CPU Profiler等,主要是輔助定位哪段程式碼、哪塊邏輯比較耗時,影響UI渲染,導致了卡頓。拿Profile GPU Rendering工具而言,它用一種很直觀的方式呈現可能超時的節點,該工具及其原理也是本文的重點:
CPU Profiler也會提供相似的圖表,本文主要圍繞著GPU呈現模式分析工具展開,簡析各個階段耗時統計的原理,同時總結下在使用及分析過程中也遇到的一些問題,可能算工具自身的BUG,這給分析帶來了不少困惑。比如如下幾點:
- GPU呈現模式分析工具跟Google官方文件上似乎對應不起來(各個顏色代表的階段)
- CPU Profiler的函式呼叫似乎有些呼叫被合併了,並非獨立的呼叫棧(影響分析哪塊耗時)
- Skip Frame掉幀可能跟我們預想的不同,而且掉幀的統計也可能不準(主要是Vsync的延時部分,有些耗時操作導致卡頓了,但是可能沒有統計出掉幀)
GPU呈現模式分析工具簡介
Profile GPU Rendering工具的使用很簡單,就是直觀上看一幀的耗時有多長,綠線是16ms的閾值,超過了,可能會導致掉幀,這個跟VSYNC垂直同步訊號有關係,當然,這個圖表並不是絕對嚴謹的(後文會說原因)。每個顏色的方塊代表不同的處理階段,先看下官方文件給的對映表:
想要完全理解各個階段,要對硬體加速及GPU渲染有一定的瞭解,不過,有一點,必須先記心裡:雖名為 Profile GPU Rendering,但圖示中所有階段都發生在CPU中,不是GPU 。最終CPU將命令提交到 GPU 後觸發GPU非同步渲染螢幕,之後CPU會處理下一幀,而GPU並行處理渲染,兩者硬體上算是並行。 不過,有些時候,GPU可能過於繁忙,不能跟上CPU的步伐,這個時候,CPU必須等待,也就是最終的swapbuffer部分,主要是最後的紅色及黃色部分(同步上傳的部分不會有問題,個人認為是因為在Android GPU與CPU是共享記憶體區域的),在等待時,將看到橙色條和紅色條中出現峰值,且命令提交將被阻止,直到 GPU 命令佇列騰出更多空間。
在使用Profile GPU Rendering工具時,我面臨第一個問題是:官方文件的使用指導好像不太對。
Profile GPU Rendering工具顏色問題
真正使用該工具的時候,條形圖的顏色跟文件好像對不上,為了測試,這裡先用一個小段程式碼模擬場景,鑑別出各個階段,最後再分析原始碼。從下往上,先忽略VSYNC部分,先看輸入事件,在一個自定義佈局中,為觸控事件新增延時,並觸發重繪。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
}
mTextView.setText("" + System.currentTimeMillis());
requestLayout();
super.dispatchTouchEvent(ev);
return true;
}
複製程式碼
這個時候看到的超時部分主要是輸入事件引起的,進而確定下輸入事件的顏色:
輸入事件加個20ms延後,上圖紅色方塊部分正好對映到輸入事件耗時,這裡就能看到,輸入事件的顏色跟官方文件的顏色對不上,如下圖
同樣,測量佈局的耗時也跟文件對不上。為佈局測量加個耗時,即可驗證:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
複製程式碼
可以看到,上圖中測量佈局的耗時跟官方文件的顏色也對不上。除此之外,似乎多出了第三部分耗時,這部分其實是VSYNC同步耗時,這部分耗時怎麼來的,真的存在耗時嗎?官方解釋似乎是連個連續幀之間的耗時,但是後面分析會發現,可能這個解釋同原始碼對應不起來。
Miscellaneous
In addition to the time it takes the rendering system to perform its work, there’s an additional set of work that occurs on the main thread and has nothing to do with rendering. Time that this work consumes is reported as misc time. Misc time generally represents work that might be occurring on the UI thread between two consecutive frames of rendering.
其次,為什麼幾乎每個條形圖都有一個測量佈局耗時跟輸入事件耗時呢?為什麼是一一對應,而不是有多個?測量佈局是在Touch事件之後立即執行呢,還是等待下一個VSYNC訊號到來再執行呢?這部主要牽扯到的內容:VSYNC垂直同步訊號、ViewRootImpl、Choreographer、Touch事件處理機制,後面會逐步說明,先來看一下以上三個事件的耗時是怎麼統計的。
Miscellaneous--VSYNC延時
Profile GPU Rendering工具統計的入口在Choreographer類中,時機是VSYNC訊號Message被執行,注意這裡是訊號訊息被執行,而不是訊號到來,因為訊號到來並不意味著立即被執行,因為VSYNC訊號的申請是非同步的,訊號申請後執行緒繼續執行當前訊息,SurfaceFlinger在下一次分發VSYNC的時候直接往APP UI執行緒的MessageQueue插入一條VSYNC到來的訊息,而訊息被插入後,並不會立即被執行,而是要等待之前的訊息執行完畢後才會執行,而VSYNC延時其實就是VSYNC訊息到來到被執行之間的延時。
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
...
long intendedFrameTimeNanos = frameTimeNanos;
<!--關鍵點1 設定vsync開始,並記錄起始時間 -->
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
try {
// 開始處理輸入事件,並記錄起始時間
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
// 開始處理動畫,並記錄起始時間
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 開始處理測量佈局,並記錄起始時間
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
} finally {
}
複製程式碼
這裡的 VSYNC延時其實是 mFrameInfo.markInputHandlingStart - frameTimeNanos,而frameTimeNanos是VSYNC訊號到達的時間戳,如下
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper) {
super(looper);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
...
<!--存下時間戳,並往UI的MessageQueue傳送一個訊息-->
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-->
mHavePendingVsync = false;
doFrame(mTimestampNanos, mFrame);
}
}
複製程式碼
onVsync是VSYNC訊號到達的時候在Native層回撥Java層的方法,其實是MessegeQueue的native訊息佇列那一套,並且VSYNC要一個執行完,下一個才會生效,否則下一個VSYNC只能在佇列中等待,所以之前說的???第三部分延時就是VSYNC延時,但是這部分不應該被算到渲染中去,另外根據寫法,VSYNC延時可能也有很大出入。看doFrame中有一部分是統計掉幀的,個人理解也許這部分統計並不是特別靠譜,下面看下掉幀的部分。
掉幀Skiped Frame同Vsync延時耗時的關係
有些APM檢測工具通過將Choreographer的SKIPPED_FRAME_WARNING_LIMIT設定為1,來達到掉幀檢測的目的,即如下設定:
try {
Field field = Choreographer.class.getDeclaredField("SKIPPED_FRAME_WARNING_LIMIT");
field.setAccessible(true);
field.set(Choreographer.class, 0);
} catch (Throwable e) {
}
複製程式碼
如果出現卡頓,在log日誌中就能看到如下資訊
感覺這裡並不是太嚴謹,看原始碼如下:
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
<!--skip frame關鍵點 -->
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.");
}
...
}
複製程式碼
可以看到跳幀檢測的演算法就是:Vsync訊號延時/16ms,有多少個,就算跳幾幀。Vsync訊號到了後,重繪並不一定會立刻執行,因為UI執行緒可能被阻塞再某個地方,比如在Touch事件中,觸發了重繪,之後繼續執行了一個耗時操作,這個時候,必然會導致Vsync訊號被延時執行,跳幀日誌就會被列印,如下
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
super.dispatchTouchEvent(ev);
scrollTo(0,new Random().nextInt(15));
try {
Thread.sleep(40);
} catch (InterruptedException e) {
}
return true;
}
複製程式碼
可以看到,顏色2的部分就是Vsync信訊號延時,這個時候會有掉幀日誌。
但是如果將觸發UI重繪的訊息放到延時操作後面呢?毫無疑問,卡頓依然有,但這時會發生一個有趣的現象,跳幀沒了,系統認為沒有幀丟失,程式碼如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
super.dispatchTouchEvent(ev);
try {
Thread.sleep(40);
} catch (InterruptedException e) {
}
scrollTo(0,new Random().nextInt(15));
return true;
}
複製程式碼
可以看到,圖中幾乎沒有Vsync信訊號延時,這是為什麼?因為下一個VSYNC訊號的申請是由scrollTo觸發,觸發後並沒有什麼延時操作,直到VSYNC訊號到來後,立即執行doFrame,這個之間的延時很少,系統就認為沒有掉幀,但是其實卡頓依舊。因為整體來看,一段時間內的幀率是相同的,整體示意如下:
以上就是scrollTo在延時前後的區別,兩種其實都是掉幀的,但是日誌統計的跳幀卻出現了問題,而且,每一幀真正的耗也並不是我們看到的樣子,個人覺得這可能算是工具的一個BUG,不能很精確的反應卡頓問題,依靠這個做FPS偵測,應該也都有問題。比如滾動時候,處理耗時操作後,再更新UI,這種方式是檢測不出跳幀的,當然不排除有其他更好的方案。下面看一下Input時間耗時,之前,針對Touch事件的耗時都是直接用了,並未分析為何一幀裡面會有且只有一個Touch事件耗時?是否所有的Touch事件都被統計了呢?Touch事件如何影響GPU 統計工具呢?
輸入事件耗時分析
輸入事件處理機制:InputManagerService捕獲使用者輸入,通過Socket將事件傳遞給APP端(往UI執行緒的訊息佇列裡插入訊息)。對於不同的觸控事件有不同的處理機制:對於Down、UP事件,APP端需要立即處理,對於Move事件,要結合重繪事件一併處理,其實就是要等到下一次VSYNC到來,分批處理。可以認為只有MOVE事件才被GPU柱狀圖統計到裡面,UP、DOWN事件被立即執行,不會等待VSYNC跟UI重繪一起執行。。這裡不妨先看一個各個階段耗時統計的依據,GPU 呈現工具圖表的繪製是在native層完成的,其各個階段統計示意如下:
FrameInfoVisualizer.cpp
前文分析的VSYNC延時其實就是 FrameInfoIndex::HandleInputStart -FrameInfoIndex::IntendedVsync 顏色是0x00796B,輸入事件耗時其實就是FrameInfoIndex::PerformTraversalsStart -FrameInfoIndex::HandleInputStart,不過這裡只有7種,跟文件的8中對應不上。在doFrame可以得到驗證:
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
if (!mFrameScheduled) {
...
// 設定vsync開始,並記錄起始時間
<!--關鍵點1 -->
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
try {
// 開始處理輸入事件,並記錄起始時間
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
// 開始處理動畫,並記錄起始時間
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
// 開始處理測量佈局,並記錄起始時間
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
} finally {
}
複製程式碼
如上程式碼很簡單,但是在利用CPU Profiler看函式呼叫棧的時候,卻發現很多問題。為Touch事件處理加入延時後,CPU Profiler看到的呼叫棧如下:
這個棧是怎麼回事?不是說好的,一次VSYNC訊號呼叫一次doFrame,而一次doFrame會依次執行不同型別的CallBack,但是看以上的呼叫棧,怎麼是穿插著來啊?這就尷尬了,莫非是BUG,事實證明,確實真可能是CPU Profiler的BUG。 證據就是doFrame的呼叫次數跟CPU Profiler 中統計的次數的壓根對應不起來,doFrame的次數明顯要很多。
也就是說CPU Profiler應該將一些類似的函式呼叫給整合分組了,所以看起來好像一個Vsync執行了一次doFrame,但是卻執行了很多CallBack,實際上,預設情況下,每種型別的CallBack在一次VSYNC期間,一般最多執行一次。**垂直同步訊號機制下,在下一個垂直同步訊號到來之前,Android系統最多隻能處理一個MOVE的Patch、一個繪製請求、一次動畫更新。**先看看Touch時間處理機制,上文的dispatchTouchEvent如何被執行的呢?
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
}
mTextView.setText("" + System.currentTimeMillis());
requestLayout();
super.dispatchTouchEvent(ev);
return true;
}
複製程式碼
InputManagerService收到Touch事件後,通過Socket傳遞給APP端,APP端的UI Loop會將事件讀取出來,在native預處理下,將事件傳送給Java層,
public abstract class InputEventReceiver {
...
public final boolean consumeBatchedInputEvents(long frameTimeNanos) {
if (mReceiverPtr == 0) {
Log.w(TAG, "Attempted to consume batched input events but the input event "
+ "receiver has already been disposed.");
} else {
return nativeConsumeBatchedInputEvents(mReceiverPtr, frameTimeNanos);
}
return false;
}
// Called from native code.
@SuppressWarnings("unused")
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
onInputEvent(event);
}
// NativeInputEventReceiver
// Called from native code.
@SuppressWarnings("unused")
private void dispatchBatchedInputEventPending() {
onBatchedInputEventPending();
}
...
}
複製程式碼
如果是DOWN、UP事件,呼叫dispatchInputEvent,如果是MOVE事件,則被封裝成Batch,呼叫dispatchBatchedInputEventPending,對於DOWN、UP事件會呼叫子類的enqueueInputEvent立即執行
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
@Override
public void onInputEvent(InputEvent event) {
<!--關鍵點 最後一個引數是true-->
enqueueInputEvent(event, this, 0, true);
}
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
adjustInputEventForCompatibility(event);
<!--獲取輸入事件-->
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
...
<!--是否立即執行-->
if (processImmediately) {
doProcessInputEvents();
} else {
scheduleProcessInputEvents();
}
}
複製程式碼
對於DOWN UP事件會呼叫 doProcessInputEvents立即執行, 而對於dispatchBatchedInputEventPending則呼叫WindowInputEventReceiver的onBatchedInputEventPending延遲到下一個VSYNC執行:
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
...
@Override
public void onBatchedInputEventPending() {
if (mUnbufferedInputDispatch) {
super.onBatchedInputEventPending();
} else {
scheduleConsumeBatchedInput();
}
}
複製程式碼
mUnbufferedInputDispatch預設都是false,為了提高執行效率,發行版的原始碼該引數都是false,所以這裡會執行scheduleConsumeBatchedInput:
void scheduleConsumeBatchedInput() {
<!--mConsumeBatchedInputScheduled保證了當前Touch事件被執行前,不會再有Batch事件被插入-->
if (!mConsumeBatchedInputScheduled) {
mConsumeBatchedInputScheduled = true;
<!--通過Choreographer暫存回撥,同時請求VSYNC訊號-->
mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
mConsumedBatchedInputRunnable, null);
}
}
複製程式碼
scheduleConsumeBatchedInput中的邏輯保證了每次VSYNC間,最多隻有一個Batch被處理。Choreographer.CALLBACK_INPUT型別的CallBack是輸入事件耗時統計的物件,只有Batch類Touch事件(MOVE事件)會涉及到這個型別,所以個人理解GPU呈現工具統計的輸入耗時只針對MOVE事件,直觀上也比較好理解:**MOVE滾動或者滑動事件一般都是要伴隨UI更新,這個持續的流程才是幀率關心的重點,如果不是持續更新,FPS(幀率)沒有意義。**繼續看Choreographer.postCallback函式
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
<!--新增回撥-->
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
<!--ViewrootImpl過來的一般都是立即執行,直接申請Vsync訊號-->
if (dueTime <= now) {
scheduleFrameLocked(now);
}
...
}
複製程式碼
Choreographer為Touch事件新增一個CallBack,並加入到快取佇列中,同時非同步申請VSYNC,等到訊號到來後,才會處理該Touch事件的回撥。VSYNC訊號到來後,Choreographer最先執行doFrame中的doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos),該函式會呼叫ConsumeBatchedInputRunnable的run函式,最終呼叫doConsumeBatchedInput處理Batch事件:
void doConsumeBatchedInput(long frameTimeNanos) {
<!--標記事件被處理,新的事件才有機會被新增進來-->
if (mConsumeBatchedInputScheduled) {
mConsumeBatchedInputScheduled = false;
if (mInputEventReceiver != null) {
if (mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos)
&& frameTimeNanos != -1) {
...
}
}
<!--處理事件-->
doProcessInputEvents();
}
}
複製程式碼
doProcessInputEvents會走事件分發機制最終回撥到對應的 dispatchTouchEvent完成Touch事件的處理。這個有個很重要的點:如果在處理Batch事件的時候觸發了UI重繪(非常常見),比如MOVE事件一般都伴隨著列表滾動,那麼這個重繪CallBack會立即被新增到Choreographer.CALLBACK_TRAVERSAL佇列中,並在執行完當前Choreographer.CALLBACK_INPUT回撥後,立刻執行,這就是為什麼CPU Profiler中總能看到一個一個Touch事件後面跟著一個UI重繪事件。拿上文例子而言requestLayout()最終會呼叫ViewRootImpl的:
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
複製程式碼
從而呼叫scheduleTraversals,可以看到這裡也用了一個標記mTraversalScheduled,保證一次VSYNC中最多一次重繪:
void scheduleTraversals() {
// 重複多次呼叫invalid requestLayout只會標記一次,等到下一次Vsync訊號到,只會執行執行一次
if (!mTraversalScheduled) {
mTraversalScheduled = true;
<!--新增一個柵欄,阻止同步訊息執行-->
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
<!--=更新UI的時候,通常伴隨MOVE事件,預先請求一次Vsync訊號,不用真的等到訊息到來再請求,提高吞吐率-->
// mUnbufferedInputDispatch =false 一般都是false 所以會執行scheduleConsumeBatchedInput,
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
複製程式碼
對於重繪事件而言,通過mChoreographer.postCallback直接新增一個CallBack,同時請求Vsync訊號,一般而言scheduleTraversals中的scheduleConsumeBatchedInput請求VSYNC是無效,因為連續兩次請求VSYNC的話,只有一次是有效的,scheduleConsumeBatchedInput只是為後續的Touch事件提前佔個位置。剛開始執行Touch事件的時候,mCallbackQueues資訊是這樣的:
可以看到,開始並沒有Choreographer.CALLBACK_TRAVERSAL型別的回撥,在處理Touch事件的時候,觸發了重繪,動態增加了Choreographer.CALLBACK_TRAVERSAL類CallBack,如下
那麼,在當前MOVE時間處理完畢後,doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos)會被執行,剛才被加入的重繪CallBack會立即執行,而不會等待到下一次Vsync訊號的到來,這就是之前MOVE跟重繪一一對應,並且重繪總是在MOVE事件之後執行的原理,同時也看到Choreographer用了不少標記,保證一次VSYNC期間,最多有一個MOVE事件、重回時間被依次執行(先忽略動畫)。以上兩個是GPU玄學曲線中比較擰巴的地方,剩餘的幾個階段其實就比較清晰了。
CALLBACK_ANIMATION類CallBack耗時 (似乎被算到Touch事件耗時中去了)
一般MOVE事件伴隨Scroll,比如List,scroll的時候可能觸發了所謂的動畫,
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
複製程式碼
最終呼叫Choreogtapher的postInvalidateOnAnimation建立Choreographer.CALLBACK_ANIMATION型別回撥
public void postInvalidateOnAnimation() {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateOnAnimation(this);
}
}
final class InvalidateOnAnimationRunnable implements Runnable {
...
private void postIfNeededLocked() {
if (!mPosted) {
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);
mPosted = true;
}
}
複製程式碼
只是呼叫View的invalidate,不怎麼耗時:
final class InvalidateOnAnimationRunnable implements Runnable {
@Override
public void run() {
final int viewCount;
final int viewRectCount;
synchronized (this) {
...
for (int i = 0; i < viewCount; i++) {
mTempViews[i].invalidate();
mTempViews[i] = null;
}
複製程式碼
當然,如果這裡有自定義動畫的話,就不一樣了。但是,就GPU呈現模式統計耗時而言,卻並非像官方文件說的那樣,似乎壓根沒有這部分耗時,而原始碼中也只有七段,如下圖:
重寫invalidate函式驗證,會發現,這部分耗時會被歸到輸入事件耗時裡面:
@Override
public void invalidate() {
super.invalidate();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
}
}
複製程式碼
也就是說下面的官方說明可能是錯誤的,因為真機上沒看到這部分耗時,或者說,這部分耗時被歸結到Touch事件耗時中去了,從原始碼中看好像也是這樣。
測量、佈局、繪製耗時
走到測量重繪的時候,整個流程已經清晰了,在UI執行緒中測量重繪耗時很直觀,也很忠誠,用多少就是多少,沒有Vsync那樣彆扭的問題,沒什麼分析必要,不過需要注意的是,這裡的Draw僅僅是構建DisplayList數,也可以看做是幫助建立OpenGL繪製命令及預處理些資料,沒有真正渲染,到這裡為止,都是在UI執行緒中進行的,剩下三個階段Sync/upload、Issue commands、swap buffers都是在RenderThread執行緒。
Sync/upload(同步和上傳 )耗時
The Sync & Upload metric represents the time it takes to transfer bitmap objects from CPU memory to GPU memory during the current frame.
As different processors, the CPU and the GPU have different RAM areas dedicated to processing. When you draw a bitmap on Android, the system transfers the bitmap to GPU memory before the GPU can render it to the screen. Then, the GPU caches the bitmap so that the system doesn’t need to transfer the data again unless the texture gets evicted from the GPU texture cache.
表示將點陣圖資訊上傳到 GPU 所花的時間,不過Android手機上 CPU跟GPU是共享實體記憶體的,這裡的上傳個人理解成拷貝,這樣的話,CPU跟GPU所使用的資料就相互獨立開來,兩者並行處理的時候不會有什麼同步問題,耗時大的話,說明需要上傳點陣圖資訊過多,這裡個人感覺主要是給紋理、材質準備的素材。
Issue commands
The Issue Commands segment represents the time it takes to issue all of the commands necessary for drawing display lists to the screen.
這部分耗時主要是CPU將繪製命令傳送給GPU,之後,GPU才能根據這些OpenGL命令進行渲染。這部分主要是CPU呼叫OpenGL ES API來實現。
swapBuffers耗時
Once Android finishes submitting all its display list to the GPU, the system issues one final command to tell the graphics driver that it's done with the current frame. At this point, the driver can finally present the updated image to the screen.
之前的GPU命令被issue完畢後,CPU一般會傳送最後一個命令給GPU,告訴GPU當前命令傳送完畢,可以處理,GPU一般而言需要返回一個確認的指令,不過,這裡並不代表GPU渲染完畢,僅僅是通知CPU,GPU有空開始渲染而已,並未渲染完成,但是之後的問題APP端無需關心了,CPU可以繼續處理下一幀的任務了。如果GPU比較忙,來不及回覆通知,則CPU需要阻塞等待,直到收到通知,才會喚起當前阻塞的Render執行緒,繼續處理下一條訊息,這個階段是在swapBuffers中完成的。可進一步參考Android硬體加速(二)-RenderThread與OpenGL GPU渲染。
OpenGL GPU Profiler原始碼 (非真機,軟體模擬的OpenGL庫libagl)
GPU Profiler繪製主要是通過FrameInfoVisualizer的draw函式實現:
void FrameInfoVisualizer::draw(OpenGLRenderer* canvas) {
RETURN_IF_DISABLED();
...
// 繪製一條條,dubug模式中可以開啟
if (mType == ProfileType::Bars) {
// Patch up the current frame to pretend we ended here. CanvasContext
// will overwrite these values with the real ones after we return.
// This is a bit nicer looking than the vague green bar, as we have
// valid data for almost all the stages and a very good idea of what
// the issue stage will look like, too
FrameInfo& info = mFrameSource.back();
info.markSwapBuffers();
info.markFrameCompleted();
<!--計算寬度及高度-->
initializeRects(canvas->getViewportHeight(), canvas->getViewportWidth());
drawGraph(canvas);
drawThreshold(canvas);
}
}
複製程式碼
這裡用的色值及用的就是之前說的7種,這部分程式碼提前markSwapBuffers跟markFrameCompleted,看註釋,CanvasContext後面用real耗時進行校準:
void FrameInfoVisualizer::drawGraph(OpenGLRenderer* canvas) {
SkPaint paint;
for (size_t i = 0; i < Bar.size(); i++) {
nextBarSegment(Bar[i].start, Bar[i].end);
paint.setColor(Bar[i].color | BAR_FAST_ALPHA);
canvas->drawRects(mFastRects.get(), mNumFastRects * 4, &paint);
paint.setColor(Bar[i].color | BAR_JANKY_ALPHA);
canvas->drawRects(mJankyRects.get(), mNumJankyRects * 4, &paint);
}
}
複製程式碼
之前簡析過Java層四種耗時,現在看看最後三種耗時的統計點:
<!--同步開始-->
void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t syncQueued) {
mRenderThread.removeFrameCallback(this);
<!--將Java層拷貝-->
mCurrentFrameInfo->importUiThreadInfo(uiFrameInfo);
mCurrentFrameInfo->set(FrameInfoIndex::SyncQueued) = syncQueued;
// 這裡表示開始同步上傳點陣圖
mCurrentFrameInfo->markSyncStart();
...
mRootRenderNode->prepareTree(info);
...
}
複製程式碼
markSyncStart標記著上傳點陣圖開始,通過prepareTree將Texture相關點陣圖拷貝給GPU可用記憶體區域後,CanvasContext::draw進一步issue GPU命令到GPU緩衝區:
void CanvasContext::draw() {
...
<!--Issue的開始-->
mCurrentFrameInfo->markIssueDrawCommandsStart();
...
<!--GPU呈現模式的圖表繪製-->
profiler().draw(mCanvas);
<!--像GPU傳送命令,可能是對應的GPU驅動,快取等-->
mCanvas->drawRenderNode(mRootRenderNode.get(), outBounds);
<!--命令傳送完畢-->
mCurrentFrameInfo->markSwapBuffers();
if (drew) {
swapBuffers(dirty, width, height);
}
// TODO: Use a fence for real completion?
<!--這裡只有用GPU fence才能獲取真正的耗時,不然還是無效的,看每個手機廠家的實現了-->
mCurrentFrameInfo->markFrameCompleted();
mJankTracker.addFrame(*mCurrentFrameInfo);
mRenderThread.jankTracker().addFrame(*mCurrentFrameInfo);
}
複製程式碼
markIssueDrawCommandsStart 標記著issue命令開始,而mCanvas->drawRenderNode負責真正issue命令到緩衝區,issue結束後,通知GPU繪製,同時將圖層移交SurfaceFlinger,這部分是通過swapBuffers來實現的,在真機上需要藉助Fence機制來同步GPU跟CPU,參考Android硬體加速(二)-RenderThread與OpenGL GPU渲染。由於後三部分可控性比較小,不再分析,有興趣可以自己查查OpenGL及GPU相關知識。
總結
- GPU Profiler的色值跟官方文件對不起來
- 動畫耗時並沒有單獨的色塊,而是被歸併到Touch事件耗時中
- Studio自帶的CPU Profiler有問題,存在合併操作的BUG
- 原始碼中關於跳幀的統計可能不準,他統計的不是跳幀,而是VSYNC的延時
- Chorgropher通過各種標記保證了一個VSYNC訊號中最多隻有一個Touch事件、一個重繪事件、一次動畫更新
- GPU呈現模式的圖表僅供參考,並不完全正確。
作者:看書的小蝸牛 Android GPU呈現模式原理及卡頓掉幀淺析
僅供參考,歡迎指正