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è):
- 利用UI線程Looper打印的日志
- 利用Choreographer
兩種方式都有一些開(kāi)源項(xiàng)目,例如:
- https://github.com/markzhai/AndroidPerformanceMonitor [方式1]
- https://github.com/wasabeef/Takt [方式2]
- https://github.com/friendlyrobotnyc/TinyDancer [方式2]
另外,還有一種非常規(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è)思路挺有意思的。