Android UI性能優化 檢測應用中的UI卡頓

一、概述

在做app性能優化的時候,大家都希望能夠寫出絲滑的UI界面,以前寫過一篇博客,主要是基于Google當時發布的性能優化典范,主要提供一些UI優化性能示例:

實際上,由于各種機型的配置不同、代碼迭代歷史悠久,代碼中可能會存在很多在UI線程耗時的操作,所以我們希望有一套簡單檢測機制,幫助我們定位耗時發生的位置。

本篇博客主要描述如何檢測應用在UI線程的卡頓,目前已經有兩種比較典型方式來檢測了:

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

兩種方式都有一些開源項目,例如:

其實編寫本篇文章,主要是因為發現一個還比較有意思的方案,該方法的靈感來源于一篇給我微信投稿的文章:

該項目主要用于捕獲UI線程的crash,當我看完該項目原理的時候,也可以用來作為檢測卡段方案,可能還可以做一些別的事情。

所以,本文出現了3種檢測UI卡頓的方案,3種方案原理都比較簡單,接下來將逐個介紹。

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

(1)原理

大家都知道在Android UI線程中有個Looper,在其loop方法中會不斷取出Message,調用其綁定的Handler在UI線程進行執行。

大致代碼如下:

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();
}

}</code></pre>

所以很多時候,我們只要有辦法檢測:

msg.target.dispatchMessage(msg);

此行代碼的執行時間,就能夠檢測到部分UI線程是否有耗時操作了。可以看到在執行此代碼前后,如果設置了logging,會分別打印出 >>>>> Dispatching to 和 <<<<< Finished to 這樣的log。

我們可以通過計算兩次log之間的時間差值,大致代碼如下:

public class BlockDetectByPrinter {

public static void start() {

    Looper.getMainLooper().setMessageLogging(new Printer() {

        private static final String START = ">>>>> Dispatching";
        private static final String END = "<<<<< Finished";

        @Override
        public void println(String x) {
            if (x.startsWith(START)) {
                LogMonitor.getInstance().startMonitor();
            }
            if (x.startsWith(END)) {
                LogMonitor.getInstance().removeMonitor();
            }
        }
    });

}

}</code></pre>

假設我們的閾值是1000ms,當我在匹配到 >>>>> Dispatching 時,我會在1000ms毫秒后執行一個任務(打印出UI線程的堆棧信息,會在非UI線程中進行);正常情況下,肯定是低于1000ms執行完成的,所以當我匹配到 <<<<< Finished ,會移除該任務。

大概代碼如下:

public class LogMonitor {

private static LogMonitor sInstance = new LogMonitor();
private HandlerThread mLogThread = new HandlerThread("log");
private Handler mIoHandler;
private static final long TIME_BLOCK = 1000L;

private LogMonitor() {
    mLogThread.start();
    mIoHandler = new Handler(mLogThread.getLooper());
}

private static Runnable mLogRunnable = new Runnable() {
    @Override
    public void run() {
        StringBuilder sb = new StringBuilder();
        StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
        for (StackTraceElement s : stackTrace) {
            sb.append(s.toString() + "\n");
        }
        Log.e("TAG", sb.toString());
    }
};

public static LogMonitor getInstance() {
    return sInstance;
}

public boolean isMonitor() {
    return mIoHandler.hasCallbacks(mLogRunnable);
}

public void startMonitor() {
    mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK);
}

public void removeMonitor() {
    mIoHandler.removeCallbacks(mLogRunnable);
}

}</code></pre>

我們利用了HandlerThread這個類,同樣利用了Looper機制,只不過在非UI線程中,如果執行耗時達到我們設置的閾值,則會執行 mLogRunnable ,打印出UI線程當前的堆棧信息;如果你閾值時間之內完成,則會remove掉該runnable。

(2)測試

用法很簡單,在Application的onCreate中調用:

BlockDetectByPrinter.start();

即可。

然后我們在Activity里面,點擊一個按鈕,讓睡眠2s,測試下:

findViewById(R.id.id_btn02)
    .setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
        }
    });

運行點擊時,會打印出log:

02-21 00:26:26.408 2999-3014/com.zhy.testlp E/TAG: 
java.lang.VMThread.sleep(Native Method)
   java.lang.Thread.sleep(Thread.java:1013)
   java.lang.Thread.sleep(Thread.java:995)
   com.zhy.testlp.MainActivity$2.onClick(MainActivity.java:70)
   android.view.View.performClick(View.java:4438)
   android.view.View$PerformClick.run(View.java:18422)
   android.os.Handler.handleCallback(Handler.java:733)
   android.os.Handler.dispatchMessage(Handler.java:95)

會打印出耗時相關代碼的信息,然后可以通過該log定位到耗時的地方。

三、 利用Choreographer

Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染。SDK中包含了一個相關類,以及相關回調。理論上來說兩次回調的時間周期應該在16ms,如果超過了16ms我們則認為發生了卡頓,我們主要就是利用兩次回調間的時間周期來判斷:

大致代碼如下:

public class BlockDetectByChoreographer {
    public static void start() {
        Choreographer.getInstance()
            .postFrameCallback(new Choreographer.FrameCallback() {
                @Override
                public void doFrame(long l) {
                    if (LogMonitor.getInstance().isMonitor()) {
                        LogMonitor.getInstance().removeMonitor();                    
                    } 
                    LogMonitor.getInstance().startMonitor();
                    Choreographer.getInstance().postFrameCallback(this);
                }
        });
    }
}

第一次的時候開始檢測,如果大于閾值則輸出相關堆棧信息,否則則移除。

使用方式和上述一致。

四、 利用Looper機制

先看一段代碼:

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

該代碼在UI線程中的MessageQueue中插入一個Message,最終會在loop()方法中取出并執行。

假設,我在run方法中,拿到MessageQueue,自己執行原本的 Looper.loop() 方法邏輯,那么后續的UI線程的Message就會將直接讓我們處理,這樣我們就可以做一些事情:

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();
            }

        }
    });
}

}</code></pre>

其實很簡單,將Looper.loop里面本身的代碼直接copy來了這里。當這個消息被處理后,后續的消息都將會在這里進行處理。

中間有變量和方法需要反射來調用,不過不影響查看 msg.getTarget().dispatchMessage(msg); 執行時間,但是就不要在線上使用這種方式了。

不過該方式和以上兩個方案對比,并無優勢,不過這個思路挺有意思的。

使用方式和上述一致。

最后,可以考慮將卡頓日志輸出到文件,慢慢分析;可以結合上述原理以及自己需求開發做一個合適的方案,也可以參考已有開源方案。

參考

 

 

來自:http://blog.csdn.net/lmj623565791/article/details/58626355

 

 本文由用戶 AhmStump 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!