android webview卡頓檢測_效能優化-介面卡頓和丟幀(Choreographer 程式碼檢測)
技術標籤:android webview卡頓檢測
標籤: Choreographer UI卡頓 UI丟幀
本文將介紹3個知識點:
- 獲取系統UI重新整理頻率
- 檢測UI丟幀和卡頓
- 輸出UI丟幀和卡頓堆疊資訊
系統UI重新整理頻率
Android系統每隔16ms重繪UI介面,16ms是因為Android系統規定UI繪圖的重新整理頻率60FPS。Android系統每隔16ms,傳送一個系統級別訊號VSYNC喚起重繪操作。1秒內繪製UI介面60次。每16ms為一個UI介面繪製週期。 現在有些手機廠商的手機螢幕重新整理頻率已經是120FPS,每隔8.3毫秒重繪UI介面; 獲取系統UI重新整理頻率
private float getRefreshRate() { //獲取螢幕主頻頻率 Display display = getWindowManager().getDefaultDisplay(); float refreshRate = display.getRefreshRate(); Log.d(TAG, "螢幕主頻頻率 =" + refreshRate); return refreshRate; }
log列印如下:
D/MainActivity: 螢幕主頻頻率 =60.0
UI丟幀和卡頓檢查-Choreographer
平常所說的“丟幀”情況,並不是真的把繪圖的幀給“丟失”了,也而是UI繪圖的操作沒有和系統16ms的繪圖更新頻率步調一致,開發者程式碼在繪圖中繪製操作太多,導致操作的時間超過16ms,在Android系統需要在16ms時需要重繪的時刻由於UI執行緒被阻塞而繪製失敗。如果丟的幀數量是一兩幀,使用者在視覺上沒有明顯感覺,但是如果超過3幀,使用者就有視覺上的感知。丟幀數如果再持續增多,在視覺上就是所謂的“卡頓”。
丟幀是引起卡頓的重要原因。在Android中可以通過Choreographer檢測Android系統的丟幀情況。
public class MainActivity extends Activity { ... private MyFrameCallback mFrameCallback = new MyFrameCallback(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Choreographer.getInstance().postFrameCallback(mFrameCallback); MYTest(); button = findViewById(R.id.bottom); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { uiLongTimeWork(); Log.d(MainActivity.class.getSimpleName(), "button click"); } }); } private void MYTest() { setContentView(R.layout.activity_main); Log.d(MainActivity.class.getSimpleName(), "MYTest"); } private float getRefreshRate() { //獲取螢幕主頻頻率 Display display = getWindowManager().getDefaultDisplay(); float refreshRate = display.getRefreshRate();// Log.d(TAG, "螢幕主頻頻率 =" + refreshRate); return refreshRate; } @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) public class MyFrameCallback implements Choreographer.FrameCallback { private String TAG = "效能檢測"; private long lastTime = 0; @Override public void doFrame(long frameTimeNanos) { if (lastTime == 0) { //程式碼第一次初始化。不做檢測統計。 lastTime = frameTimeNanos; } else { long times = (frameTimeNanos - lastTime) / 1000000; int frames = (int) (times / (1000/getRefreshRate())); if (times > 16) { Log.w(TAG, "UI執行緒超時(超過16ms):" + times + "ms" + " , 丟幀:" + frames); } lastTime = frameTimeNanos; } Choreographer.getInstance().postFrameCallback(mFrameCallback); } } private void uiLongTimeWork() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }
Choreographer週期性的在UI重繪時候觸發,在程式碼中記錄上一次和下一次繪製的時間間隔,如果超過16ms,就意味著一次UI執行緒重繪的“丟幀”。丟幀的數量為間隔時間除以16,如果超過3,就開始有卡頓的感知。 Log如下
W/效能檢測: UI執行緒超時(超過16ms):33ms , 丟幀:1W/效能檢測: UI執行緒超時(超過16ms):19ms , 丟幀:1W/效能檢測: UI執行緒超時(超過16ms):1016ms , 丟幀:60W/效能檢測: UI執行緒超時(超過16ms):24ms , 丟幀:1W/效能檢測: UI執行緒超時(超過16ms):21ms , 丟幀:1W/效能檢測: UI執行緒超時(超過16ms):1016ms , 丟幀:60W/效能檢測: UI執行緒超時(超過16ms):23ms , 丟幀:1W/效能檢測: UI執行緒超時(超過16ms):33ms , 丟幀:1
如果手動點選按鈕故意阻塞1秒,丟棄的幀數更多。丟幀:60,就是點選button按鈕,執行uiLongTimeWork產生的;
UI丟幀和卡頓堆疊資訊輸出
以上是“UI丟幀和卡頓檢查-Choreographer”使用Android的Choreographer監測App發生的UI卡頓丟幀問題。Choreographer本身依賴於Android主執行緒的Looper訊息機制。 發生在Android主執行緒的每(1000/UI重新整理頻率)ms重繪操作依賴於Main Looper中訊息的傳送和獲取。如果App一切執行正常,無卡頓無丟幀現象發生,那麼開發者的程式碼在主執行緒Looper訊息佇列中傳送和接收訊息的時間會很短,理想情況是(1000/UI重新整理頻率)ms,這是也是Android系統規定的時間。但是,如果一些發生在主執行緒的程式碼寫的太重,執行任務花費時間太久,就會在主執行緒延遲Main Looper的訊息在(1000/UI重新整理頻率)ms尺度範圍內的讀和寫。
先看下Android官方實現的Looper中loop()函式程式碼官方實現:
/** * Run the message queue in this thread. Be sure to call * {@link #quit()} to end the loop. */ public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; // Make sure the identity of this thread is that of the local process, // and keep track of what that identity token actually is. Binder.clearCallingIdentity(); final long ident = Binder.clearCallingIdentity(); for (;;) { Message msg = queue.next(); // might block if (msg == null) { // No message indicates that the message queue is quitting. return; } // 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); } final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs; final long traceTag = me.mTraceTag; if (traceTag != 0 && Trace.isTagEnabled(traceTag)) { Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); } final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); final long end; try { msg.target.dispatchMessage(msg); end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); } finally { if (traceTag != 0) { Trace.traceEnd(traceTag); } } if (slowDispatchThresholdMs > 0) { final long time = end - start; if (time > slowDispatchThresholdMs) { Slog.w(TAG, "Dispatch took " + time + "ms on " + Thread.currentThread().getName() + ", h=" + msg.target + " cb=" + msg.callback + " msg=" + msg.what); } } if (logging != null) { logging.println("<<<<< Finished to " + msg.target + " " + msg.callback); } // Make sure that during the course of dispatching the // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); if (ident != newIdent) { Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " " + msg.callback + " what=" + msg.what); } msg.recycleUnchecked(); } }
在loop()函式中,Android完成了Looper訊息佇列的分發,在分發訊息開始,會列印一串log日誌:
logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);
同時在訊息處理結束後也會列印一串訊息日誌:
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
正常的情況下,分發訊息開始到訊息結束,理想的情況下應該在(1000/UI重新整理頻率)ms以內。但是分發處理的訊息到上層,由開發者程式碼接管並處理,如果耗時太久,就很可能超出(1000/UI重新整理頻率)ms,也即發生了丟幀,超時太多,由於Android系統依賴主執行緒Looper重繪UI的訊息遲遲得不到處理,那麼就導致繪圖動作停滯,使用者視覺上就會感受到卡頓。 利用這一特性和情景,可以使用主執行緒的Looper監測系統發生的卡頓和丟幀。具體是這樣的: 首先給App的主執行緒Looper註冊一個自己的訊息日誌輸出列印器,正常情況下,該日誌列印器將輸出全部的Android Looper上的日誌,但是在這裡,技巧性的過濾兩個特殊日誌:
>>>>> Dispatching to
表示Looper開始分發主執行緒上的訊息。
<<<<< Finished to
表示Looper分發主執行緒上的消失結束。 從>>>>> Dispatching to 到 <<<<< Finished to 之間這段操作,就是留給開發者所寫的程式碼發生在上層主執行緒操作的動作,通常所說的卡頓也就發生這一段。
正確情況下,從訊息分發(>>>>> Dispatching to)開始,到訊息處理結束(<<<<< Finished to),這段操作理想情況應在(1000/UI重新整理頻率)ms以內完成,如果超過這一時間,也即意味著卡頓和丟幀。
現在設計一種技巧性的程式設計方案:在(>>>>> Dispatching to)開始時候,延時一定時間(THREADHOLD)執行一個執行緒,延時時間為THREADHOLD,該執行緒只完成列印當前Android堆疊的資訊。THREADHOLD即為開發者意圖捕捉到的超時時間。如果沒什麼意外,該執行緒在THREADHOLD後,就打印出當前Android的堆疊資訊。巧就巧妙在利用這一點兒,因為延時THREADHOLD執行的執行緒和主執行緒Looper中的執行緒是並行執行的,當在>>>>> Dispatching to時刻把延時執行緒任務構建完丟擲去等待THREADHOLD後執行,而當前的Looper執行緒中的訊息分發也在執行,這兩個是併發執行的不同執行緒。 設想如果Looper執行緒中的操作程式碼很快就執行完畢,不到16ms就到了<<<<< Finished to,那麼毫無疑問當前的主執行緒無卡頓和丟幀發生。如果特意把THREADHOLD設定成大於16ms的延時時間,比如1000ms,如果執行緒執行順暢不卡頓無丟幀,那麼從>>>>> Dispatching to到達<<<<< Finished to後,把延時THREADHOLD執行的執行緒刪除掉,那麼執行緒就不會輸出任何堆疊資訊。若不行主執行緒發生阻塞,當從>>>>> Dispatching to到達<<<<< Finished to花費1000ms甚至更長時間後,而由於到達<<<<< Finished to的時候沒來得及把堆疊列印執行緒刪除掉,因此就輸出了當前堆疊資訊,此堆疊資訊剛好即為發生卡頓和丟幀的程式碼堆疊,正好就是所需的卡頓和丟幀檢測程式碼。
public class MainActivity extends Activity { ... private CheckTask mCheckTask = new CheckTask(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); check(); ... button = findViewById(R.id.bottom); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { uiLongTimeWork(); Log.d(MainActivity.class.getSimpleName(), "button click"); } }); } private void check() { Looper.getMainLooper().setMessageLogging(new Printer() { private final String START = ">>>>> Dispatching to"; private final String END = "<<<<< Finished to"; @Override public void println(String s) { if (s.startsWith(START)) { mCheckTask.start(); } else if (s.startsWith(END)) { mCheckTask.end(); } } }); } private void uiLongTimeWork() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } private class CheckTask { private HandlerThread mHandlerThread = new HandlerThread("卡頓檢測"); private Handler mHandler; private final int THREAD_HOLD = 1000; public CheckTask() { mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); } private Runnable mRunnable = new Runnable() { @Override public void run() { log(); } }; public void start() { mHandler.postDelayed(mRunnable, THREAD_HOLD); } public void end() { mHandler.removeCallbacks(mRunnable); } } /** * 輸出當前異常或及錯誤堆疊資訊。 */ private void log() { StringBuilder sb = new StringBuilder(); StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace(); for (StackTraceElement s : stackTrace) { sb.append(s + ""); } Log.w(TAG, sb.toString()); }
執行輸出:
1970-02-14 17:35:06.367 11590-11590/com.yanbing.aop_project D/MainActivity: button click1970-02-14 17:35:06.367 11590-11611/com.yanbing.aop_project W/MainActivity: java.lang.String.indexOf(String.java:1658) java.lang.String.indexOf(String.java:1638) java.lang.String.contains(String.java:2126) java.lang.Class.classNameImpliesTopLevel(Class.java:1169) java.lang.Class.getEnclosingConstructor(Class.java:1159) java.lang.Class.isLocalClass(Class.java:1312) java.lang.Class.getSimpleName(Class.java:1219) com.yanbing.aop_project.MainActivity$2.onClick(MainActivity.java:71) android.view.View.performClick(View.java:6294) android.view.View$PerformClick.run(View.java:24770) android.os.Handler.handleCallback(Handler.java:790) android.os.Handler.dispatchMessage(Handler.java:99) android.os.Looper.loop(Looper.java:164) android.app.ActivityThread.main(ActivityThread.java:6494) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
可以看到當點選按鈕故意製造一個卡頓後,卡頓被檢測到,並且輸出和定位到了卡頓的具體程式碼位置。 總結:利用主執行緒的Looper檢測卡頓和丟幀,從成對的訊息分發(>>>>> Dispatching to),到訊息處理結束(<<<<< Finished to),正常的理想時間應該在16ms以內,若當前程式碼耗時太多,這一段時間就會超過16ms。假設現在要檢測耗時超過1秒(1000ms)的耗時操作,那就在>>>>> Dispatching to時刻,丟擲一個延時執行的執行緒,該執行緒列印當前堆疊的資訊,延時的時間特意設定成閾值1000。此種情況下,正常順暢執行無卡頓無丟幀的程式碼從>>>>> Dispatching to到<<<<< Finished to之間不會超過設定的閾值1000ms,因此當Looper中的程式碼到達<<<<< Finished to就把之前丟擲來延時執行的執行緒刪除掉,也就不會輸出任何堆疊資訊。但是隻有當耗時程式碼從>>>>> Dispatching to到<<<<< Finished to超過了1000ms,由於Looper中由於耗時操作很晚(超過我們設定的閾值)才到達<<<<< Finished to,沒趕上刪掉堆疊列印執行緒,於是堆疊執行緒得以有機會列印當前堆疊資訊,這就是卡頓和丟幀的發生場景檢測機制。 事實上可以靈活設定延時閾值THREAD_HOLD,從而檢測到任何大於或等於該時間的耗時操作。