性能优化之Matrix

本文讲解微信团队开源的 Android 性能优化检测工具-Matrix 中 TracePlugin 原理,分析检测慢函数、卡顿的过程 。

原理

Trace Plugin 代码框架

Trace Canary通过字节码插桩的方式在编译期预埋了方法进入、方法退出的埋点。运行期,慢函数检测、FPS检测、卡顿检测、启动检测使用这些埋点信息排查具体哪个函数导致的异常。

编译期

函数埋点思路:

代理编译期间的任务 transformClassesWithDexTask,将全局 class 文件作为输入,利用 ASM 工具,修改字节码的方式,在编译期修改所有 class 文件中的函数字节码,对所有函数前后进行打点插桩。

(图示1)matrix tracecanary总框架

(图示2)matrix 编译期方法代码插装分析

代码插桩的整体流程如上图。在打包过程中,hook生成Dex的Task任务,添加方法插桩的逻辑。hook点是在Proguard之后,Class已经被混淆了,所以需要考虑类混淆的问题。

插桩代码逻辑大致分为三步:

  • hook原有的Task,执行自己的MatrixTraceTransform,并在最后执行原逻辑
  • 在方法插桩之前先要读取ClassMapping文件,获取混淆前方法、混淆后方法的映射关系并存储在MappingCollector中。
  • 之后遍历所有Dir、Jar中的Class文件,实际代码执行的时候遍历了两次。

第一次遍历Class,获取所有待插桩的Method信息,并将信息输出到methodMap文件中;
第二次遍历Class,利用ASM执行Method插桩逻辑。

运行期

基于编译期函数插装的逻辑,在运行期,检测到某个方法异常时,会上报一个 methodId,后端通过下图的 methodId 到 method name 的映射关系,追查到有问题的方法。

matrix-methodmapping文件.jpg

(图示3)matrix methodmapping文件

API

  • FrameTracer

计算掉帧率,生成 json 报告上报

  • UIThreadMonitor

ui 主线程监控 回调 doFrame(focusedActivityName, long frameCostMs) 每一帧总耗时,供 FrameTracer 来计算掉帧率(frameCostMs/frameIntervalMs 16.6667 + 1),

  • Choregrapher.getInstance()

监控相邻两次 Vsync 事件通知的时间差

  • LooperMonitor

implements MessageQueue.IdleHandler 监控空闲事件

FPS 帧率检测

clicfg_matrix_trace_fps_time_slice 表示检测总时长,由IDynamicConfig#getInt() 设置即可

慢函数

  • 原理:

上部分讲述了编译器,会在每个方法的执行前后添加 AppMethodBeat.i(int methodId)AppMethodBeat.o(int methodId)的方法调用,methodId 是在编译期生成的,在运行期是一个写死的常量。通过编译期的这个操作,就能感知到具体每个方法的进入、退出操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* hook method when it's called in.
*
* @param methodId
*/
public static void i(int methodId) {

...

if (Thread.currentThread().getId() == sMainThread.getId()) {
if (assertIn) {
android.util.Log.e(TAG, "ERROR!!! AppMethodBeat.i Recursive calls!!!");
return;
}
assertIn = true;
if (sIndex < Constants.BUFFER_SIZE) {
mergeData(methodId, sIndex, true);
} else {
sIndex = -1;
}
++sIndex;
assertIn = false;
}
}

/**
* hook method when it's called out.
*
* @param methodId
*/
public static void o(int methodId) {

...

if (Thread.currentThread().getId() == sMainThread.getId()) {
if (sIndex < Constants.BUFFER_SIZE) {
mergeData(methodId, sIndex, false);
} else {
sIndex = -1;
}
++sIndex;
}
}
  • 检测过程

代码统计了当应用处于前台时,在主线程执行方法的进入、退出,这些信息存储在 AppMethodBeat 的 sBuffer「数组 long[100 * 10000]」 中。当主线程有疑似慢函数存在时,读取 Buffer 的数据,分析可能的慢函数,并上报 json 数据到后端,后端将 methodId 转换为具体的方法声明。

  • 发生场景
    1. 掉帧场景
    2. 类似 ANR 长时间主线程阻塞 UI 绘制的场景

