云米客户端APM性能监控组件分析(二)

上一篇文章已经分析了Activity的生命周期耗时监控原理
https://www.jianshu.com/p/f9a5172e858d

现在来分析监控卡顿的原理和技巧。

问题一:为什么手机在处理任务的时候会卡顿?

这就涉及到Android这个手机系统的消息通知机制了,整个Android系统就想一个巨大的用不关门的邮局,里面有一个分发机制,当收到一个message会立马进行转发,如果等待分发的时间过长,就会出现我们熟知的“等待”、“关闭”弹窗,触发了ANR。要等多久?5秒。

这个巨大的用不停歇的邮局由Looper、Handler、MessageQueue、Message这四个对象组成,这里只监听主线程的卡顿信息,不做子线程思维发散。


云米客户端APM性能监控组件分析(二)_第1张图片
机制原理.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();
    }

其余的额外信息按需获取。卡顿信息获取就是这么简单。

未完待续......

你可能感兴趣的:(云米客户端APM性能监控组件分析(二))