Android 检测UI卡顿
相关工具代码可以在这里找到:
- BlockDetect 检测应用在UI线程的卡顿,打印出卡顿时调用堆栈。
- FrameAnalyze 帧分析工具类,一个打印帧率和丢帧情况log的工具类,原理是利用Choreographer的FrameCallback。
补充:
看到腾讯Bugly的一篇关于检测UI卡顿的文章,整体思路和下文相似,而且总结的更全面,更是有生产环境上的实施经验分享,十分值得学习,放在这里以供参考:《广研Android卡顿监控系统》
文章内容
原文地址
在实际开发中,经常会碰到UI卡顿的现象,为方便定位问题原因,能在UI卡顿时或者UI线程执行耗时操作时打印出调用堆栈是非常有必要的。目前有两种典型方法来检测:
- 利用UI线程Looper打印的日志
- 利用Choreographer
两种方式都有一些开源项目,例如:
- https://github.com/markzhai/AndroidPerformanceMonitor [方式1]
- https://github.com/wasabeef/Takt [方式2]
- https://github.com/friendlyrobotnyc/TinyDancer [方式2]
另外,还有一种非常规的方式,是hack掉Looper.loop()方法,自己实现loop方法来处理Message的方式:
-
https://github.com/android-notes/Cockroach
该项目主要用于捕获UI线程的crash,这里也可以用来作为检测卡顿方案,或者也可能可以做一些别的事情。
一、利用loop()中打印的日志
在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();
}
}
所以,我们只要能检测:msg.target.dispatchMessage(msg)
的执行时间,就能够检测到UI操作上是否有耗时操作了,可以看到此行代码前后,如果设置了logging
,会分别打印出>>>>> Dispathcing to
和<<<<< Finished to
这样的log。
我们可以匹配这两个log,得到两次log之间的时间差值,如果差值打印时间阈值,就打印出UI线程的堆栈信息(在非UI线程执行),这里阈值设置为1000ms,正常情况下,UI线程操作肯定是低于1000ms执行完成的。
二、利用Choreographer
Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染。SDK中包含了一个相关类,以及相关回调。理论上来说两次回调的时间周期应该在16ms,如果超过了16ms我们则认为发生了卡顿,我们主要就是利用两次回调间的时间周期来判断。
三、利用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();
}
}
});
}
}
其实很简单,将Looper.loop里面本身的代码直接copy来了这里。当这个消息被处理后,后续的消息都将会在这里进行处理。
中间有变量和方法需要反射来调用,不过不影响查看msg.getTarget().dispatchMessage(msg);执行时间,但是就不要在线上使用这种方式了。
不过该方式和以上两个方案对比,并无优势,不过这个思路挺有意思的。