转载:https://mp.weixin.qq.com/s/W4-1tfepKg2XMYvVn62B-Q

什么是卡顿

什么是卡顿,很多人能马上联系到的是帧率 FPS (每秒显示帧数)。那么多低的 FPS 才是卡顿呢?又或者低 FPS 真的就是卡顿吗?(以下 FPS 默认指平均帧率)

其实并非如此,举个例子,游戏玩家通常追求更流畅的游戏画面体验一般要达到 60FPS 以上,但我们平时看到的大部分电影或视频 FPS 其实不高,一般只有 25FPS ~ 30FPS,而实际上我们也没有觉得卡顿。在人眼结构上看,当一组动作在 1 秒内有 12 次变化(即 12FPS),我们会认为这组动作是连贯的;而当大于 60FPS 时,人眼很难区分出来明显的变化,所以 60FPS 也一直作为业界衡量一个界面流畅程度的重要指标。一个稳定在 30FPS 的动画,我们不会认为是卡顿的,但一旦 FPS 很不稳定,人眼往往容易感知到。

FPS 低并不意味着卡顿发生,而卡顿发生 FPS 一定不高。FPS 可以衡量一个界面的流程性,但往往不能很直观的衡量卡顿的发生,这里有另一个指标(掉帧程度)可以更直观地衡量卡顿。

什么是掉帧(跳帧)?按照理想帧率 60FPS 这个指标,计算出平均每一帧的准备时间有 1000ms/60 = 16.6667ms,如果一帧的准备时间超出这个值,则认为发生掉帧,超出的时间越长,掉帧程度越严重。假设每帧准备时间约 32ms,每次只掉一帧,那么 1 秒内实际只刷新 30 帧,即平均帧率只有 30FPS,但这时往往不会觉得是卡顿。反而如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到。所以界面的掉帧程度,往往可以更直观的反映出卡顿。

怎么衡量流程性

我们将掉帧数划分出几个区间进行定级,掉帧数小于 3 帧的情况属于最佳,依次类推,见下表:

相比单看平均帧率,掉帧程度的分布可以明显的看出,界面卡顿(平均帧率低)的原因是因为连续轻微的掉帧(下图1),还是某次严重掉帧造成的(下图2)。

再通过 Activity 区分不同场景,计算每个界面在有效绘制的时间片内,掉帧程度的分布情况及平均帧率,从而来评估出一个界面的整体流畅程度。

具体如何计算界面在有交互的场景下的平均帧率及掉帧情况,可见 Matrix 中 TraceCannary 模块。

可重现的卡顿

在微信Android客户端中,每天都面临着各式各样的卡顿,其中有一部分通常是可本地复现的,对于这种容易重现的场景,一般我们在开发及体验测试阶段容易注意得到,而定位卡顿的根源,我们常用的方法是通过系统工具 TraceView 来抓取卡顿过程中函数的执行情况(堆栈,耗时,调用次数等),此方法支持在代码中进行打点,也支持在 TraceView/Android Studio Profiler 中手动启动收集。trace 的信息如下:

通过 TraceView 的可视化界面,我们可以具体知道某个过程中的调用栈信息及各个函数的执行次数与耗时,能比较直观的找到严重耗时的函数,帮助我们快速解决卡顿问题。

不可重现的卡顿

但往往大部分卡顿是很难及时发现的,不可重现的卡顿,经常出现在线上用户的真实使用过程中,这种卡顿往往跟机器性能,手机环境,甚至是操作偏好等因素息息相关。一般也是从用户反馈中得到,通常表述为“新版本变卡了”,“朋友圈很卡”,“聊天经常无响应”,我们很难在这种描述中,直接洞察到卡顿的根源,甚至有些连卡顿的场景都不知道,很难准确重现,所以这种卡顿容易让人摸不着头脑。当然作为开发者,我更希望用户反馈的是,“亲爱的开发同志,本次卡顿罪魁祸首是,某某函数耗时666ms,请速速解决它。”

那么面对这种卡顿,我们是不是就束手无策了呢?并非如此。

解决方案

我们知道造成卡顿的直接原因通常是,主线程执行繁重的UI绘制、大量的计算或IO等耗时操作。

业界有几种常见解决方案,都可以从一定程度上,帮助开发者快速定位到卡顿的堆栈,如 BlockCanary、ArgusAPM、LogMonitor 。这些方案的主要思想是,监控主线程执行耗时,当超过阈值时,dump出当前主线程的执行堆栈,通过堆栈分析找到卡顿原因。

从监控主线程的实现原理上,主要分为两种:

  • 依赖主线程 Looper,监控每次 dispatchMessage 的执行耗时。(BlockCanary)

  • 依赖 Choreographer 模块,监控相邻两次 Vsync 事件通知的时间差。(ArgusAPM、LogMonitor)

