详解Android UI线程卡顿收集

时间:2022-05-07
本文章向大家介绍详解Android UI线程卡顿收集,主要内容包括1、整体概述、1.2卡顿因素、1.3卡顿监控系统整体结构、2、主线程卡顿收集SDK实现、2.2 卡顿监控核心流程图、2.3 数据的处理、2.4 开发过程中遇到的问题、3、总结、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。
作者:段云飞

京东前台产品研发部-资深Android工程师,主要负责手机京东Android端图片框架,性能优化,性能数据收集,对Android Framework、App性能优化有深入研究。

1、整体概述

1.1背景

  • 我所在的平台化技术组致力于打造阿凡达开放平台,通过全面的技术解决方案及完善的支撑系统,为业务开发提供便捷的一站式服务,并将手机京东多年来积累技术能力输出到各个京东体系的各个应用中。其中性能监控分析技术是为APP质量的护卫舰,本文要讲解的卡顿收集系统就为性能监控的一部分。
  • 经过多年的技术积累,我们发现因为业务场景复杂、版本迭代快、历史代码庞大、包含各种第三方库等客观原因,很多大型的Android项目都面临着同样的问题——卡顿
  • 当App出现卡顿的时候,即使定位到具体界面,动辄数千行的代码夹杂着各种业务逻辑,也很难定位到底是哪里出现了问题,结果就是客户端越来越卡,恶性循环。

大型项目遇到的现状:

1.2卡顿因素

导致卡顿的因素有很多,常见有:

  • UI线程中的耗时操作
  • 复杂、不合理的布局以及过度绘制
  • 内存使用异常导致频繁GC
  • 错误的异步实现

以上四条中,最主要的卡顿原因为UI线程中执行耗时操作。 我们也一直在研究,在不影响京东App性能的前提下,完美的实现一个UI线程卡顿监控系统。该系统能够监控线上用户的卡顿,上报卡顿数据,数据聚合,根据聚合结果自动生成工单,将工单发给对应模块的负责人。

希望实现的效果为:

  • 非侵入式,不需要到处打点,破坏代码的优雅性
  • 精准定位,直接定位到行
  • 不影响App性能

1.3卡顿监控系统整体结构

整个系统分成4部分:

  1. 主线程卡顿采集SDK
  2. 性能数据上报SDK
  3. 服务端收集到数据后,进行数据聚合
  4. 自动产生工单,发送给对应的工程师

卡顿监控系统结构图:

2、主线程卡顿收集SDK实现

2.1 监控原理

1.主线程只有一个looper

Looper.java的源码可以看到,定义了一个静态变量sMainLooper,主线程无论有多少个Handler,但只有这一个looper存在,主线程中执行任何代码都会回到loop()函数中。

Looper.java的loop():

public static void loop() {
    ...    

    for (;;) {
        ...
        Printer logging = me.mLogging;        
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target);
        }
        msg.target.dispatchMessage(msg);        
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target);
        }
    }

    ...
}

就是这个mLogging,它在每个message处理的前后被调用,而如果主线程发生卡顿,是在dispatchMessage里执行了耗时操作。

2.将主线程的Printer替换

可以看到Looper中的Printer是可以替换的,谢谢Google大牛们目前留了接口,不过仔细想想,即使没有留接口也可以使用反射替换掉。

替换接口:Looper.getMainLooper().setMessageLogging(printer);

3.卡顿条件(endTime-startTime > 卡顿阈值)

Printer在每一个message执行前和执行后成对的调用,就可以知道消息开始时间startTime,以及消息的结束时间endTime。如果endTime-startTime > 卡顿阈值,则认为该条消息执行过长,主线程卡顿。

4.采样

在执行主线程的同时,采样线程要对主线程的堆栈、cpu等信息进行周期采样。执行前采样线程要先休眠一段时间,这样做的主要原因就是为了不对主线程的短消息(即执行时间很短的message)进行干扰,避免抢夺cpu资源,降低对性能的影响。

采样示意图:

2.2 卡顿监控核心流程图

  • 采样线程:每隔一段时间就会采样一次,会产生大量的临时对象,所以采样过程中要控制采样对象个数,这里我用链表实现了一个轻量级的对象池,实现原理参考的Android系统控制Message数量的方案。
  • 主线程:发生卡顿后需要从采样线程的对象池中将属于T2-T1时间段的堆栈等采样信息保存起来,将数据传递给缓存池。
  • 缓存池:即内存缓存,实现一个定时器,每隔固定时间就去检查是否符合上报条件,如果符合就进行数据上报。

2.3 数据的处理

1、 数据分成两类:

  • 确认卡顿:两个相邻的采样时间间隔,取出的主线程堆栈是完全一致的。这种情况就划分到确认卡顿范畴,因为可以明确的判断,该函数在两个时间间隔内还没有执行完。
  • 疑似卡顿:没有相邻间采样间隔堆栈相同,这种属于疑似卡顿。

2、 堆栈预处理:

  • 数据初步聚合:在采样时,假如两个相邻间隔的堆栈完全一致,只需定义一个count字段,然后把count+1,服务端会根据这个count字段来确定本次采样该堆栈花费了多少个时间间隔。这样可以减少重复堆栈缓存,降低内存和流量消耗。
  • 关键行:客户端会过滤堆栈内容,找到带有jd或者jingdong包名的代码行,标记为关键代码行,将关键行做为数据聚合的依据,这样做可以大大减轻服务端的数据聚合压力。

3、数据收集策略和展示

  • 收集策略:策略包含App版本号、Build号、Android系统、开关灰度比例、不同网络环境(2G,3G,4G,WIFI)、数据是否实时上报等。可以自由组合,精确控制覆盖范围。
  • 精准匹配:可为某些特殊用户单独开启,比如该用户一直投诉页面卡顿,可以精确控制只让该用户开启此功能。
  • 数据展示:部分数据聚合结果如下图

2.4 开发过程中遇到的问题

1、 Printer替换:由于任何一个模块都可以设置主线程的Printer,测试时发现,经常有其他未知模块会替换掉主线程的Printer,最典型的就是WebView类,WebView类中有一个setWebContentsDebuggingEnabled()函数,无论设置true还是false,都会将线程的Printer替换掉。解决办法是设置一个暗门,等h5开发同事真正需要调试时,触发暗门,调用setWebContentsDebuggingEnabled,默认不调用。

2、 getMainPrinter():Looper中并没有提供获得主线程Printer的方法。解决办法,通过反射Framework层代码获得,代码如下:

/**
 * 反射获得主线程的Printer对象
 * @return
 */

private static Printer getMainPrinter(){
   try {
       Field privatePrinterField =Looper.class.getDeclaredField("mLogging");
       privatePrinterField.setAccessible(true);
       Looper mainLooper = Looper.getMainLooper();
       Printer oldPrinter = (Printer) privatePrinterField.get(mainLooper);//获得私有字段值
       if (oldPrinter != null){
           return oldPrinter;
       }
    } catch (Exception e) {
        e.printStackTrace();
    }    return null;
}

3、总结

卡顿收集属于我们APM监控系统的重要组成部分,收集到的卡顿数据样本越大越准确,目前手机京东App使用此系统,每天接收到用户卡顿数据上百万条,能够精准定位卡顿用户以及卡顿原因,再通过大数据的聚合,可以基本掌握每个版本的卡顿情况。

当然,卡顿数据收集只是整个工作的第一步,数据收集到服务端后,还需要QA,测试,业务研发等同学一起努力优化,才能真正的降低卡顿率。