APP開發中,卡頓絕對優化的大頭,Google爲了幫助開發者更好的定位問題,提供了很多工具,如Systrace、GPU呈現模式分析工具、Android Studio自帶的CPU Profiler等,主要是輔助定位哪段代碼、哪塊邏輯比較耗時,影響UI渲染,致使了卡頓。拿Profile GPU Rendering工具而言,它用一種很直觀的方式呈現可能超時的節點,該工具及其原理也是本文的重點:算法
CPU Profiler也會提供類似的圖表,本文主要圍繞着GPU呈現模式分析工具展開,簡析各個階段耗時統計的原理,同時總結下在使用及分析過程當中也遇到的一些問題,可能算工具自身的BUG,這給分析帶來了很多困惑。好比以下幾點:canvas
Profile GPU Rendering工具的使用很簡單,就是直觀上看一幀的耗時有多長,綠線是16ms的閾值,超過了,可能會致使掉幀,這個跟VSYNC垂直同步信號有關係,固然,這個圖表並非絕對嚴謹的(後文會說緣由)。每一個顏色的方塊表明不一樣的處理階段,先看下官方文檔給的映射表:緩存
想要徹底理解各個階段,要對硬件加速及GPU渲染有必定的瞭解,不過,有一點,必須先記內心:雖名爲 Profile GPU Rendering,但圖標中全部階段都發生在CPU中,不是GPU 。最終CPU將命令提交到 GPU 後觸發GPU異步渲染屏幕,以後CPU會處理下一幀,而GPU並行處理渲染,二者硬件上算是並行。 不過,有些時候,GPU可能過於繁忙,不能跟上CPU的步伐,這個時候,CPU必須等待,也就是最終的swapbuffer部分,主要是最後的紅色及黃色部分(同步上傳的部分不會有問題,我的認爲是由於在Android GPU與CPU是共享內存區域的),在等待時,將看到橙色條和紅色條中出現峯值,且命令提交將被阻止,直到 GPU 命令隊列騰出更多空間。app
在使用Profile GPU Rendering工具時,我面臨第一個問題是:官方文檔的使用指導好像不太對。less
真正使用該工具的時候,條形圖的顏色跟文檔好像對不上,爲了測試,這裏先用一個小段代碼模擬場景,鑑別出各個階段,最後再分析源碼。從下往上,先忽略VSYNC部分,先看輸入事件,在一個自定義佈局中,爲觸摸事件添加延時,並觸發重繪。dom
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
}
mTextView.setText("" + System.currentTimeMillis());
requestLayout();
super.dispatchTouchEvent(ev);
return true;
}
複製代碼
這個時候看到的超時部分主要是輸入事件引發的,進而肯定下輸入事件的顏色:異步
輸入事件加個20ms延後,上圖紅色方塊部分正好映射到輸入事件耗時,這裏就能看到,輸入事件的顏色跟官方文檔的顏色對不上,以下圖ide
一樣,測量佈局的耗時也跟文檔對不上。爲佈局測量加個耗時,便可驗證:函數
@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事件處理機制,後面會逐步說明,先來看一下以上三個事件的耗時是怎麼統計的。
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中有一部分是統計掉幀的,我的理解也許這部分統計並非特別靠譜,下面看下掉幀的部分。
有些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玄學曲線中比較擰巴的地方,剩餘的幾個階段其實就比較清晰了。
通常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線程。
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所使用的數據就相互獨立開來,二者並行處理的時候不會有什麼同步問題,耗時大的話,說明須要上傳位圖信息過多,這裏我的感受主要是給紋理、材質準備的素材。
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來實現。
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渲染。
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相關知識。
做者:看書的小蝸牛 Android GPU呈現模式原理及卡頓掉幀淺析
僅供參考,歡迎指正