云米客户端APM性能监控组件分析(二)
上一篇文章已经分析了Activity的生命周期耗时监控原理
https://www.jianshu.com/p/f9a5172e858d
现在来分析监控卡顿的原理和技巧。
问题一:为什么手机在处理任务的时候会卡顿?
这就涉及到Android这个手机系统的消息通知机制了,整个Android系统就想一个巨大的用不关门的邮局,里面有一个分发机制,当收到一个message会立马进行转发,如果等待分发的时间过长,就会出现我们熟知的“等待”、“关闭”弹窗,触发了ANR。要等多久?5秒。
这个巨大的用不停歇的邮局由Looper、Handler、MessageQueue、Message这四个对象组成,这里只监听主线程的卡顿信息,不做子线程思维发散。
机制原理.png
具体运转原理请查阅相关资料,这里只分析应该从哪里入手监听卡顿发生的时机。
Looper对象内部有一个mLogging的Printer对象,在loop()方法内部,消息分发前会打印一句“>>>>> Dispatching”,在消息分发后也会打印一个“<<<<< Finished”
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
...
}
根据这个特点可以想出一个好办法。Looper对象有一个方法叫【setMessageLogging()】,可以设置自己的Log对象,然后重写println方法。以下是核心实现
private Runnable runnable = new Runnable() {
@Override
public void run() {
if (!isCanWork()) {
return;
}
String stackInfo = CommonUtils.getStack();
if (Manager.isDebug()) {
ApmLogX.d(APM_TAG, SUB_TAG, "thread stack info:" + stackInfo);
}
saveBlockInfo(stackInfo);
}
};
// 监听主线程的Looper分发消息入口
Looper.getMainLooper().setMessageLogging(new Printer() {
// 这里通过设置新的Printer来hook Looper的消息遍历操作,监听分发消息开始和结束关键字
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
@Override
public void println(String x) {
if (x.startsWith(START)) {
mHandler.postDelayed(runnable, TaskConfig.DEFAULE_BLOCK_TIME);
}
if (x.startsWith(END)) {
mHandler.removeCallbacks(runnable);
}
}
});
通过判断start和end的字符串,判断是否需要触发runnable对象执行任务,DEFAULE_BLOCK_TIME我默认设置4.5秒,因为5秒会响应系统自己的ANR弹窗。如果两个打印时间间隔在4.5秒内,则认为不卡顿,清理runnable对象;如果4.5秒内没有收到end字符串,则认为卡顿,会触发runnable运行。在runnable内执行保存卡顿信息的操作。
问题二:我现在知道什么时候卡顿了,但卡顿信息从哪里获取?
Android系统对每一个APP都独立维护了一个进程,每一个进程至少有一个主线程来运行程序任务,所以可以从主线程的堆栈信息中获得卡顿时的整个方法调用链。
public static String getStack() {
StringBuilder sb = new StringBuilder();
StackTraceElement[] traceElements = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement element : traceElements) {
sb.append(element.toString() + "\n");
}
return sb.toString();
}
其余的额外信息按需获取。卡顿信息获取就是这么简单。