故事開始
面試官:平時開發中有遇到卡頓問題嗎?你一般是如何處理的?
來面試的小夥:額...沒有遇到過卡頓問題,我平時寫的程式碼品質比較高,不會出現卡頓。
面試官:...
上面對話像是開玩笑,但是前段時間真的遇到一個來面試的小夥這樣答,問他有沒有遇到過卡頓問題,一般怎麼處理的?他說沒遇到過,說他寫的程式碼不會出現卡頓。這回答似乎沒啥問題,但是我會認為你在卡頓優化這一塊是0經驗。
卡頓這個話題,相信大部分兩年或以上工作經驗的同學都應該能說出個大概。 一般的回答可能類似這樣:
卡頓是由於主執行緒有耗時操作,導致View繪製掉幀,螢幕每16毫秒會重新整理一次,也就是每秒會重新整理60次,人眼能感覺到卡頓的幀率是每秒24幀。所以解決卡頓的辦法就是:耗時操作放到子執行緒、View的層級不能太多、要合理使用include、ViewStub標籤等等這些,來保證每秒畫24幀以上。
如果稍微問深一點, 卡頓的底層原理是什麼?如何理解16毫秒重新整理一次?假如介面沒有更新操作,View會每16毫秒draw一次嗎?
這個問題相信會難倒一片人,包括大部分3年以上經驗的同學,如果沒有去閱讀原始碼,未必能答好這個問題。當然,我希望你剛好是小部分人~
接下來將從原始碼角度分析螢幕重新整理機制,深入理解卡頓原理,以及介紹卡頓監控的幾種方式,希望對你有幫助。
一、螢幕重新整理機制從 View#requestLayout 開始分析,因為這個方法是主動請求UI更新,從這裡分析完全沒問題。
1. View#requestLayout
protected ViewParent mParent; ... public void requestLayout() { ... if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); //1 } }主要看註釋1,這裡的 mParent.requestLayout(),最終會呼叫 ViewRootImpl 的 requestLayout 方法。你可能會問,為什麼是ViewRootImpl?因為根View是DecorView,而DecorView的parent就是ViewRootImpl,具體看ViewRootImpl的setView方法裡呼叫view.assignParent(this);,可以暫且先認為就是這樣的,之後整理View的繪製流程的時候會詳細分析。
2. ViewRootImpl#requestLayout
public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { //1 檢測執行緒 checkThread(); mLayoutRequested = true; //2 scheduleTraversals(); }}註釋1 是檢測當前是不是在主執行緒
2.1 ViewRootImpl#checkThread
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); }}這個異常很熟悉吧,我們平時說的子執行緒不能更新UI,會拋異常,就是在這裡判斷的,ViewRootImpl#checkThread
接著看註釋2
2.2 ViewRootImpl#scheduleTraversals
void scheduleTraversals() { //1、注意這個標誌位,多次呼叫 requestLayout,要這個標誌位false才有效 if (!mTraversalScheduled) { mTraversalScheduled = true; // 2\\. 同步屏障 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 3\\. 向 Choreographer 提交一個任務 mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } //繪製前發一個通知 notifyRendererOfFramePending(); //這個是釋放鎖,先不管 pokeDrawLockIfNeeded(); }}主要看註釋的3點:
註釋1:防止短時間多次呼叫 requestLayout 重複繪製多次,假如呼叫requestLayout 之後還沒有到這一幀繪製完成,再次呼叫是沒什麼意義的。
註釋2: 涉及到Handler的一個知識點,同步屏障: 往訊息佇列插入一個同步屏障訊息,這時候訊息佇列中的同步訊息不會被處理,而是優先處理非同步訊息。這裡很好理解,UI相關的操作優先順序最高,比如訊息佇列有很多沒處理完的任務,這時候啟動一個Activity,當然要優先處理Activity啟動,然後再去處理其他的訊息,同步屏障的設計堪稱一絕吧。 同步屏障的處理程式碼在MessageQueue的next方法:
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為空 // Stalled by a barrier. Find the next asynchronous message in the queue. do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } ...}邏輯就是:如果msg不為空並且target為空,說明是一個同步屏障訊息,進入do while迴圈,遍歷連結串列,直到找到非同步訊息msg.isAsynchronous()才跳出迴圈交給Handler去處理這個非同步訊息。
回到上面的註釋3:mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);,往Choreographer 提交一個任務 mTraversalRunnable,這個任務不會馬上就執行,接著看~
3. Choreographer
看下 mChoreographer.postCallback
3.1 Choreographer#postCallback
public void postCallback(int callbackType, Runnable action, Object token) { postCallbackDelayed(callbackType, action, token, 0);}public void postCallbackDelayed(int callbackType, Runnable action, Object token, long delayMillis) { if (action == null) { throw new IllegalArgumentException("action must not be null"); } if (callbackType < 0 || callbackType > CALLBACK_LAST) { throw new IllegalArgumentException("callbackType is invalid"); } postCallbackDelayedInternal(callbackType, action, token, delayMillis);}private void postCallbackDelayedInternal(int callbackType, Object action, Object token, long delayMillis) { if (DEBUG_FRAMES) { Log.d(TAG, "PostCallback: type=" + callbackType + ", action=" + action + ", token=" + token + ", delayMillis=" + delayMillis); } synchronized (mLock) { final long now = SystemClock.uptimeMillis(); final long dueTime = now + delayMillis; //1.將任務新增到佇列 mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token); //2\\. 正常延時是0,走這裡 if (dueTime <= now) { scheduleFrameLocked(now); } else { //3\\. 什麼時候會有延時,繪製超時,等下一個vsync? Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); msg.arg1 = callbackType; msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, dueTime); } }}入參 callbackType 這裡傳的是 Choreographer.CALLBACK_TRAVERSAL,後面會說到,最終呼叫了 postCallbackDelayedInternal 方法。
註釋1:將任務新增到佇列,不會馬上執行,後面會用到。
註釋2: scheduleFrameLocked,正常的情況下delayMillis是0,走這裡,看下面分析。
註釋3:什麼情況下會有延時,TextView中有呼叫到,暫時不管。
3.2. Choreographer#scheduleFrameLocked
// Enable/disable vsync for animations and drawing. 系統屬性引數,預設trueprivate static final boolean USE_VSYNC = SystemProperties.getBoolean( "debug.choreographer.vsync", true);...private void scheduleFrameLocked(long now) { //標誌位,避免不必要的多次呼叫 if (!mFrameScheduled) { mFrameScheduled = true; if (USE_VSYNC) { if (DEBUG_FRAMES) { Log.d(TAG, "Scheduling next frame on vsync."); } // 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. //1 如果當前執行緒是UI執行緒,直接執行scheduleFrameLocked,否則通過Handler處理 if (isRunningOnLooperThreadLocked()) { scheduleVsyncLocked(); } else { Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC); msg.setAsynchronous(true); mHandler.sendMessageAtFrontOfQueue(msg); } } else { 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); } }}這個方法有個系統引數判斷,預設true,我們分析true的情況。
註釋1: 判斷當前執行緒如果是UI執行緒,直接執行scheduleVsyncLocked方法,否則,通過Handler發一個非同步訊息到訊息佇列,最終也是到主執行緒處理,所以直接看scheduleVsyncLocked方法。
3.3 Choreographer#scheduleVsyncLocked
private final FrameDisplayEventReceiver mDisplayEventReceiver;private void scheduleVsyncLocked() { mDisplayEventReceiver.scheduleVsync();}呼叫 DisplayEventReceiver 的 scheduleVsync 方法
4. DisplayEventReceiver
4.1 DisplayEventReceiver#scheduleVsync
/** * Schedules a single vertical sync pulse to be delivered when the next * display frame begins. */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 { nativeScheduleVsync(mReceiverPtr); //1、請求vsync }}// Called from native code. //2、vsync來的時候底層會通過JNI回撥這個方法@SuppressWarnings("unused")private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) { onVsync(timestampNanos, builtInDisplayId, frame);}這裡的邏輯就是:通過JNI,跟底層說,下一個vsync脈衝訊號來的時候請通知我。 然後在下一個vsync訊號來的時候,就會收到底層的JNI回撥,也就是dispatchVsync這個方法會被呼叫,然後會呼叫onVsync這個空方法,由實現類去自己做一些處理。
/** * Called when a vertical sync pulse is received. * The recipient should render a frame and then call {@link #scheduleVsync} * to schedule the next vertical sync pulse. * * @param timestampNanos The timestamp of the pulse, in the {@link System#nanoTime()} * timebase. * @param builtInDisplayId The surface flinger built-in display id such as * {@link SurfaceControl#BUILT_IN_DISPLAY_ID_MAIN}. * @param frame The frame number. Increases by one for each vertical sync interval. */ public void onVsync(long timestampNanos, int builtInDisplayId, int frame) { }這裡是螢幕重新整理機制的重點,應用必須向底層請求vsync訊號,然後下一次vsync訊號來的時候會通過JNI通知到應用,然後接下來才到應用繪製邏輯。
往回看,DisplayEventReceiver的實現類是 Choreographer 的內部類 FrameDisplayEventReceiver,程式碼不多,直接貼上來
5. Choreographer
5.1 Choreographer$FrameDisplayEventReceiver
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) { // 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; Message msg = Message.obtain(mHandler, this); //1 callback是this,會回撥run方法 msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); } @Override public void run() { mHavePendingVsync = false; doFrame(mTimestampNanos, mFrame); //2 }}根據上面4.1分析,收到vsync訊號後,onVsync方法就會被呼叫,裡面主要做了什麼呢?通過Handler,往訊息佇列插入一個非同步訊息,指定執行的時間,然後看註釋1,callback傳this,所以最終會回撥run方法,run裡面呼叫doFrame(mTimestampNanos, mFrame);,重點來了,如果Handler此時存在耗時操作,那麼需要等耗時操作執行完,Looper才會輪循到下一條訊息,run方法才會呼叫,然後才會呼叫到doFrame(mTimestampNanos, mFrame);,doFrame幹了什麼?呼叫慢了會怎麼樣?繼續看
5.2 Choreographer#doFrame
void doFrame(long frameTimeNanos, int frame) { final long startNanos; synchronized (mLock) { ... long intendedFrameTimeNanos = frameTimeNanos; startNanos = System.nanoTime(); // 1 當前時間戳減去vsync來的時間,也就是主執行緒的耗時時間 final long jitterNanos = startNanos - frameTimeNanos; if (jitterNanos >= mFrameIntervalNanos) { //1幀是16毫秒,計算當前跳過了多少幀,比如超時162毫秒,那麼就是跳過了10幀 final long skippedFrames = jitterNanos / mFrameIntervalNanos; // SKIPPED_FRAME_WARNING_LIMIT 預設是30,超時了30幀以上,那麼就log提示 if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) { Log.i(TAG, "Skipped " + skippedFrames + " frames! " + "The application may be doing too much work on its main thread."); } // 取餘,計算離上一幀多久了,一幀是16毫秒,所以lastFrameOffset 在0-15毫秒之間,這裡單位是納秒 final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; if (DEBUG_JANK) { Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms " + "which is more than the 8frame interval of " + (mFrameIntervalNanos * 0.000001f) + " ms! " + "Skipping " + skippedFrames + " frames and setting frame " + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past."); } // 出現掉幀,把時間修正一下,對比的是上一幀時間 frameTimeNanos = startNanos - lastFrameOffset; } //2、時間倒退了,可能是由於改了系統時間,此時就重新申請vsync訊號(一般不會走這裡) if (frameTimeNanos < mLastFrameTimeNanos) { if (DEBUG_JANK) { Log.d(TAG, "Frame time appears to be going backwards. May be due to a " + "previously skipped frame. Waiting for next vsync."); } //這裡申請下一次vsync訊號,流程跟上面分析一樣了。 scheduleVsyncLocked(); return; } mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos); mFrameScheduled = false; mLastFrameTimeNanos = frameTimeNanos; } //3 能繪製的話,就走到下面 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); mFrameInfo.markPerformTraversalsStart(); doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos); } }分析:
1. 計算收到vsync訊號到doFrame被呼叫的時間差,vsync訊號間隔是16毫秒一次,大於16毫秒就是掉幀了,如果超過30幀(預設30),就列印log提示開發者檢查主執行緒是否有耗時操作。
2. 如果時間發生倒退,可能是修改了系統時間,就不繪製,而是重新註冊下一次vsync訊號 3. 正常情況下會走到 doCallbacks 裡去,callbackType 按順序是Choreographer.CALLBACK_INPUT、Choreographer.CALLBACK_ANIMATION、Choreographer.CALLBACK_TRAVERSAL、Choreographer.CALLBACK_COMMIT
看 doCallbacks 裡的邏輯
5.3 Choreographer#doCallbacks
void doCallbacks(int callbackType, long frameTimeNanos) { CallbackRecord callbacks; synchronized (mLock) { final long now = System.nanoTime(); //1\\. 從佇列取出任務,任務什麼時候新增到佇列的,上面有說過哈 callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS); if (callbacks == null) { return; } mCallbacksRunning = true; ... //2.更新這一幀的時間,確保提交這一幀的時間總是在最後一幀之後 if (callbackType == Choreographer.CALLBACK_COMMIT) { final long jitterNanos = now - frameTimeNanos; Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos); 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 { Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]); for (CallbackRecord c = callbacks; c != null; c = c.next) { if (DEBUG_FRAMES) { Log.d(TAG, "RunCallback: type=" + callbackType + ", action=" + c.action + ", token=" + c.token + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime)); } // 3\\. 執行任務, c.run(frameTimeNanos); } } ...}這裡主要就是取出對應型別的任務,然後執行任務。
註釋2:if (callbackType == Choreographer.CALLBACK_COMMIT)是流程的最後一步,資料已經繪製完準備提交的時候,會更正一下時間戳,確保提交時間總是在最後一次vsync時間之後。這裡文字可能不太好理解,引用一張圖
圖中 doCallbacks 從 frameTimeNanos2 開始執行,執行到進入 CALLBACK_COMMIT 時,經過了2.2幀,判斷 now - frameTimeNanos >= 2 * mFrameIntervalNanos,lastFrameOffset = jitterNanos % mFrameIntervalNanos取餘就是0.2了,於是修正的時間戳 frameTimeNanos = now - lastFrameOffset 剛好就是3的位置。
註釋3,還沒到最後一步的時候,取出其它任務出來run,這個任務肯定就是跟View的繪製相關了,記得開始requestLayout傳過來的型別嗎,Choreographer.CALLBACK_TRAVERSAL,從佇列get出來的任務類對應是mTraversalRunnable,型別是TraversalRunnable,定義在ViewRootImpl裡面,饒了一圈,回到ViewRootImpl繼續看~
6. ViewRootImpl
剛開始看的是ViewRootImpl#scheduleTraversals,繼續往下分析
6.1 ViewRootImpl#scheduleTraversals
void scheduleTraversals() { if (!mTraversalScheduled) { ... mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); }}這個mTraversalRunnable 任務繞了一圈,通過請求vsync訊號,到收到訊號,然後終於被呼叫了。
6.2 ViewRootImpl$TraversalRunnable
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); }}6.3 ViewRootImpl#doTraversal
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; //移除同步屏障 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); performTraversals(); } }先移除同步屏障訊息,然後呼叫performTraversals 方法, performTraversals 這個方法程式碼有點多,挑重點看
6.4 ViewRootImpl#performTraversals
private void performTraversals() { // mAttachInfo 賦值給View host.dispatchAttachedToWindow(mAttachInfo, 0); // Execute enqueued actions on every traversal in case a detached view enqueued an action getRunQueue().executeActions(mAttachInfo.mHandler); ... //1 測量 if (!mStopped || mReportNextDraw) { // Ask host how big it wants to be //1.1測量一次 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); / Implementation of weights from WindowManager.LayoutParams // We just grow the dimensions as needed and re-measure if // needs be if (lp.horizontalWeight > 0.0f) { width += (int) ((mWidth - width) * lp.horizontalWeight); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); measureAgain = true; } if (lp.verticalWeight > 0.0f) { height += (int) ((mHeight - height) * lp.verticalWeight); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); measureAgain = true; } //1.2、如果有設定權重,比如LinearLayout設定了weight,需要測量兩次 if (measureAgain) { performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } } ... //2.佈局 if (didLayout) { // 會回撥View的layout方法,然後會呼叫View的onLayout方法 performLayout(lp, mWidth, mHeight); } ... //3.畫 if (!cancelDraw && !newSurface) { performDraw(); } }可以看到,View的三個方法回撥measure、layout、draw是在performTraversals 裡面,需要注意的點是LinearLayout設定權重的情況下會measure兩次。
到這裡,螢幕重新整理機制就分析完了,整個流程總結一下:
7. 小結
View 的 requestLayout 會調到ViewRootImpl 的 requestLayout方法,然後通過 scheduleTraversals 方法向Choreographer 提交一個繪製任務,然後再通過DisplayEventReceiver向底層請求vsync訊號,當vsync訊號來的時候,會通過JNI回調回來,通過Handler往主執行緒訊息佇列post一個非同步任務,最終是ViewRootImpl去執行那個繪製任務,呼叫performTraversals方法,裡面是View的三個方法的回撥。
網上的流程圖雖然很漂亮,但是不如自己畫一張印象深刻
認真看完,想必大家對螢幕重新整理機制應該清楚了:
應用需要主動請求vsync,vsync來的時候才會通過JNI通知到應用,然後才呼叫View的三個繪製方法。如果沒有發起繪製請求,例如沒有requestLayout,View的繪製方法是不會被呼叫的。ViewRootImpl裡面的這個View其實是DecorView。
那麼有兩個地方會造成掉幀,一個是主執行緒有其它耗時操作,導致doFrame沒有機會在vsync訊號發出之後16毫秒內呼叫,對應下圖的3;還有一個就是當前doFrame方法耗時,繪製太久,下一個vsync訊號來的時候這一幀還沒畫完,造成掉幀,對應下圖的2。1是正常的
這一張圖很形象,大家可以參考這張圖自己研究研究。 關於Choreographer如果還有不了解的地方,我看這篇文章寫的還不錯:Choreographer 解析。
二、如何監控應用卡頓?上面從原始碼角度分析了螢幕重新整理機制,為什麼主執行緒有耗時操作會導致卡頓?原理想必大家已經心中有數,那麼平時開發中如何去發現那些會造成卡頓的程式碼呢?
接下來總結幾種比較流行、有效的卡頓監控方式:
2.1 基於訊息佇列
2.1.1 替換 Looper 的 Printer
Looper 暴露了一個方法
public void setMessageLogging(@Nullable Printer printer) { mLogging = printer; }在Looper 的loop方法有這樣一段程式碼
public static void loop() { ... for (;;) { ... // This must be in a local variable, in case a UI event sets the logger final Printer logging = me.mLogging; if (logging != null) { logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what); }Looper輪循的時候,每次從訊息佇列取出一條訊息,如果logging不為空,就會呼叫 logging.println,我們可以通過設定Printer,計算Looper兩次獲取訊息的時間差,如果時間太長就說明Handler處理時間過長,直接把堆疊資訊打印出來,就可以定位到耗時程式碼。不過println 方法引數涉及到字串拼接,考慮效能問題,所以這種方式只推薦在Debug模式下使用。基於此原理的開源庫代表是:BlockCanary,看下BlockCanary核心程式碼:
類:LooperMonitor
public void println(String x) { if (mStopWhenDebugging && Debug.isDebuggerConnected()) { return; } if (!mPrintingStarted) { //1、記錄第一次執行時間,mStartTimestamp mStartTimestamp = System.currentTimeMillis(); mStartThreadTimestamp = SystemClock.currentThreadTimeMillis(); mPrintingStarted = true; startDump(); //2、開始dump堆疊資訊 } else { //3、第二次就進來這裡了,呼叫isBlock 判斷是否卡頓 final long endTime = System.currentTimeMillis(); mPrintingStarted = false; if (isBlock(endTime)) { notifyBlockEvent(endTime); } stopDump(); //4、結束dump堆疊資訊 } } //判斷是否卡頓的程式碼很簡單,跟上次處理訊息時間比較,比如大於3秒,就認為卡頓了 private boolean isBlock(long endTime) { return endTime - mStartTimestamp > mBlockThresholdMillis; }原理是這樣,比較Looper兩次處理訊息的時間差,比如大於3秒,就認為卡頓了。細節的話大家可以自己去研究原始碼,比如訊息佇列只有一條訊息,隔了很久才有訊息入隊,這種情況應該是要處理的,BlockCanary是怎麼處理的呢?
這個我在BlockCanary 中測試,並沒有出現此問題,所以BlockCanary 是怎麼處理的?簡單分析一下原始碼:
上面這段程式碼,註釋1和註釋2,記錄第一次處理的時間,同時呼叫startDump()方法,startDump()最終會通過Handler 去執行一個AbstractSampler 類的mRunnable,程式碼如下:
abstract class AbstractSampler { private static final int DEFAULT_SAMPLE_INTERVAL = 300; protected AtomicBoolean mShouldSample = new AtomicBoolean(false); protected long mSampleInterval; private Runnable mRunnable = new Runnable() { @Override public void run() { doSample(); //呼叫startDump 的時候設定true了,stop時設定false if (mShouldSample.get()) { HandlerThreadFactory.getTimerThreadHandler() .postDelayed(mRunnable, mSampleInterval); } } };可以看到,呼叫doSample之後又通過Handler執行mRunnable,等於是迴圈呼叫doSample,直到stopDump被呼叫。
doSample方法有兩個類實現,StackSampler和CpuSampler,分析堆疊就看StackSampler的doSample方法
protected void doSample() { StringBuilder stringBuilder = new StringBuilder(); // 獲取堆疊資訊 for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) { stringBuilder .append(stackTraceElement.toString()) .append(BlockInfo.SEPARATOR); } synchronized (sStackMap) { // LinkedHashMap中資料超過100個就remove掉連結串列最前面的 if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) { sStackMap.remove(sStackMap.keySet().iterator().next()); } //放入LinkedHashMap,時間作為key,value是堆疊資訊 sStackMap.put(System.currentTimeMillis(), stringBuilder.toString()); } }所以,BlockCanary 能做到連續呼叫幾個方法也能準確揪出耗時是哪個方法,是採用開啟迴圈去獲取堆疊資訊並儲存到LinkedHashMap的方式,避免出現誤判或者漏判。核心程式碼就先分析到這裡,其它細節大家可以自己去看原始碼。
2.1.2 插入空訊息到訊息佇列
這種方式可以了解一下。
通過一個監控執行緒,每隔1秒向主執行緒訊息佇列的頭部插入一條空訊息。假設1秒後這個訊息並沒有被主執行緒消費掉,說明阻塞訊息執行的時間在0~1秒之間。換句話說,如果我們需要監控3秒卡頓,那在第4次輪詢中,頭部訊息依然沒有被消費的話,就可以確定主執行緒出現了一次3秒以上的卡頓。
2.2 插樁
編譯過程插樁(例如使用AspectJ),在方法入口和出口加入耗時監控的程式碼。 原來的方法:
public void test(){ doSomething();}通過編譯插樁之後的方法類似這樣
public void test(){ long startTime = System.currentTimeMillis(); doSomething(); long methodTime = System.currentTimeMillis() - startTime;//計算方法耗時}當然,原理是這樣,實際上可能需要封裝一下,類似這樣
public void test(){ methodStart(); doSomething(); methodEnd();}在每個要監控的方法的入口和出口分別加上methodStart和methodEnd兩個方法,類似插樁埋點。
當然,這種插樁的方法缺點比較明顯:
無法監控系統方法apk體積會增大(每個方法都多了程式碼)需要注意:
過濾簡單的方法只需要監控主執行緒執行的方法2.3 其它
作為擴充套件:
Facebook 開源的:Profilo
三、總結這篇文章圍繞卡頓這個話題
從原始碼角度分析了螢幕重新整理機制,底層每間隔16毫秒會發出vsyn訊號,應用介面要更新,必須先向底層請求vsync訊號,這樣下一個16毫秒vsync訊號來的時候,底層會通過JNI通知到應用,然後通過主執行緒Handler執行View的繪製任務。所以兩個地方會造成卡頓,一個是主執行緒在執行耗時操作導致View的繪製任務沒有及時執行,還有一個是View繪製太久,可能是層級太多,或者裡面繪製演算法太複雜,導致沒能在下一個vsync訊號來臨之前準備完資料,導致掉幀卡頓。介紹目前比較流行的幾種卡頓監控方式,基於訊息佇列的代表BlockCanary原理,以及通過編譯插樁的方式在每個方法入口和出口加入計算方法耗時的程式碼的方式。面試中應對卡頓問題,可以圍繞卡頓原理、螢幕重新整理機制、卡頓監控這幾個方面來回答,當然,卡頓監控這一塊,還可以通過TraceView、SysTrace等工具來找出卡頓程式碼。在BlockCanary出現之前,TraceView、Systrace是開發者必備的卡頓分析工具,而現在,能把BlockCanary原理講清楚我認為就很不錯了,而對於廠商做系統App開發維護的,不會輕易接入開源庫,所以就有必要去了解TraceView、Systrace工具的使用。
本文主要介紹卡頓原理和卡頓監控,至於View具體是怎麼繪製的,軟體繪製和硬體繪製的區別,繪製流程走完之後,如何更新到螢幕,這個涉及到的內容很多,以後有時間會整理一下。
本文素材來自:藍師傅~~