Android UI性能優化 檢測應用中的UI卡頓
一、概述
在做app性能優化的時候,大家都希望能夠寫出絲滑的UI界面,以前寫過一篇博客,主要是基于Google當時發布的性能優化典范,主要提供一些UI優化性能示例:
實際上,由于各種機型的配置不同、代碼迭代歷史悠久,代碼中可能會存在很多在UI線程耗時的操作,所以我們希望有一套簡單檢測機制,幫助我們定位耗時發生的位置。
本篇博客主要描述如何檢測應用在UI線程的卡頓,目前已經有兩種比較典型方式來檢測了:
- 利用UI線程Looper打印的日志
- 利用Choreographer
兩種方式都有一些開源項目,例如:
- https://github.com/markzhai/AndroidPerformanceMonitor [方式1]
- https://github.com/wasabeef/Takt [方式2]
- https://github.com/friendlyrobotnyc/TinyDancer [方式2]
其實編寫本篇文章,主要是因為發現一個還比較有意思的方案,該方法的靈感來源于一篇給我微信投稿的文章:
該項目主要用于捕獲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); 執行時間,但是就不要在線上使用這種方式了。
不過該方式和以上兩個方案對比,并無優勢,不過這個思路挺有意思的。
使用方式和上述一致。
最后,可以考慮將卡頓日志輸出到文件,慢慢分析;可以結合上述原理以及自己需求開發做一個合適的方案,也可以參考已有開源方案。
參考
- https://github.com/markzhai/AndroidPerformanceMonitor
- https://github.com/wasabeef/Takt
- https://github.com/friendlyrobotnyc/TinyDancer
來自:http://blog.csdn.net/lmj623565791/article/details/58626355