其中,掉帧场景,内部 FrameBeat 类实现了 Choreographer.FrameCallback,可以感知每一帧的绘制时间,通过前后两帧的时间差判断是否有慢函数发生;

主线程长时间阻塞 UI 绘制的场景,LazyScheduler 内有一个 HandlerThread,调用 LazyScheduler.setup 方法向 HandlerThread 的 MQ 发送一个延时5s的消息,若没有发生类似 ANR 的场景,在每一帧的 doFrame 回调中取消这个消息,同时发送一个新的延时 5s 的消息(正常情况下消息是得不到执行的),若发生类似 ANR 的情况,doFrame 没有被回调,这个延时 5s 的消息得到执行,将回调到 onTimeExpire 方法。

  • 生成映射文件

目前生成函数堆栈映射文件在工程的 app/build/outputs/mapping/debug/methodMapping.txt,如下格式

  • 分析结果

1
2
3
4
5
6
7

123570,1,com.google.zxing.client.activity.MipcaActivityCapture onWindowFocusChanged (Z)V
...

* 第一个数字表示分配方法的Id,-1表示插桩为activity加入的onWindowFocusChanged方法。其他方法从1开始计数
* 第二个数字表示方法权限修饰符,常见的值为ACC_PUBLIC = 1; ACC_PRIVATE = 2;ACC_PROTECTED = 4; ACC_STATIC = 8等等。1即表示public方法
* 第三个参数标识类名 包名+类名;方法名 onWindowFocusChanged;参数及返回值类型Z表示参数为boolean类型,V表示返回值为空

卡顿

  • 原因

主线程执行繁重的UI绘制、大量的计算或IO等耗时操作

  • 方案

业内主要框架的主要思想是,监控主线程执行耗时,当超过阈值时,dump出当前主线程的执行堆栈,通过堆栈分析找到卡顿原因。

  • 监控原理

    1. 依赖主线程 Looper,监控每次 dispatchMessage 的执行耗时。(BlockCanary)
    2. 依赖 Choreographer 模块,监控相邻两次 Vsync 事件通知的时间差。(ArgusAPM、LogMonitor)
  • 生成结果

1
2
3
4
5
6
* scene 表示场景,使用 Activity + Fragment 类名作为唯一标志
* dropLevel 是表示掉帧情况-掉帧次数,衡量帧率掉帧的水平
* dropSum 总共掉帧的帧数
* fps 表示当前帧率

掉帧计算 final int droppedCount = (int) ((frameNanos - lastFrameNanos) / REFRESH_RATE_MS);
  • 常见卡顿场景

    • 布局嵌套层次太深,可以使用 merge、viewStub、include 来优化
    • onDraw() 里面循环创建了大量临时变量,频繁触发 GC
    • 主线程等待优先级子线程问题(锁同步问题)
    • 主线程执行耗时操作,阻塞主线程执行(同步读写文件,DB 操作)

Q&A

  • 如何监听app 是否退到后台?

通过 Application.ActivityLifecycleCallbacks 接口向全局 app 注册监听,当有 onActivityStarted(activity) 时,主动标记前台为 true,当 onActivityStopped(activity) 时,先判断当前 activity 堆栈里是否有被 paused 的页面,如果没有,则表示已退到后台。

  • FPS 检测时,如何判断当前是在 drawing?

  • stack 慢函数堆栈如何分析?

可以参考 Matrix issue #104 stack 如何分析,或者 hotfix/0.4.x sample-android

参考

WeChat-Matrix

Matrix Stack

其它 APM

Choreographer

悬浮窗

进程优先级

Android是如何管理内存的?

Android Davik vs Java JVM?

性能实践

Gradle

都看到这了,不打赏下?您的支持将鼓励我继续创作!:)