1. 程式人生 > 其它 >android webview卡頓檢測_效能優化-介面卡頓和丟幀(Choreographer 程式碼檢測)

android webview卡頓檢測_效能優化-介面卡頓和丟幀(Choreographer 程式碼檢測)

技術標籤:android webview卡頓檢測

標籤: Choreographer UI卡頓 UI丟幀


7039b6e4bb37d8adc7c129a08d02b9ed.png

本文將介紹3個知識點:

  1. 獲取系統UI重新整理頻率
  2. 檢測UI丟幀和卡頓
  3. 輸出UI丟幀和卡頓堆疊資訊
d55b2fc7472d8f62888b214bde3ecb88.png

系統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,從而檢測到任何大於或等於該時間的耗時操作。