编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.崩溃捕获设计实践
  • 02.崩溃治理优化总结
  • 03.Native崩溃治理实践
  • 04.ANR监控设计实践
  • 05.CPU消耗优化实践
  • 06.卡顿监控设计实践
  • 07.卡顿治理优化实践
  • 08.网络分析与优化实践
  • 09.线程优化实践操作
  • 10.高性能图片优化方案
  • 11.OOM异常优化实践
  • 12.内存监控优化方案
  • 13.内存治理优化实践
  • 14.FPS监测设计实践
  • 15.进程优化设计实践
  • 16.App启动优化实践
  • 17.App页面UI优化实践
  • 18.App稳定性专项实践
  • 19.App瘦身优化实践
  • 20.常见代码优化实践
  • 21.移动端防抓包实践
  • 22.App磁盘沙盒实践
  • 23.Ping工具开发实践
  • 24.Gradle构建优化实践
  • 25.CodeReview实践总结

06.卡顿监控设计实践

目录介绍

  • 01.卡顿整体概述
    • 1.1 项目背景介绍
    • 1.2 卡顿简单介绍
    • 1.3 卡顿分类说明
    • 1.4 卡顿例子
    • 1.5 设计目标
  • 02.卡顿技术要点说明
    • 2.1 UI卡顿产生原因
    • 2.2 卡顿和ANR的关系
    • 2.3 什么是FPS流畅度
    • 2.4 一帧显示的步骤
  • 03.卡顿问题排查思路
    • 3.1 卡顿排查总思路
  • 04.卡顿监控思路
    • 4.1 卡顿监控核心思路
    • 4.2 LooperPrinter
    • 4.3 WatchDog
    • 4.4 FPS帧率检测
    • 4.5 Matrix插桩
    • 4.7 卡顿日志记录上传
  • 05.卡顿原理探索
    • 5.1 Handler机制说一下
    • 5.3 UI卡顿检测实现
    • 5.4 那些卡顿无法监控
    • 5.5 监控IdleHandler卡顿
    • 5.6 监控TouchEvent卡顿
    • 5.7 监控SyncBarrier泄漏

01.卡顿整体概述

1.1 项目背景介绍

  • App中卡顿让用户使用体验不佳
    • 在复杂的项目环境中,由于历史代码庞大,业务复杂,包含各种第三方库,所以在出现了卡顿的时候,很难定位到底是哪里出现了问题。
    • 即便知道是哪一个Activity/Fragment,也仍然需要进去里面一行一行看,动辄数千行的类再加上跳来跳去调来调去的,结果就是不了了之随它去了,实在不行了再优化吧。
  • 卡顿可能引发未知问题
    • 卡顿还可能会引发一些未知的问题,比如卡顿会造成掉帧,会造成抖动,设置还会造成ANR,对用户来说体验特别不好。简单来说,就是APP用起来有点卡……
  • 排查卡顿问题非常费劲
    • 很多情况下卡顿不是必现的,它们可能与机型、环境、操作等有关,存在偶然性,即使发生了,再去查那如山般的logcat,也不一定能找到卡顿的原因,是我们自己的应用导致的还是其他应用抢占资源导致的?是哪些方法导致的?很难去回朔。

1.2 卡顿简单介绍

  • 什么是卡顿
    • 界面呈现是指从应用生成帧并将其显示在屏幕上的动作。我们要确保用户能够流畅地与应用互动,我们的应用呈现每帧的时间不应超过16ms,以达到每秒 60 帧的呈现速度。
    • 如果我们的应用存在界面呈现缓慢的问题,系统会不得不跳过一些帧,这会导致用户感觉您的应用不流畅。我们将这种情况称为卡顿。
  • UI线程执行时间
    • 在Android中,UI线程负责执行UI视图的布局、渲染等工作,UI在更新期间,如果UI线程执行时间超过了16ms,则就会产生丢帧的现象,大量的丢帧,就会造成卡顿,影响用户体验。
    • Android中规定,每秒可以执行60次屏幕刷新,当我们的APP能够达到60帧/秒时,这种体验是优秀的,当帧率降低到40帧以下,甚至30帧以下,用户就可以感知到卡顿了。
  • 时间计算公式
    • 公式(1秒刷新60次,那么每次刷新耗时16毫秒):1000毫秒 / 60帧 = 16ms , 关于帧率怎么查看呢?可以通过fps查看。