简单看下这两种方案的实现原理:

第一种方案,看下 Looper#loop 代码片段

public static void loop() {...for (;;) {...// This must be in a local variable, in case a UI event sets the loggerPrinter logging = me.mLogging;if (logging != null) {logging.println(">>>>> Dispatching to " + msg.target + " " +msg.callback + ": " + msg.what);}msg.target.dispatchMessage(msg);if (logging != null) {logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);}...}
}

主线程所有执行的任务都在 dispatchMessage 方法中派发执行完成,我们通过 setMessageLogging 的方式给主线程的 Looper 设置一个 Printer ,因为 dispatchMessage 执行前后都会打印对应信息,在执行前利用另外一条线程,通过 Thread#getStackTrace 接口,以轮询的方式获取主线程执行堆栈信息并记录起来,同时统计每次 dispatchMessage 方法执行耗时,当超出阈值时,将该次获取的堆栈进行分析上报,从而来捕捉卡顿信息,否则丢弃此次记录的堆栈信息。

第二种方案,利用系统 Choreographer 模块,向该模块注册一个 FrameCallback 监听对象,同时通过另外一条线程循环记录主线程堆栈信息,并在每次 Vsync 事件 doFrame 通知回来时,循环注册该监听对象,间接统计两次 Vsync 事件的时间间隔,当超出阈值时,取出记录的堆栈进行分析上报。

简单代码实现如下:

Choreographer.getInstance().postFrameCallback(newChoreographer.FrameCallback() {@Override    public void doFrame(long frameTimeNanos) {if(frameTimeNanos - mLastFrameNanos > 100) {...}mLastFrameNanos = frameTimeNanos;Choreographer.getInstance().postFrameCallback(this);}
});

这两种方案,可以较方便的捕捉到卡顿的堆栈,但其最大的不足在于,无法获取到各个函数的执行耗时,对于稍微复杂一点的堆栈,很难找出可能耗时的函数,也就很难找到卡顿的原因。另外,通过其他线程循环获取主线程的堆栈,如果稍微处理不及时,很容易导致获取的堆栈有所偏移,不够准确,加上没有耗时信息,卡顿也就不好定位。

所以我们希望寻求一种可以在线上准确地捕捉卡顿堆栈,又能计算出各个函数执行耗时的方案。而要计算函数的执行耗时,最关键的点在于如何对执行过程中的函数进行打点监控。

这里介绍两种方式:

  1. 在应用启动时,默认打开 Trace 功能(Debug.startMethodTracing),应用内所有函数在执行前后将会经过该函数(dalvik 上 dvmMethodTraceAdd 函数 或  art 上 Trace::LogMethodTraceEvent 函数),通过hack手段代理该函数,在每个执行方法前后进行打点记录。

  2. 修改字节码的方式,在编译期修改所有 class 文件中的函数字节码,对所有函数前后进行打点插桩。

第一种方案,最大的好处是能统计到包括系统函数在内的所有函数出入口,对代码或字节码不用做任何修改,所以对apk包的大小没有影响,但由于方式比较hack,在兼容性和安全性上存在一定的风险。

第二种方案,利用 Java 字节码修改工具(如 BCEL、ASM、Javassis等),在编译期间收集所有生成的 class 文件,扫描文件内的方法指令进行统一的打点插桩,同样也可以高效的记录函数执行过程中的信息,相比第一种方案,除了无法统计系统内执行的函数,其它应用内实现的函数都可以覆盖到。而往往造成卡顿的函数并不是系统内执行的函数,一般都是我们应用开发实现的函数,所以这里无法统计系统内执行的函数对卡顿的定位影响不大。此方案无需 hook 任何函数,所以在兼容性方面会比第一个方案更可靠。

在这考虑上,我们最终选择了修改字节码的方案,来实现 Matrix-TraceCannary 模块,解决其它方案中卡顿堆栈无耗时信息的主要问题,来帮助开发者发现及定位卡顿问题。

实现细节

编译期:

通过代理编译期间的任务 transformClassesWithDexTask,将全局 class 文件作为输入,利用 ASM 工具,高效地对所有 class 文件进行扫描及插桩。

插桩过程有几个关键点:

1、选择在该编译任务执行时插桩,是因为 proguard 操作是在该任务之前就完成的,意味着插桩时的 class 文件已经被混淆过的。而选择 proguard 之后去插桩,是因为如果提前插桩会造成部分方法不符合内联规则,没法在 proguard 时进行优化,最终导致程序方法数无法减少,从而引发方法数过大问题。

2、为了减少插桩量及性能损耗,通过遍历 class 方法指令集,判断扫描的函数是否只含有 PUT/READ FIELD 等简单的指令,来过滤一些默认或匿名构造函数,以及 get/set 等简单不耗时函数。

3、针对界面启动耗时,因为要统计从 Activity#onCreate 到 Activity#onWindowFocusChange 间的耗时,所以在插桩过程中需要收集应用内所有 Activity 的实现类,并覆盖 onWindowFocusChange 函数进行打点。

4、为了方便及高效记录函数执行过程,我们为每个插桩的函数分配一个独立 ID,在插桩过程中,记录插桩的函数签名及分配的 ID,在插桩完成后输出一份 mapping,作为数据上报后的解析支持。

归纳起来,编译期所做的工作如下图:

运行期:

编译期已经对全局的函数进行插桩,在运行期间每个函数的执行前后都会调用 MethodBeat.i/o 的方法,如果是在主线程中执行,则在函数的执行前后获取当前距离 MethodBeat 模块初始化的时间 offset(为了压缩数据,存进一个long类型变量中),并将当前执行的是 MethodBeat i或者o、mehtod id 及时间 offset,存放到一个 long 类型变量中,记录到一个预先初始化好的数组 long[] 中 index 的位置(预先分配记录数据的 buffer 长度为 100w,内存占用约 7.6M)。数据存储如下图:

通过向 Choreographer 注册监听,在每一帧 doframe 回调时判断距离上一帧的时间差是否超出阈值(卡顿),如果超出阈值,则获取数组 index 前的所有数据(即两帧之间的所有函数执行信息)进行分析上报。同时,我们在每一帧 doFrame 到来时,重置一个定时器,如果 5s 内没有 cancel,则认为 ANR 发生,这时会主动取出当前记录的 buffer 数据进行独立分析上报,对这种 ANR 事件进行单独监控及定位。

另外,考虑到每个方法执行前后都获取系统时间(System.nanoTime)会对性能影响比较大,而实际上,单个函数执行耗时小于 5ms 的情况,对卡顿来说不是主要原因,可以忽略不计,如果是多次调用的情况,则在它的父级方法中可以反映出来,所以为了减少对性能的影响,通过另一条更新时间的线程每 5ms 去更新一个时间变量,而每个方法执行前后只读取该变量来减少性能损耗。

堆栈聚类问题:如果将收集的原始数据进行上报,数据量很大而且后台很难聚类有问题的堆栈,所以在上报之前需要对采集的数据进行简单的整合及裁剪,并分析出一个能代表卡顿堆栈的 key,方便后台聚合。

通过遍历采集的 buffer ,相邻 i 与 o 为一次完整函数执行,计算出一个调用树及每个函数执行耗时,并对每一级中的一些相同执行函数做聚合,最后通过一个简单策略,分析出主要耗时的那一级函数,作为代表卡顿堆栈的key。

性能数据

TraceCanary 其本身作为一款性能检测工具,对自身性能的损耗要求比较高,以下是插桩前后的对比数据:

从实测数据上看,TraceCanary 对于好机器的性能影响可忽略,对差机器性能稍有损耗,但影响很小。对安装包大小影响,对于微信这种大体量的应用,实际插桩函数 16w+,对安装包增加了 800K 左右。

方案对比:

以下是对比系统工具及一些比较热门的方案

小结

最终,我们希望通过观察大盘整体的帧率及掉帧程度,来评估并监控一些重要场景的流畅性。通过一个闭环的流程,利用 Matrix-TraceCanary 模块从客户端对卡顿进行捕捉与分析上报,通过后台聚类问题堆栈及版本对比,找到卡顿堆栈的责任人,通知其进行解决优化,而最终处理的效果也会在 Matrix 平台中反应出来。在这样不断发现卡顿,解决卡顿的过程中,希望尽可能地优化微信Android客户端的流畅性,给用户带来更好的体验。

如今 Matrix 已经开源,TraceCanary 模块依然在不断优化中,解决卡顿问题,我们一直在路上。

关于 Matrix

当前 matrix 已经开源,开源地址:https://github.com/Tencent/Matrix 。

Matrix TraceCanary相关推荐

  1. 记录一个android性能优化宝藏级总结

    发现一个android性能优化文章宝藏级总结,太赞了,感谢大佬的无私奉献总结,防止丢失,在此记录一下 传送门 复制一些目录,增加一些篇幅{嘻嘻} 优化心得和经验 抖音 Android 性能优化系列:启 ...

  2. Android 性能优化必知必会

    做了这么久性能相关的工作,也接触了不少模块,说实话要做好性能这一块,真心不容易.为什么这么说? 是因为需要接触的知识实在是太多了, Android 是一个整体,牵一发而动全身,不是说只懂一个模块就可以 ...

  3. Android 性能优化必知必会(2020-5-16)

    做了这么久性能相关的工作,也接触了不少模块,说实话要做好性能这一块,真心不容易.为什么这么说? 是因为需要接触的知识实在是太多了, Android 是一个整体,牵一发而动全身,不是说只懂一个模块就可以 ...

  4. Android 高级面试高频知识点,flutter跳转到appstore

    Android JNI 之 Bitmap 操作 Android组件化方案及组件消息总线modular-event实战 Android 组件化最佳实践 Android中JSBridge的原理和实现 An ...

  5. Matrix原理分析系列之开篇

    背景 应用性能监控和性能优化一直是老生常谈的话题,很多大厂都有专门的团队在做,腾讯就做了一款性能监控的框架matrix,且已经开源,对于个人而言,这是一次绝佳的学习机会,像如何做到启动耗时的计算,就要 ...

  6. Android 自定义View ——Matrix (矩阵)

    Matrix的作用: Matrix类包含一个3x3矩阵,用于转换坐标 Matrix (矩阵) 的原理很遗憾自己目前也是含糊的很,这里就不说了,记录自己在项目使用的方法, 这里就简单的记录下Matrix ...

  7. Codeforces 903F Clear The Matrix(状态压缩DP)

    题目链接 Clear The Matrix 题意 给定一个$4 * n$的矩形,里面的元素为$'.'$或$'*'$.现在有$4$种正方形可以覆盖掉$'*'$,正方形的边长分别为$1,2,3,4$. 求 ...

  8. ie旋转滤镜Matrix

    旋转一个元素算是一个比较常见的需求了吧,在支持CSS3的浏览器中可以使用transform很容易地实现,这里有介绍:http://www.css88.com/archives/2168,这里有演示ht ...

  9. Eigen(1):Matrix模板类

    Matrix是一个模板类,利用模板类可以定义矩阵类. 矩阵类模板: 1Matrix类有6个模板参数,只需要了解前3个就好了. Matrix<typename Scalar, int RowsAt ...

  10. CUDA Samples: matrix multiplication(C = A * B)

    以下CUDA sample是分别用C++和CUDA实现的两矩阵相乘运算code即C= A*B,CUDA中包含了两种核函数的实现方法,第一种方法来自于CUDA Samples\v8.0\0_Simple ...

最新文章

  1. postfix邮箱服务
  2. Spring Cloud Alibaba 之 服务治理:Nacos 如何实现微服务服务治理
  3. 在Windows7 (SP1)配置IIS7.5 + .Net Framework 4.0.30319
  4. [转]linq to sql (Group By/Having/Count/Sum/Min/Max/Avg操作符)
  5. tensorflow随笔-读写数据tf.data
  6. 百度宣布架构调整,BAT要在To B业务重开一局
  7. 系统操作手册_东芝CT操作手册——系统概述
  8. YFI 创始人 Andre Cronje:有四个新产品待发布,但担心在推出后被分叉
  9. 迟到的,2016年终总结
  10. 实战丨基于接口的银行系统自动化测试实践
  11. 字符串当id用 转换成json对象
  12. Centos 8 安装和配置 VNC 服务,让 MAC 可以远程桌面管理Centos(linux)
  13. excel 工作表保护密码破解代码
  14. AI+IoT行业“飞轮效应”凸显,全球云服务能力将发挥关键作用
  15. 演绎类清吧音响怎么选?
  16. steam 集换式卡牌挂卡软件 ASF Linux服务器搭建
  17. c语言中NULL到底是什么?
  18. 2021-06-05按键精灵实现远程获取消息内容
  19. 怎么添加校园邮箱到Outlook?
  20. Linux串口调试总结

热门文章

  1. 如何规范小开发公司的测试流程。?
  2. windows10 wifi热点手机连接显示无网络连接问题解决
  3. 魔兽争霸3 ce基址 偏移
  4. 基于javaweb的文具学习用品商城系统(java+ssm+jsp+jquery+mysql)
  5. [渝粤教育] 中国地质大学(武汉) 走近国粹 中国陶瓷 参考 资料
  6. 大会没看够?2021 Google 开发者大会总结看这里
  7. IP 地址 与硬件地址
  8. Nginx常用Rewrite(伪静态规则)WordPress/PHPCMS/ECSHOP/ShopEX/SaBlog/Discuz/DiscuzX/PHPWind/Typecho/DEDECMS...
  9. 推荐国内外,安全好用的电子邮箱
  10. 网课答案接口 查题系统