Android 檢測(cè)UI卡頓

Android 檢測(cè)UI卡頓

相關(guān)工具代碼可以在這里找到:

  • BlockDetect 檢測(cè)應(yīng)用在UI線程的卡頓,打印出卡頓時(shí)調(diào)用堆棧。
  • FrameAnalyze 幀分析工具類(lèi),一個(gè)打印幀率和丟幀情況log的工具類(lèi),原理是利用Choreographer的FrameCallback。

補(bǔ)充:

看到騰訊Bugly的一篇關(guān)于檢測(cè)UI卡頓的文章,整體思路和下文相似,而且總結(jié)的更全面,更是有生產(chǎn)環(huán)境上的實(shí)施經(jīng)驗(yàn)分享,十分值得學(xué)習(xí),放在這里以供參考:《廣研Android卡頓監(jiān)控系統(tǒng)》

文章內(nèi)容

原文地址
在實(shí)際開(kāi)發(fā)中,經(jīng)常會(huì)碰到UI卡頓的現(xiàn)象,為方便定位問(wèn)題原因,能在UI卡頓時(shí)或者UI線程執(zhí)行耗時(shí)操作時(shí)打印出調(diào)用堆棧是非常有必要的。目前有兩種典型方法來(lái)檢測(cè):

  1. 利用UI線程Looper打印的日志
  2. 利用Choreographer

兩種方式都有一些開(kāi)源項(xiàng)目,例如:

另外,還有一種非常規(guī)的方式,是hack掉Looper.loop()方法,自己實(shí)現(xiàn)loop方法來(lái)處理Message的方式:

  • https://github.com/android-notes/Cockroach
    該項(xiàng)目主要用于捕獲UI線程的crash,這里也可以用來(lái)作為檢測(cè)卡頓方案,或者也可能可以做一些別的事情。

一、利用loop()中打印的日志

在UI線程中通過(guò)Looper,在其loop()方法中不斷取出Message,調(diào)用其綁定的Handler在UI線程中執(zhí)行。

public static void loop() {
    final Looper me = myLooper();

    final MessageQueue queue = me.mQueue;
    // ...
    for (;;) {
        Message msg = queue.next(); // might block
        // This must be in a local variable, in case a UI event sets the logger
        Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        // focus
        msg.target.dispatchMessage(msg);

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // ...
        }
        msg.recycleUnchecked();
    }
}

所以,我們只要能檢測(cè):msg.target.dispatchMessage(msg) 的執(zhí)行時(shí)間,就能夠檢測(cè)到UI操作上是否有耗時(shí)操作了,可以看到此行代碼前后,如果設(shè)置了logging,會(huì)分別打印出>>>>> Dispathcing to<<<<< Finished to這樣的log。

我們可以匹配這兩個(gè)log,得到兩次log之間的時(shí)間差值,如果差值打印時(shí)間閾值,就打印出UI線程的堆棧信息(在非UI線程執(zhí)行),這里閾值設(shè)置為1000ms,正常情況下,UI線程操作肯定是低于1000ms執(zhí)行完成的。

二、利用Choreographer

Android系統(tǒng)每隔16ms發(fā)出VSYNC信號(hào),觸發(fā)對(duì)UI進(jìn)行渲染。SDK中包含了一個(gè)相關(guān)類(lèi),以及相關(guān)回調(diào)。理論上來(lái)說(shuō)兩次回調(diào)的時(shí)間周期應(yīng)該在16ms,如果超過(guò)了16ms我們則認(rèn)為發(fā)生了卡頓,我們主要就是利用兩次回調(diào)間的時(shí)間周期來(lái)判斷。

三、利用Looper機(jī)制 (非常規(guī))

先看一段代碼:

new Handler(Looper.getMainLooper())
        .post(new Runnable() {
            @Override
            public void run() {}
       }

該代碼在UI線程中的MessageQueue中插入一個(gè)Message,最終會(huì)在loop()方法中取出并執(zhí)行。
假設(shè),我在run方法中,拿到MessageQueue,自己執(zhí)行原本的Looper.loop()方法邏輯,那么后續(xù)的UI線程的Message就會(huì)將直接讓我們處理,這樣我們就可以做一些事情:

public class BlockDetectByLooper {
    private static final String FIELD_mQueue = "mQueue";
    private static final String METHOD_next = "next";

    public static void start() {
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                try {
                    Looper mainLooper = Looper.getMainLooper();
                    final Looper me = mainLooper;
                    final MessageQueue queue;
                    Field fieldQueue = me.getClass().getDeclaredField(FIELD_mQueue);
                    fieldQueue.setAccessible(true);
                    queue = (MessageQueue) fieldQueue.get(me);
                    Method methodNext = queue.getClass().getDeclaredMethod(METHOD_next);
                    methodNext.setAccessible(true);
                    Binder.clearCallingIdentity();
                    for (; ; ) {
                        Message msg = (Message) methodNext.invoke(queue);
                        if (msg == null) {
                            return;
                        }
                        LogMonitor.getInstance().startMonitor();
                        msg.getTarget().dispatchMessage(msg);
                        msg.recycle();
                        LogMonitor.getInstance().removeMonitor();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        });
    }
}

其實(shí)很簡(jiǎn)單,將Looper.loop里面本身的代碼直接copy來(lái)了這里。當(dāng)這個(gè)消息被處理后,后續(xù)的消息都將會(huì)在這里進(jìn)行處理。
中間有變量和方法需要反射來(lái)調(diào)用,不過(guò)不影響查看msg.getTarget().dispatchMessage(msg);執(zhí)行時(shí)間,但是就不要在線上使用這種方式了。
不過(guò)該方式和以上兩個(gè)方案對(duì)比,并無(wú)優(yōu)勢(shì),不過(guò)這個(gè)思路挺有意思的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容