1.3 卡顿分类说明

  • CPU紧张
    • 系统CPU资源紧张,分配给APP主线程(UI线程)的CPU时间片减少。
    • UI线程中执行了大量的耗时任务,导致了UI线程视图刷新工作的阻塞。
  • 频繁GC
    • Android虚拟机频繁执行GC操作导致的卡顿。由于GC会占用大量的系统资源,同时GC过程中会产生UI线程停顿,从而产生卡顿。
  • 过渡绘制
    • 过度绘制产生卡顿。过度绘制会导致GPU执行时间变长,从而产生丢帧现象。

1.4 卡顿例子

  • 卡顿其实分为直观的和微观两个方向的,举个例子
    • 比如用户点击了登录,预期是得到登录成功/失败的反馈,可现在没有页面刷新,实际的刷新耗时超出了预期,这就是直观的卡顿。
    • 比如用户在看股票,正常60秒刷新一次,可到了60秒,触发了刷新但是刷新处理逻辑耗时较长,导致新的数据在下一个60秒之前的第59秒才回来,用户基本无感知,主要说的是内部耗时的问题,这种情况可以粗略的理解为微观卡顿。

1.5 设计目标

  • 设计目标要求
    • 对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用。
    • 要求接入方便,外部开发者调用简单,非侵入式,简单的两行就打开监控,不需要到处打点,破坏代码优雅性。
    • 精准,输出的信息可以帮助定位到问题所在(精确到行),不需要像Logcat一样,慢慢去找。
  • 卡顿优化点分析
    • 如果没能在16ms内完成这个过程,就会使屏幕重复显示上一帧的内容,即造成了卡顿。在这16ms内,需要完成视图树的所有测量、布局、绘制渲染及合成。而我们的优化工作主要就是针对这个过程的。

02.卡顿技术要点说明

2.1 UI卡顿产生的原因

  • UI卡顿通常产生的原因如下:
    • 系统CPU资源紧张,分配给APP主线程(UI线程)的CPU时间片减少。
    • UI线程中执行了大量的耗时任务,导致了UI线程视图刷新工作的阻塞。
    • Android虚拟机频繁执行GC操作导致的卡顿。由于GC会占用大量的系统资源,同时GC过程中会产生UI线程停顿,从而产生卡顿。
    • 过度绘制产生卡顿。过度绘制会导致GPU执行时间变长,从而产生丢帧现象。
  • 举一些案例说明
    • 比如滑动列表

2.2 卡顿和ANR的关系

  • 卡顿与ANR的关系
    • 产生卡顿的根本原因是UI线程不能够及时的进行渲染,导致UI的反馈不能按照用户的预期,连续、一致的呈现。
    • 产生卡顿的原因多种多样,很难一一列举,而ANR是Google人为规定的概念,产生ANR的原因最多也只有四个。
  • 两者相关但不同纬度
    • 事实上,长时间的UI卡顿是导致ANR最常见的原因;但另一方面,从原理上来看,两者既不充分也不必要,是两个维度的概念。
  • 卡顿监控ANR准确吗
    • 一些卡顿监控工具,经常被拿来监控ANR(卡顿阈值设置为5秒),这其实很不严谨。
    • 首先,5秒只是发生ANR的其中一种原因(Touch事件5秒未被及时消费)的阈值,而其他原因发生ANR的阈值并不是5秒;
    • 另外,就算是主线程卡顿了5秒,如果用户没有输入任何的Touch事件,同样是不会发生ANR的,更何况还有后台ANR等情况。

2.3 什么是FPS流畅度

  • 流畅度,是页面在滑动、渲染等过程中的体验。
    • Android系统要求每一帧都要在 16ms 内绘制完成,平滑的完成一帧意味着任何特殊的帧需要执行所有的渲染代码(包括 framework 发送给 GPU 和 CPU 绘制到缓冲区的命令)都要在 16ms 内完成,保持流畅的体验。
    • 如果没有在期间完成渲染秒就会发生掉帧。掉帧是用户体验中一个非常核心的问题。丢弃了当前帧,并且之后不能够延续之前的帧率,这种不连续的间隔会容易会引起用户的注意,也就是常说的卡顿、不流畅。
  • 那么是不是1s只要绘制了60帧是不是就是流畅的呢?
    • 也不一定,如果发生抖动的情况,那么肯定会有其中几帧是有问题的。其中肯定会有最大绘制帧,和最小绘制帧的情况,所以平均值,最大值最小值都是我们需要知道的。

2.4 一帧显示的步骤

  • 一帧的显示要经历如下步骤:
    • cpu计算画多大,画在哪里,画什么,然后gpu渲染计算结果存到buffer,显示器每隔一段时间从buffer取帧。若没有取到帧,只能继续显示上一帧。
  • 16ms 内都需要完成什么?
    • VSync调度、doframe消息调度、input输入处理、动画、测量/布局、绘制、同步和上传、命令问题、交换缓冲区。也就是我们常用的 GPU 严格模式。

04.卡顿监听思路

4.1 卡顿监控核心思路

可以监控卡顿,从而可以对卡顿优化做到极致。我们可以从下面四个方面来监控应用程序卡顿:

  1. 基于 Looper 的 Printer 分发消息的时间差值来判断是否卡顿。
  2. 基于 Choreographer 回调函数 postFrameCallback 来监控
  3. 基于开源框架 BlockCanary 来监控
  4. 基于开源框架 rabbit-client 来监控

4.2 LooperPrinter

  • 卡顿监控不优雅的处理
    • 直接创建一个基类放在我们的项目代码中,所有需要Handler的地方都对此进行继承,然后我们在基类中添加日志监控,这样就可以实现我们的目的了吧?
    • 不好,这样对项目改造的成本太高了,而且我们也监控不到系统中的消息,也监控不到第三方sdk中的消息执行时间!
  • 卡顿监控优雅的处理
    • 分析Looper中的loop方法可知,处理消息前后有日志打印,看到有个Printer日志输出管理的类,并且暴露setMessageLogging方法。这个可以自己定义实现。
  • 使用LooperPrinter监控
    • 替换主线程Looper的Printer,从而监控dispatchMessage的执行时间。甚至,在Android源码中,主线程Looper也会根据执行dispatchMessage的时间来判断是否有卡顿,有则会打印一些日志。
    • 在Printer中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值(如2000毫秒)为主线程卡慢发生,并dump出各种信息,提供开发者分析性能瓶颈。
    • 卡顿之后收集相关dump信息,主要包含一些基本信息,耗时信息,CPU信息还有堆栈信息等等。可以直接参考LeakCanary即可。
  • 优缺点分析
    • 优点:不会随机漏报,无需轮询,一劳永逸;缺点:某些类型的卡顿无法被监控到。

4.3 WatchDog

4.3.1 WatchDog卡顿监控

如何监控UI卡顿:启动一个卡顿检测线程,该线程定期的向UI线程发送一条延迟消息,执行一个标志位加1的操作,如果规定时间内,标志位没有变化,则表示产生了卡顿。如果发生了变化,则代表没有长时间卡顿,我们重新执行延迟消息即可。

具体的原理和实现方法很简单:不断向UI线程发送Message,每隔一段时间检查一次刚刚发送的Message是否被处理,如果没有被处理,则说明这段时间主线程被卡住了。

其源码核心是ANRWatchDog这个类,继承自Thread,它的run 方法如下,看注释处。

public void run() {
    setName("|ANR-WatchDog|");
    long interval = _timeoutInterval;
   // 1、开启循环
    while (!isInterrupted()) {
        boolean needPost = _tick == 0;
        _tick += interval;
        if (needPost) {
           // 2、往UI线程post 一个Runnable,将_tick 赋值为0,将 _reported 赋值为false                      
          _uiHandler.post(_ticker);
        }

        try {
            // 3、线程睡眠5s
            Thread.sleep(interval);
        } catch (InterruptedException e) {
            _interruptionListener.onInterrupted(e);
            return ;
        }

        // If the main thread has not handled _ticker, it is blocked. ANR.
        // 4、线程睡眠5s之后,检查 _tick 和 _reported 标志,正常情况下_tick 已经被主线程改为0,_reported改为false,如果不是,说明 2 的主线程Runnable一直没有被执行,主线程卡住了
        if (_tick != 0 && !_reported) {
            ...
            if (_namePrefix != null) {
                // 5、判断发生ANR了,那就获取堆栈信息,回调onAppNotResponding方法
                error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
            } else {
                error = ANRError.NewMainOnly(_tick);
            }
            _anrListener.onAppNotResponding(error);
            interval = _timeoutInterval;
            _reported = true;
        }
    }
}

ANRWatchDog 的原理是比较简单的,概括为以下几个步骤:

  1. 开启一个线程,死循环,循环中睡眠5s。
  2. 往UI线程post 一个Runnable,将_tick 赋值为0,将 _reported 赋值为false。
  3. 线程睡眠5s之后检查_tick和_reported字段是否被修改。
  4. 如果_tick和_reported没有被修改,说明给主线程post的Runnable一直没有被执行,也就说明主线程卡顿至少5s**(只能说至少,这里存在5s内的误差)**。
  5. 将线程堆栈信息输出。

4.3.2 WatchDog的优缺点

  • 优缺点
    • 优点:简单,稳定,结果论,可以监控到各种类型的卡顿
    • 缺点:轮询不优雅,不环保,有不确定性,随机漏报
  • 间隔时间设置
    • 这种方法的轮询的时间间隔选择很重要,又让人左右为难,轮询的时间间隔越小,对性能的负面影响就越大,而时间间隔选择的越大,漏报的可能性也就越大。
  • 如何理解性能负面影响
    • 前者很容易理解,UI线程要不断处理我们发送的Message,必然会影响性能和功耗。

4.3.3 如何理解漏报数据

  • 时间间隔选择了4秒
    • 事实上,之前是想要通过这种方案来监控ANR,当然,这并不严谨。来分析一下
  • 举一个例子
    • 每隔4秒,向主线程发送一个消息。下面是轮训的过程
    • 0秒 ---- 4秒 ---- 8秒 ---- 12秒 ---- 16秒
    • 现在有一个5秒的卡顿发生在第2秒,结束在第7秒,这种情况无论是在0-4秒的周期内,还是4-8秒的周期内,都有一段时间是不卡顿的,消息都可以被处理掉,这种情况自然就无法被监控到。
  • 计算监控成功率
    • 计算公式:p = x/a - 1 ;注意条件(a<= x <= 2a)
    • 上面案例计算 : p = 5/4 - 1 = 0.25 ; 如果轮询间隔设置为4秒,发现一个5秒的卡顿的概率仅为25%。
  • 修改轮训间隔时间
    • 默认轮询间隔为5秒,如果有一个8秒的卡顿(8秒已经很容易产生ANR),被发现的概率也只有8/5-1=60%
    • 从这个概率公式还可以发现,对于一个固定的轮询间隔,只有卡顿时间大于两倍的轮询间隔,才能百分之百被监控到。
  • 思考把间隔时间缩短
    • 每隔2秒,向主线程发送一个消息。下面是轮训的过程
    • 0秒 -- 2秒 -- 4秒 -- 6秒 -- 8秒 -- 10秒 -- 12秒
    • 现在有一个6秒的卡顿发生在第1秒,结束在第7秒,那么这个在在2-4,和4-6区间可以捕获到。

4.4 FPS帧率检测

  • 具体可以看:14.FPS检测设计实践

4.5 Matrix插桩

对于线上卡顿监控,需要了解字节码插桩技术。

通过Gradle Plugin+ASM,编译期在每个方法开始和结束位置分别插入一行代码,统计方法耗时。伪代码如下:

插桩前
fun method(){
   run()
}

插桩后
fun method(){
   input(1)
   run()
   output(1)
}

目前微信的Matrix 使用的卡顿监控方案就是字节码插桩。插桩需要注意的问题:

  1. 避免方法数暴增:在方法的入口和出口应该插入相同的函数,在编译时提前给代码中每个方法分配一个独立的 ID 作为参数。
  2. 过滤简单的函数:过滤一些类似直接 return、i++ 这样的简单函数,并且支持黑名单配置。对一些调用非常频繁的函数,需要添加到黑名单中来降低整个方案对性能的损耗。

微信Matrix做了大量优化,整体包体积增加1%-2%,帧率下降2帧以内,对性能影响整体可以接受,不过依然只会在灰度包使用。

4.7 卡顿日志记录上传

  • 卡顿信息捕获,发生卡顿时需要捕获如下四类信息,以提高定位卡顿问题的效率与精度。
    • 1、基础信息:系统版本、机型、进程名、应用版本号、磁盘空间、UID等。
    • 2、耗时信息:卡顿开始和结束时间。
    • 3、CPU信息:CPU的信息、整体CPU使用率和本进程CPU使用率(可粗略判断是当前应用消耗CPU资源太多导致的卡顿,还是其他原因)等。
    • 4、堆栈信息。
  • 注意
    • 这里的信息建议抽样上报或者可以先将其保存到本地,在合适的时机以及达到一定的量时,再压缩上报到服务器,供开发者分析。
    • 具体监控代码实现可以参考BlockCanary开源项目的代码。

05.卡顿原理探索

5.1 Handler机制说一下

想要监控线上用户UI线程的卡顿,也就是要把UI线程中的耗时逻辑找出来,然后进行优化开发。那么我们如何如做呢?

  1. Android中的应用程序是消息驱动的,也就是UI线程执行的所有操作,通常都会经过消息机制来进行传递(也就是Handler通信机制)。
  2. Handler的handleMessage负责在UI线程中处理UI相关逻辑,如果我们能在handleMessage执行之前和handleMessage执行之后,分别插入一段我们的日志代码,不就可以实现UI任务执行时间的监控了吗?

5.3 UI卡顿检测实现

来看Looper的loop方法,loop方法中有一个Printer类型的logging,它会在消息执行之前和消息执行之后,输出一行日志,用于标记消息执行的开始和结束。

只要记录开始日志和结束日志的时间差,就可以计算出该任务在UI线程的执行时间了,如果执行时间很长,则必然产生了卡顿。

public static void loop() {
    for (;;) {
        //1、取消息
        Message msg = queue.next(); // might block
        if (msg == null) {
            return;
        }
        final Printer logging = me.mLogging;
        //2、消息处理前回调
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }
        //3、消息开始处理
        msg.target.dispatchMessage(msg);
        //4、消息处理完回调
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
    }
}

从上面的代码块可以看出,导致卡顿的原因可能有两个地方:

  1. 注释1的queue.next()阻塞。
  2. 注释3的dispatchMessage耗时太久。

如何监控这个Printer类型的日志呢

发现mLogging这个对象可以通过一个public方法进行设置!这简直太好了!可以通过setMessageLogging方法设置我们自己的Printer对象就可以实现卡顿的监控了!

private Printer mLogging;
public void setMessageLogging(@Nullable Printer printer) {
    mLogging = printer;
}

为何说可以监测大部分卡顿

  1. 通过计算执行dispatchMessage方法之后和之前打印字符串的时间的差值,就可以拿到到dispatchMessage方法执行的时间。
  2. 即整个应用的主线程,只有这一个looper,不管有多少handler,最后都会回到这里。而大部分的主线程的操作最终都会执行到这个dispatchMessage方法中。

具体怎么判断卡顿呢?

在Printer中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值(如2000毫秒)为主线程卡慢发生,并dump出各种信息,提供开发者分析性能瓶颈。

卡顿之后收集相关dump信息【参考LeakCanary】

  1. 基本信息:安装包标示、机型、api等级、uid、CPU内核数、进程名、内存、版本号等
  2. 耗时信息:实际耗时、主线程时钟耗时、卡顿开始时间和结束时间
  3. CPU信息:时间段内CPU是否忙,时间段内的系统CPU/应用CPU占比,I/O占CPU使用率
  4. 堆栈信息:发生卡慢前的最近堆栈,可以用来帮助定位卡慢发生的地方和重现路径

存在的问题有:如果queue.next()卡住了,那么就无法往下执行了。这个时候无法检测到卡顿。

5.4 那些卡顿无法监控

  • 看到上面的queue.next(),这里给了注释:might block
    • 代码直接跟你说这里是可能会卡住的,这时候再计算dispatchMessage方法的耗时显然就没有意义了。
    • 有的同学可能会想,那我改成计算相邻两次dispatchMessage执行之前打印字符串的时间差值不就好了?这样就可以把next方法的耗时也计算在内。
    • 不幸的是,主线程空闲时,也会阻塞在MessageQueue的next方法中,我们很难区分究竟是发生了卡顿还是主线程空闲。
  • 分析一下MessageQueue的next方法
    • 是什么原因会卡在MessageQueue的next方法中呢?下图是next方法简化过后的源码
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }
        nativePollOnce(ptr, nextPollTimeoutMillis);
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
            mPendingIdleHandlers[i] = null; // release the reference to the handler
            boolean keep = false;
            try {
                keep = idler.queueIdle();
            } catch (Throwable t) {
            }
            if (!keep) {
                synchronized (this) {
                    mIdleHandlers.remove(idler);
                }
            }
        }
        //......
    }
  • 有这么几类无法监控到
    • View的TouchEvent中的卡顿这种方案是无法监控的
    • IdleHandler的queueIdle()回调方法也是无法被监控的
    • SyncBarrier(同步屏障)的泄漏同样无法被监控到

5.4.1 View的TouchEvent卡顿

  • 在MessageQueue的next方法中
    • 除了主线程空闲时就是阻塞在nativePollOnce之外,非常重要的是,应用的Touch事件也是在这里被处理的。
    • 这就意味着,View的TouchEvent中的卡顿这种方案是无法监控的。
  • 那么TouchEvent造成卡顿场景有哪些呢?
    • 待完善

5.4.2 IdleHandler的queueIdle()

  • IdleHandler的queueIdle()回调方法也是无法被监控的
    • 这个方法会在主线程空闲的时候被调用。然而实际上,很多开发同学都先入为主的认为这个时候反正主线程空闲,做一些耗时操作也没所谓。
    • 其实主线程MessageQueue的queueIdle默认当然也是执行在主线程中,所以这里的耗时操作其实是很容易引起卡顿和ANR的。
  • 那么queueIdle()造成卡顿场景有哪些呢?
    • 例如App之前就使用IdleHandler在进入主界面后,做一些读写文件的IO操作,就造成了一些卡顿和ANR问题。

5.4.3 SyncBarrier(同步屏障)的泄漏

  • SyncBarrier(同步屏障)的泄漏同样无法被监控到
    • 当我们每次通过invalidate来刷新UI时,最终都会调用到ViewRootImpl中的scheduleTraversals方法,会向主线程的Looper中post一个SyncBarrier,其目的是为了在刷新UI时,主线程的同步消息都被跳过,此时渲染UI的异步消息就可以得到优先处理。
    • 但是注意到这个方法是线程不安全的,如果在非主线程中调用到了这里,就有可能会同时post多个SyncBarrier,但只能remove掉最后一个,从而有一个SyncBarrier就永远无法被remove,就导致了主线程Looper无法处理同步消息(Message默认就是同步消息),导致卡死
  • ViewRootImpl源码如下所示
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
    void unscheduleTraversals() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
            mChoreographer.removeCallbacks(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        }
    }

5.5 监控IdleHandler卡顿

  • 首先从简单的下手,对于IdleHandler的queueIdle回调方法的监控。
    • 我们惊喜的发现MessageQueue中的mIdleHandlers是可以被反射的,这个变量保存了所有将要执行的IdleHandler,我们只需要把ArrayList类型的mIdleHandlers,通过反射,替换为MyArrayList,在我们自定义的MyArrayList中重写add方法,再将我们自定义的MyIdleHandler添加到MyArrayList中,就完成了“偷天换日”。
    • 从此之后MessageQueue每次执行queueIdle回调方法,都会执行到我们的MyIdleHandler中的的queueIdle方法,就可以在这里监控queueIdle的执行时间了。
  • 代码如下所示
    private static void detectIdleHandler() {
        try {
            MessageQueue mainQueue = Looper.getMainLooper().getQueue();
            Field field = MessageQueue.class.getDeclaredField("mIdleHandlers");
            field.setAccessible(true);
            MyArrayList<MessageQueue.IdleHandler> myIdleHandlerArrayList = new MyArrayList<>();
            field.set(mainQueue, myIdleHandlerArrayList);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
    
    static class MyArrayList<T> extends ArrayList {
        Map<MessageQueue.IdleHandler, MyIdleHandler> map = new HashMap<>();
    
        @Override
        public boolean add(Object o) {
            if (o instanceof MessageQueue.IdleHandler) {
                MyIdleHandler myIdleHandler = new MyIdleHandler((MessageQueue.IdleHandler) o);
                map.put((MessageQueue.IdleHandler) o, myIdleHandler);
                return super.add(myIdleHandler);
            }
            return super.add(o);
        }
    
        @Override
        public boolean remove(@Nullable Object o) {
            if (o instanceof MyIdleHandler) {
                MessageQueue.IdleHandler idleHandler = ((MyIdleHandler) o).idleHandler;
                map.remove(idleHandler);
                return super.remove(o);
            } else {
                MyIdleHandler myIdleHandler = map.remove(o);
                if (myIdleHandler != null) {
                    return super.remove(myIdleHandler);
                }
                return super.remove(o);
            }
        }
    }

5.6 监控TouchEvent卡顿

  • 那么TouchEvent我们有什么办法监控吗?
    • 首先想到的可能是反射View的mListenerInfo,然后进一步替换其中的mTouchListener,但是这需要我们枚举所有需要被监控的View,全部反射替换一遍,这完全是憨憨行为。
  • 那有没有更加根本,全局性的方法呢?
    • 暂时还没想到

5.7 监控SyncBarrier泄漏

05.参考资料

  • 需要列出方案设计过程的文档,包括但不局限于PM需求文档,技术参考文档等。
  • 微信卡顿监测方案
    • https://mp.weixin.qq.com/s/3dubi2GVW_rVFZZztCpsKg
  • 卡顿监控方案
    • https://github.com/markzhai/AndroidPerformanceMonitor
  • 卡顿监控
    • https://github.com/Knight-ZXW/BlockCanaryX
  • Android卡顿监测的方方面面
    • https://mp.weixin.qq.com/s/-pfWolCWNK326JEkWNUbew
  • 【卡顿优化】卡顿问题如何监控?
    • https://juejin.cn/post/7222651312073850935
贡献者: yangchong211
上一篇
05.CPU消耗优化实践
下一篇
07.卡顿治理优化实践