Android 卡顿研究
[TOC]
稳定化,不是说说而已
基础概念
这里主要是根据张绍文老师的文章做的笔记,根据张绍文老师的文笔去实践具体卡顿监控的内容
散列知识点
- JVM中的线程切换大概花费CPU 20000个时钟周期
CPU
这里CPU需要单独搞出来提一下,卡顿优化前需要搞清楚CPU是什么,能干什么,正在干什么,然后才是“什么”这个区间里面应用程序此时的参数是否合理,优化的空间又是多少
查看一个CPU的参数需要看CPU的频率,核心等参数,具体参考 Wiki
这里就仅仅点相对重要的一些参数含义
-
时钟周期:CPU每秒可以完成几个时钟周期,如
可以完成这么多个时钟周期 -
机器周期:主存中读取一个指令字的最短时间(由于CPU内部的操作速度较快.而CPU访问一次主存所花的时间较长,因此机器周期通常用主存中读取一个指令字的最短时间来规定。),所以 机器周期 = 时钟周期 * n(n >= 1)
-
指令周期:完成一个指令需要的时间,一般由 几个或者一个机器周期组成,相当于 指令周期 = 机器周期 * n(n >= 1)
方法论
指标
- CPU使用率
如果 CPU 使用率长期大于 60% ,表示系统处于繁忙状态,就需要进一步分析用户时间和系统时间的比例。对于普通应用程序,系统时间不会长期高于 30%,如果超过这个值,就得考虑是否I/O调用过多或者锁调用的过于频繁的问题。利用Android Studio的profile也能查看CPU的使用率
- CPU饱和度
CPU 饱和度反映的是线程排队等待 CPU 的情况,也就是 CPU 的负载情况。
CPU 饱和度首先会跟应用的线程数有关,如果启动的线程过多,易导致系统不断地切换执行的线程,把大量的时间浪费在上下文切换,要知道每一次 CPU 上下文切换都需要刷新寄存器和计数器,至少需要几十纳秒的时间。
可以通过vmstat命令查看CPU上下文切换次数
proc/self/sched:
nr_voluntary_switches:主动上下文切换次数,因为线程无法获取资源导致上下文切换,最普遍的就是IO
nr_involuntary_switches:被动上下文切换次数,线程被系统强制调度导致上下文切换,例如大量线程在抢占CPU
se.statistics.iowait_count:IO 等待次数
se.statistics.iowait_sum:IO 等待时间
此外也可以通过 uptime 命令可以检查 CPU 在 1 分钟、5 分钟和 15 分钟内的平均负载。比如一个 4 核的 CPU,如果当前平均负载是 8,这意味着每个 CPU 上有一个线程在运行,还有一个线程在等待。一般平均负载建议控制在“0.7 × 核数”以内。
00:02:39 up 7 days, 46 min, 0 users,
load average: 13.91, 14.70, 14.32
另外一个会影响 CPU 饱和度的是线程优先级,线程优先级会影响 Android 系统的调度策略,它主要由 nice 和 cgroup 类型共同决定。nice 值越低,抢占 CPU 时间片的能力越强。当 CPU 空闲时,线程的优先级对执行效率的影响并不会特别明显,但在 CPU 繁忙的时候,线程调度会对执行效率有非常大的影响。
关于线程优先级,你需要注意是否存在高优先级的线程空等低优先级线程,例如主线程等待某个后台线程的锁。从应用程序的角度来看,无论是用户时间、系统时间,还是等待 CPU 的调度,都是程序运行花费的时间。
市场调研
Traceview 和 systrace 都是我们比较熟悉的排查卡顿的工具,从实现上这些工具分为两个流派。
第一个流派是 instrument。获取一段时间内所有函数的调用过程,可以通过分析这段时间内的函数调用流程,再进一步分析待优化的点。
第二个流派是 sample。有选择性或者采用抽样的方式观察某些函数调用过程,可以通过这些有限的信息推测出流程中的可疑点,然后再继续细化分析。
根据流派,对目前市场上的性能监控工具做一些调研和使用,包括但不限于官方提供的性能监控工具,如systrace,Matrix等,关于Android上Systrace的使用可以参考我之前写过的一个blog里面有提到过如何使用Android 性能优化
选择哪种工具,需要看具体的场景。如果需要分析 Native 代码的耗时,可以选择 Simpleperf;如果想分析系统调用,可以选择 systrace;如果想分析整个程序执行流程的耗时,可以选择 Traceview 或者插桩版本的 systrace。
对目前市场上的一些性能监控框架做基本调研,如DoraemonKit,Matrix,BlockCanary
BlockCanary & DoraemonKit
这里之所以把两个都放到一起,是因为滴滴的哆啦A梦的卡顿检测其实就是blockCanary,实现很简单,但是思路很巧妙~
想要检测卡顿,其实就是检测主线程的运行情况,为什么这么说呢,因为每一帧渲染数据的创建,就依托于主线程来创建,而想要保证每一帧CPU都能在16.7ms内(这里仅限于60帧这种情况,如果是90或者120,可以反推的哈~)完成工作,这样就不会出现丢帧的现象,也就不会造成卡顿,而我们就监测每个而如何监测主线程的运行情况呢?这里需要知道安卓中的handler机制,通过检测每次处理主线程消息的耗时情况,就能够知道是否产生了卡顿,而在发生卡顿的时候,同时抓取此时主线程的堆栈,那么就更能方便的定位到需要优化的代码。
BlockCanary核心的地方,主要分为两个部分:
- 检测handleMessage
- 主线程抓取堆栈的部分
handleMessage
在主线程Looper每次处理消息的过程中,通过hook主线程Looper每次处理消息的过程,在处理消息之前记录一个时间戳,处理完消息之后记录一个时间戳,那么两个时间的差值,就是处理一条消息所花费的时间。通过给这个时间设置阈值,如:处理时间 > 阈值时间(430ms > 200ms)那么就认为是发生了卡顿
这里的hook其实非常简单,因为framework给咱们预留了这样的口子,可以看下在handlemessage这里的源码:
public void loop(){
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// 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);
}
......
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
final long dispatchEnd;
try {
msg.target.dispatchMessage(msg);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
msg.recycleUnchecked();
}
}
......
/**
* Control logging of messages as they are processed by this Looper. If
* enabled, a log message will be written to <var>printer</var>
* at the beginning and ending of each message dispatch, identifying the
* target Handler and message contents.
*
* @param printer A Printer object that will receive log messages, or
* null to disable message logging.
*/
public void setMessageLogging(@Nullable Printer printer) {
mLogging = printer;
}
可见们只需要手动调用Looper.setMessageLogging方法就能给线程looper对象设置printer对象,在每次处理消息的时候,通过监听printer的println回调,解析出内容,就能知道知道分发消息的开始和结束了
//Printer 接口
public interface Printer {
/**
* Write a line of text to the output. There is no need to terminate
* the given string with a newline.
*/
void println(String x);
}
通过过滤printer.println打印的内容,判断是否是在消息分发处理相关的内容,然后进行时间差的计算,来判断卡顿是否发生
//LooperPrinter
class LooperPrinter implements Printer {
public Printer origin;
boolean isHasChecked = false;
boolean isValid = false;
LooperPrinter(Printer printer) {
this.origin = printer;
}
@Override
public void println(String x) {
if (null != origin) {
origin.println(x);
if (origin == this) {
throw new RuntimeException(MonitorConstants.LOG_TAG + " origin == this");
}
}
if (!isHasChecked) {
isValid = x.charAt(0) == '>' || x.charAt(0) == '<';
isHasChecked = true;
if (!isValid) {
InsectLogger.ne("[println] Printer is inValid! x:%s", x);
}
}
if (isValid) {
dispatch(x.charAt(0) == '>', x);
}
}
}
那么依据主线程卡顿的监控就已经完成了,接下来是对于卡顿问题的定位,也就是对主线程堆栈的抓取
dumpStack
这里不完全参照blockCanary的实现,但是大家都是为了解决能够抓取到问题发生的堆栈,这里先说一下对于主线程堆栈dump需要关注的问题。不同的抓取策略也是为了解决这个问题,此处先不考虑对性能带来的影响
假设此时发生了卡顿,那么在调用getStackTrace的时候,这时候虚拟机中所跟踪的堆栈中会把当前记录的一些堆栈返回。通过在发生卡顿的时候,dump出当前的堆栈,记录下来,再追溯问题的时候直接看存储下来的堆栈信息,那么定位问题就会方便很多,而实际情况下并不能如此理想,因为从VM中取出的堆栈dalvik.system.VMStack#getThreadStackTrace
返回的数据是未知的,不能保证里面到底有多少内容,可能只有一部分,这样就可能会遗漏真正的问题所在,可以参考下图~
可以看到真正有问题的函数其实是FunctionA-1,而如果捞出来的堆栈只有FunctionA-2或者A-3的话,当然可以优化A-3,但是会漏掉真正发生问题的函数。所以对于堆栈的抓取,基于VMStack抓取堆栈的方式下,笔者思考了两种方案来解决这样的问题,这两种应该也是市面上基于VMStack方式的大概方案,再深入往VM中去研究感觉可以有,但是不推荐,因为成本高,且回报的话不太会有预期中的高。
周期性Dump
通过每个一段时间从VM中获取主线程的堆栈,在发生卡顿的时候,过滤出时间,然后直接取出这段时间内的堆栈来进行问题排查。
周期Dmp.png
在实现的时候需要注意的一些小细节:
- 循环队列
- 堆栈去重
- 时间区间筛选
起止Dump
这里可以“忽略”多线程的特性,因为我们关注的仅仅是主线程,那么只需要在消息分发之初dump一次堆栈,然后再消息处理之后再dump一次堆栈,这样既能在dump出来的堆栈中发现可能存在的问题,同时又能自行推断这中间的执行过程来观测代码中出现的问题。当然不可缺少一个代码耗时检测的小工具~
Matrix
关于matrix解剖,需要先了解定义,再根据具体代码进行分析,最后根据代码梳理出实现的思路
卡顿定义
微信开发者对于卡顿的定义,很简单,很清晰,很明了,这里就cv过来了,一定要仔细读对卡顿的定义
什么是卡顿,很多人能马上联系到的是帧率 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),那么这一次的变化,通常很容易感知到。所以界面的掉帧程度,往往可以更直观的反映出卡顿。
流畅性
综上所述,其实可以明白对于卡顿的定义,衡量流畅性的指标可以简单理解为:
- 在用户有操作的前提下
- 平均掉帧率,只有在某一时刻发生的掉帧情况远远大于其他时刻,那么才界定为卡顿,这也是上面说到的界面的掉帧程度,才更直观的反映出卡顿
Best | Normal | Middle | High | Frozen |
---|---|---|---|---|
[0:3) | [3:9) | [9:24) | [24:42) | [42:∞) |
Code实现
关于反射 Choreographer 来做到如何监测用户触发后开始计算平均帧率
关于Choreographer的知识相关,这里不做赘述,只根据实现原理来对使用的地方做说明
- 用户触发刷新
了解下源码中callbackType的含义
/**
* Callback type: Input callback. Runs first.
* @hide
*/
public static final int CALLBACK_INPUT = 0;
/**
* Callback type: Animation callback. Runs before traversals.
* @hide
*/
@TestApi
public static final int CALLBACK_ANIMATION = 1;
/**
* Callback type: Traversal callback. Handles layout and draw. Runs
* after all other asynchronous messages have been handled.
* @hide
*/
public static final int CALLBACK_TRAVERSAL = 2;
显而易见,CALLBACK_INPUT都已经注释好了,首先run的是这个类型的回调,然后我们平时注册的又是什么样子呢?
/**
* Posts a frame callback to run on the next frame.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @param callback The frame callback to run during the next frame.
*
* @see #postFrameCallbackDelayed
* @see #removeFrameCallback
*/
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}
/**
* Posts a frame callback to run on the next frame after the specified delay.
* <p>
* The callback runs once then is automatically removed.
* </p>
*
* @param callback The frame callback to run during the next frame.
* @param delayMillis The delay time in milliseconds.
*
* @see #postFrameCallback
* @see #removeFrameCallback
*/
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
注册类型的callbackType为ANIMATION的,而ANIMATION的type又是什么时候回调呢?
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
......
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
//优先执行CALLBACK_INPUT类型链表里面的回调
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
......
}
可见是在执行完优先级最高的输入类型的回调才会回调ANIMATION的(注意这里提供的两个参数,第一个是执行该frame的时间戳,第二个是当前帧号,是native调用,在DisplayEventReceiver事件中收到后维护的一个成员变量,具体实现类也在Choreagrapher中),而显然不能够符合我们的要求,我们是期望在用户有操作的情况下是否发生丢帧情况
Choreographer类结构.png而如何计算input时机的帧率呢? 势必需要在input类型中添加自己实现的callback,在animation开始的执行的时候,标识为input执行结束
Hook时机.png好了,原理分析完毕,接下来看一下在Matrix中带佬如何实现的,核心类主要是com.tencent.matrix.trace.core.UIThreadMonitor
初始化中先拿到需要hook的方法,然后模拟顺序进行执行
public void init(TraceConfig config) {
......
//反射同步锁对象和对应doFrame的所有回调数组链表对象
choreographer = Choreographer.getInstance();
callbackQueueLock = reflectObject(choreographer, "mLock");
callbackQueues = reflectObject(choreographer, "mCallbackQueues");
//先拿到添加对应回调的可执行反射方法
addInputQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);
addAnimationQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);
addTraversalQueue = reflectChoreographerMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);
frameIntervalNanos = reflectObject(choreographer, "mFrameIntervalNanos");
......
this.isInit = true;
......
}
通过上面分析可得,doframe的回调执行是顺序执行下来,也就是说一个类型的callback执行结束时间,就是下一个类型的开始时间,那么在addCallback的时机也是如此,最开始要添加的则是input类型回调
public void init(TraceConfig config) {
......
choreographer = Choreographer.getInstance();
callbackQueueLock = ReflectUtils.reflectObject(choreographer, "mLock", new Object());
//反射获取回调的数组链表对象,可以理解为单object的hashMap 数组+链表实现
callbackQueues = ReflectUtils.reflectObject(choreographer, "mCallbackQueues", null);
if (null != callbackQueues) {
addInputQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_INPUT], ADD_CALLBACK, long.class, Object.class, Object.class);
addAnimationQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_ANIMATION], ADD_CALLBACK, long.class, Object.class, Object.class);
addTraversalQueue = ReflectUtils.reflectMethod(callbackQueues[CALLBACK_TRAVERSAL], ADD_CALLBACK, long.class, Object.class, Object.class);
}
//主要是拿vsync回调上来的信号开始绘制的时间戳,可以分析出来丢帧数,源码也是这么干的
vsyncReceiver = ReflectUtils.reflectObject(choreographer, "mDisplayEventReceiver", null);
//产生vsync信号的时间戳
frameIntervalNanos = ReflectUtils.reflectObject(choreographer, "mFrameIntervalNanos", Constants.DEFAULT_FRAME_DURATION);
LooperMonitor.register(new LooperMonitor.LooperDispatchListener() {
@Override
public boolean isValid() {
return isAlive;
}
@Override
public void dispatchStart() {
super.dispatchStart();
UIThreadMonitor.this.dispatchBegin();
}
@Override
public void dispatchEnd() {
super.dispatchEnd();
UIThreadMonitor.this.dispatchEnd();
}
});
......
}
public synchronized void onStart() {
......
if (!isAlive) {
this.isAlive = true;
synchronized (this) {
MatrixLog.i(TAG, "[onStart] callbackExist:%s %s", Arrays.toString(callbackExist), Utils.getStack());
callbackExist = new boolean[CALLBACK_LAST + 1];
}
//为三种callback增加状态维护数组
queueStatus = new int[CALLBACK_LAST + 1];
//为三种callback增加耗时数组
queueCost = new long[CALLBACK_LAST + 1];
//首次添加input类型callback
addFrameCallback(CALLBACK_INPUT, this, true);
}
}
可以看到,在添加input类型的回调时,传的是自己,那么来分析一下接下来的run实现
@Override
public void run() {
//来自vsync信号开始
doFrameBegin(token);
//维护input类型数组们的状态
doQueueBegin(CALLBACK_INPUT);
//animation回调注册回调
addFrameCallback(CALLBACK_ANIMATION, new Runnable() {
@Override
public void run() {
//animation回调,input结束
doQueueEnd(CALLBACK_INPUT);
doQueueBegin(CALLBACK_ANIMATION);
}
}, true);
addFrameCallback(CALLBACK_TRAVERSAL, new Runnable() {
@Override
public void run() {
//traversal类型回调,animation结束
doQueueEnd(CALLBACK_ANIMATION);
doQueueBegin(CALLBACK_TRAVERSAL);
}
}, true);
}
可以看到,如此就能得到一个Vsync信号过来的轮回,但是走到这里只能完成一次,matrix如何把每一次串起来的咧?
还记得上面初始化的时候注册looper监听,每次消息的处理开始和结束都会激活一次dispatchStart和dispatchEnd,start这里就不分析了,其实就是往外回调,主要是end
private void dispatchEnd() {
......
//在第一次Vsync开始的时候赋值为true,直接进来
if (isVsyncFrame) {
doFrameEnd(token);
intendedFrameTimeNs = getIntendedFrameTimeNs(startNs);
}
......
//来自一次vsync信号结束
this.isVsyncFrame = false;
}
private void doFrameEnd(long token) {
//下一次input开始,上一次的traversal结束
doQueueEnd(CALLBACK_TRAVERSAL);
......
//开启下一次input轮回
addFrameCallback(CALLBACK_INPUT, this, true);
}
可以看到,这里才是真正的结束,一个完整的Choreographer循环~
卡顿策略
Matrix的文档里面已经非常清楚的用文字描述一个卡顿是如何产生的,以及卡顿的定义
FPS 低并不意味着卡顿发生,而卡顿发生 FPS 一定不高。 FPS 可以衡量一个界面的流程性,但往往不能很直观的衡量卡顿的发生,这里有另一个指标(掉帧程度)可以更直观地衡量卡顿。
什么是掉帧(跳帧)? 按照理想帧率 60FPS 这个指标,计算出平均每一帧的准备时间有 1000ms/60 = 16.6667ms,如果一帧的准备时间超出这个值,则认为发生掉帧,超出的时间越长,掉帧程度越严重。假设每帧准备时间约 32ms,每次只掉一帧,那么 1 秒内实际只刷新 30 帧,即平均帧率只有 30FPS,但这时往往不会觉得是卡顿。反而如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到。所以界面的掉帧程度,往往可以更直观的反映出卡顿。
然后分析下关于瞬时平均帧率的代码需要重点关注的就是com.tencent.matrix.trace.tracer.FrameTracer这个类
在每一次doFrame的回调中去分析这个参数
@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
if (isForeground()) {
notifyListener(focusedActivity, startNs, endNs, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
}
}
private void notifyListener(final String focusedActivity, final long startNs, final long endNs, final boolean isVsyncFrame,
final long intendedFrameTimeNs, final long inputCostNs, final long animationCostNs, final long traversalCostNs) {
try {
//计算丢帧,跟源码的计算方式一致,上一个版本局部变量的声明并没有如此直观,这个版本改了,清爽许多
final long jiter = endNs - intendedFrameTimeNs;
final int dropFrame = (int) (jiter / frameIntervalNs);
synchronized (listeners) {
//listeners目前注册进来的就俩,一个内部类FPSCollect,一个用于UI展示的FrameDecorator
for (final IDoFrameListener listener : listeners) {
if (config.isDevEnv()) {
listener.time = SystemClock.uptimeMillis();
}
if (null != listener.getExecutor()) {
if (listener.getIntervalFrameReplay() > 0) {
//数据收集部分
listener.collect(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
} else {
//卡顿分析部分
listener.getExecutor().execute(new Runnable() {
@Override
public void run() {
listener.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
}
});
}
} else {
listener.doFrameSync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame,
intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
}
}
}
} finally {
}
}
为什么说丢帧计算和源码一致呢? 这里我们可以和源码对比一下:
final long jiter = endNs - intendedFrameTimeNs;
final int dropFrame = (int) (jiter / frameIntervalNs);
//源码 android.view.Choreographer#doFrame
final long jitterNanos = startNanos - frameTimeNanos;
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
这么看上去,瞬间就友好了很多~
从注释里面也能看到,注册的俩监听,一个用于记录,一个用于展示,记录其实就是填充此时此刻的关于FPS的快照,没什么可看的,学习而言,展示的要好一些,因为他需要分析数据,然后展示到UI上,那么接下来就直接看下Matix中的com.tencent.matrix.trace.view.FrameDecorator#doFrameAsync,源码过长,这里就一步一步分析代码是如何体现上面的表和描述
计算出平均每一帧的准备时间有 1000ms/60 = 16.6667ms,如果一帧的准备时间超出这个值,则认为发生掉帧,超出的时间越长,掉帧程度越严重。假设每帧准备时间约 32ms,每次只掉一帧,那么 1 秒内实际只刷新 30 帧,即平均帧率只有 30FPS,但这时往往不会觉得是卡顿。反而如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到
Best Normal Middle High Frozen [0:3) [3:9) [9:24) [24:42) [42:∞)
/**
* 流畅指标,佳0,正常1,中等2,严重3,冻帧4
*/
public enum DropStatus {
DROPPED_FROZEN(4), DROPPED_HIGH(3), DROPPED_MIDDLE(2), DROPPED_NORMAL(1), DROPPED_BEST(0);
public int index;
DropStatus(int index) {
this.index = index;
}
}
在回调回来的函数中,分析流畅指标
卡顿分布数据.png@Override
public void doFrameAsync(String focusedActivity, long startNs, long endNs, int dropFrame, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
super.doFrameAsync(focusedActivity, startNs, endNs, dropFrame, isVsyncFrame, intendedFrameTimeNs, inputCostNs, animationCostNs, traversalCostNs);
......
if (dropFrame >= Constants.DEFAULT_DROPPED_FROZEN) { //冻帧
dropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;
sumDropLevel[FrameTracer.DropStatus.DROPPED_FROZEN.index]++;
} else if (dropFrame >= Constants.DEFAULT_DROPPED_HIGH) { //严重
dropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;
sumDropLevel[FrameTracer.DropStatus.DROPPED_HIGH.index]++;
} else if (dropFrame >= Constants.DEFAULT_DROPPED_MIDDLE) { //中等
dropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;
sumDropLevel[FrameTracer.DropStatus.DROPPED_MIDDLE.index]++;
} else if (dropFrame >= Constants.DEFAULT_DROPPED_NORMAL) { //正常
dropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;
sumDropLevel[FrameTracer.DropStatus.DROPPED_NORMAL.index]++;
} else {
dropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;
sumDropLevel[FrameTracer.DropStatus.DROPPED_BEST.index]++;
}
......
}
这里的代码非常简单,接下来是分析造成严重卡顿的情况,也就是严重丢帧的时候,也是文章中所分析的内容:
如果出现某次严重掉帧(>300ms),那么这一次的变化,通常很容易感知到
sumFrameCost += (dropFrame + 1) * frameIntervalMs;
sumFrames += 1;
float duration = sumFrameCost - lastCost[0];
long collectFrame = sumFrames - lastFrames[0];
if (duration >= 200) {
//更新视图
}
综上,就是Matrix中对页面流畅性分析的核心代码,而对于Matrix中精准命中堆栈则可以取自冻帧或者所谓的duration >= 200这个条件下dump一次主线程的堆栈来获取
慢函数
其实关于上述严重掉帧情况下的抓取堆栈的数量不多,同样避不开上面提到的漏掉其他耗时代码的情况,不过笔者认为这样的情况不会特别多,因为卡顿发生的时候,大概率避免不了正在执行一个耗时操作,那么这个耗时操作的堆栈出现在此刻dump出来的堆栈里面的可能性很大,所以Matrix干脆利落的出了一个慢函数检测,这样感觉就无孔不入了,对所有在主线程上运行的函数耗时进行收集,和BlockCanary检测卡顿的策略一样,但是堆栈的抓取就要复杂一些,这也是为什么Matrix性能更好的原因,能在灰度下上线的能力。同时也能有对应的聚合策略,这样结合后端的能力方便我们分析代码运行情况,然后做 “狭义” 上的优化
,这里推荐一个在解决卡顿时候您可以用到的方法耗时小插件
Matrix中的慢函数和BlockCanary的卡顿堆栈获取时机和检测卡顿或者慢
的策略一致,下面可以简单看下,关于Matrix中如何去捞堆栈的~
@Override
public void dispatchEnd(long beginNs, long cpuBeginMs, long endNs, long cpuEndMs, long token, boolean isVsyncFrame) {
super.dispatchEnd(beginNs, cpuBeginMs, endNs, cpuEndMs, token, isVsyncFrame);
long start = config.isDevEnv() ? System.currentTimeMillis() : 0;
long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;
try {
//查过阈值,和BlockCanary一样com.tencent.matrix.trace.constants.Constants.DEFAULT_EVIL_METHOD_THRESHOLD_MS = 700
if (dispatchCost >= evilThresholdMs) {
long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
long[] queueCosts = new long[3];
System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3);
//scene拿的是当前的activity
String scene = AppMethodBeat.getVisibleScene();
MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO));
}
} finally {
indexRecord.release();
}
}
可以看到,慢函数的阈值是700ms,如果超出阈值,则会从AppMethod中拿取相关的堆栈数据,同时记录下当前的页面然后上传一波,那么关键的地方就在于这个
AppMethodBeat.getInstance().copyData(indexRecord)
关于Matrix的堆栈实现主要分为两块
- ASM插桩记录方法
- Java侧关于记录方法耗时以及方法记录的实现
插桩流程.png编译期:
通过代理编译期间的任务 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,作为数据上报后的解析支持。
下面重点介绍一下关于Matrix中对性能考量以及具体的业务插桩代码
- 编译优化,内联规则
选择在该编译任务执行时插桩,是因为 proguard 操作是在该任务之前就完成的,意味着插桩时的 class 文件已经被混淆过的。而选择 proguard 之后去插桩,是因为如果提前插桩会造成部分方法不符合内联规则,没法在 proguard 时进行优化,最终导致程序方法数无法减少,从而引发方法数过大问题。
解释一下什么叫做方法内联,说白了其实就是你写了两个方法,在编译时会将其中一个方法的实现直接放到调用的地方,这样就无需去走一遍调用其他方法,从而达到优化的目的,因为每个方法会生成一个栈帧,然后进行压栈出栈的操作,过程比较反复,这里用代码解释一下编译优化之一,方法内联:
//编译前,source.java
public int doubleNum(int num){
return num * 2;
}
public void method1(){
int a = 10;
int b = doubleNum(2);
System.out.println(String.format("a:%s, b:%s", a, b));
}
//编译后, source.class
public void method1(){
int a = 10;
int b = a * 2;//doubleNum(2);
System.out.println(String.format("a:%s, b:%s", a, b));
}
可以看到在编译之后就没有了doubleNum这个方法,在method1中调用的时候直接变成了 a * 2 , 这样就无需在运行的过程中去对doubleNum压栈操作
虽然能保证proguard时候优化能正常进行,不过优化程度上来讲,笔者也没有统计过哈,也不晓得从哪里能够看到,因为目前在7.0之后,安装的时候会进行JIT和AOT混合的方式,这个结果应该不是很好看,在JIT选择编译热代码的时候,优化的那部分内联又该如何考虑呢?想想太复杂了。。。大家感兴趣的话可以统计下在打出来的release包中,可以看日志,优化的效果~
那接下里就是Matrix中如何做到在proguard的transform后进行插桩呢,核心工程是matrix-gradle-plugin
入口:
//com.tencent.matrix.plugin.MatrixPlugin#apply
/**
* <p>Adds a closure to be called immediately after this project has been evaluated. The project is passed to the
* closure as a parameter. Such a listener gets notified when the build file belonging to this project has been
* executed. A parent project may for example add such a listener to its child project. Such a listener can further
* configure those child projects based on the state of the child projects after their build files have been
* run.</p>
*
* @param closure The closure to call.
*/
void afterEvaluate(Closure closure);
@Override
void apply(Project project) {
......
//完成所有transform之后执行
project.afterEvaluate {
def android = project.extensions.android
def configuration = project.matrix
android.applicationVariants.all { variant ->
if (configuration.trace.enable) {
//代码插桩入口
MatrixTraceTransform.inject(project, configuration.trace, variant.getVariantData().getScope())
}
......
}
}
}
在afterEvaluate后传入闭包,开始插桩代码,为什么是这个时机,可以参考上面的源码注释哈~
接下来就是注入代码,因为不依赖于自定义的transformTask,Matrix的实现是通过往transformTask里面去注入执行事件,这里的写法也是个新姿势~
//com.tencent.matrix.trace.transform.MatrixTraceTransform#inject
public static void inject(Project project, MatrixTraceExtension extension, VariantScope variantScope) {
......
try {
String[] hardTask = getTransformTaskName(extension.getCustomDexTransformName(), variant.getName());
for (Task task : project.getTasks()) {
for (String str : hardTask) {
if (task.getName().equalsIgnoreCase(str) && task instanceof TransformTask) {
//如果确实是Transform的task后进来执行反射hook注入matrix的transform任务
TransformTask transformTask = (TransformTask) task;
Log.i(TAG, "successfully inject task:" + transformTask.getName());
Field field = TransformTask.class.getDeclaredField("transform");
field.setAccessible(true);
//这里就是注入自定义的任务,在编译执行的时候
field.set(task, new MatrixTraceTransform(config, transformTask.getTransform()));
break;
}
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
}
}
//源码中可以看到执行transformTask的时候调用这个注入的transform执行处,com.android.build.gradle.internal.pipeline.TransformTask#transform
@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs)
throws IOException, TransformException, InterruptedException {
......
ThreadRecorder.get().record(ExecutionType.TASK_TRANSFORM,
new Recorder.Block<Void>() {
@Override
public Void call() throws Exception {
//这里就是调用transform.transform的时机
transform.transform(new TransformInvocationBuilder(TransformTask.this)
.addInputs(consumedInputs.getValue())
.addReferencedInputs(referencedInputs.getValue())
.addSecondaryInputs(changedSecondaryInputs.getValue())
.addOutputProvider(outputStream != null
? outputStream.asOutput()
: null)
.setIncrementalMode(isIncremental.getValue())
.build());
return null;
}
},
new Recorder.Property("project", getProject().getName()),
new Recorder.Property("transform", transform.getName()),
new Recorder.Property("incremental", Boolean.toString(transform.isIncremental())));
}
[手动表情秒啊~],简直了。。。 不愧是微信的带佬,如果不是对编译任务的task有一定了解的话,要做到这里感觉不太可能。。。 不过还好能站在巨人的肩膀上~
这里注意下关于MatrixTraceTransform的构造,这里也是一个优秀的细节处理,虽然hook了系统transform的task,但是不会扔弃系统的transformTask,而是会传递进来,接下来在代码中分析这个伪代理的使用~
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
long start = System.currentTimeMillis();
try {
//在执行系统的transform之前,执行自己的transform
doTransform(transformInvocation); // hack
} catch (ExecutionException e) {
e.printStackTrace();
}
long cost = System.currentTimeMillis() - start;
long begin = System.currentTimeMillis();
//ok 接下来,执行系统原来内置的orignTransformTask, 优秀的细节,不过貌似也只能这么干,可以考虑到卡顿hook looer中printer的时候也可以这么干~
origTransform.transform(transformInvocation);
}
最后就是插桩的核心代码了,这里分为两个部分来进行介绍,一个是如何过滤简单方法,一个是插桩细节
- 过滤简单方法
- 插桩
总的来说,流程也比较简单,整个方法分为三个过程
首先解析mapping文件,毕竟是对混淆过的代码插桩,这里要能知道自己到底插的哪个方法
然后是收集要插桩的方法,就是过滤,过滤黑名单中不需要插桩的类或者方法
对收集后的方法开始插桩
精简一下代码:
//com.tencent.matrix.trace.transform.MatrixTraceTransform#doTransform
private void doTransform(TransformInvocation transformInvocation) throws ExecutionException, InterruptedException {
final boolean isIncremental = transformInvocation.isIncremental() && this.isIncremental();
/**
* step 1 把编译后的mapping文件对应的混淆方法关系记录下来,如:a() -> onCreate()
*/
List<Future> futures = new LinkedList<>();
//...干掉乱八七糟的代码干掉哈,主要就是生成方法id,然后解析~
//拿到需要查找的类文件
for (TransformInput input : inputs) {
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
futures.add(executor.submit(new CollectDirectoryInputTask(dirInputOutMap, directoryInput, isIncremental)));
}
for (JarInput inputJar : input.getJarInputs()) {
futures.add(executor.submit(new CollectJarInputTask(inputJar, isIncremental, jarInputOutMap, dirInputOutMap)));
}
}
//这里调用get方法就是执行
for (Future future : futures) {
future.get();
}
futures.clear();
/**
* step 2 这里就是收集下来需要进行插桩的方法,过滤出来黑名单或者是简单方法,这些方法不需要插桩~ 这里这个判断再一次[手动秒啊~]
*/
MethodCollector methodCollector = new MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap);
methodCollector.collect(dirInputOutMap.keySet(), jarInputOutMap.keySet());
/**
* step 3 对收集的方法进行插桩~
*/
MethodTracer methodTracer = new MethodTracer(executor, mappingCollector, config, methodCollector.getCollectedMethodMap(), methodCollector.getCollectedClassExtendMap());
methodTracer.trace(dirInputOutMap, jarInputOutMap);
}
mapping的解析就是工作量问题,我们可以看下输入结果:
1,1,sample.tencent.matrix.listener.TestPluginListener <init> (Landroid.content.Context;)V
2,0,sample.tencent.matrix.trace.TestFpsActivity$4 <init> (Lsample.tencent.matrix.trace.TestFpsActivity;Landroid.content.Context;I[Ljava.lang.Object;)V
3,1,sample.tencent.matrix.trace.TestTraceFragmentActivity <init> ()V
4,1,sample.tencent.matrix.trace.TestTraceFragmentActivity$2 onClick (Landroid.view.View;)V
5,1,sample.tencent.matrix.listener.TestPluginListener onReportIssue (Lcom.tencent.matrix.report.Issue;)V
然后分析函数是否简单的地方,先看一下文档中的定义
为了减少插桩量及性能损耗,通过遍历 class 方法指令集,判断扫描的函数是否只含有 PUT/READ FIELD 等简单的指令,来过滤一些默认或匿名构造函数,以及 get/set 等简单不耗时函数。
简单方法过滤
内部获取方法同样利用ASM那一套逻辑,上面介绍耗时插件已经介绍过了哈~
可以重点关注核心代码:
//com.tencent.matrix.trace.MethodCollector.TraceClassAdapter#visitMethod
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
//抽象类不做处理
if (isABSClass) {
return super.visitMethod(access, name, desc, signature, exceptions);
} else {
//判断该方法是不是onWindowFocus
if (!hasWindowFocusMethod) {
hasWindowFocusMethod = isWindowFocusChangeMethod(name, desc);
}
//真正核心处理过滤逻辑
return new CollectMethodNode(className, access, name, desc, signature, exceptions);
}
}
//com.tencent.matrix.trace.MethodCollector.CollectMethodNode#visitEnd
@Override
public void visitEnd() {
super.visitEnd();
TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);
if ("<init>".equals(name)) {
isConstructor = true;
}
//判断是否是黑名单里面的,这里和黑名单中进行了互斥,在插桩中也会判断
//所以可以不用在意这个细节,说白了就是判断这个方法不在黑名单中而已
boolean isNeedTrace = isNeedTrace(configuration, traceMethod.className, mappingCollector);
// filter simple methods 这里就是过滤简单方法,我们重点关注这里
if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
&& isNeedTrace) {
ignoreCount.incrementAndGet();
collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
return;
}
......
}
上面截取出来的额低吗可见端倪,一个是判断构造方法,然后就是isEmptyMethod
,isGetSetMethod
,isSingleMethod
,接下来就分析下这个所谓的过滤简单方法到底🐂🍺与否
//com.tencent.matrix.trace.MethodCollector.CollectMethodNode#isEmptyMethod
/**
* 检测空方法,不知道这里为什么这么写。。。反正我验证这个方法基本没有用
* -1 是F_NEW指令,不应该是判断return指令么?
*
* @return
*/
private boolean isEmptyMethod() {
ListIterator<AbstractInsnNode> iterator = instructions.iterator();
while (iterator.hasNext()) {
//逻辑就是过滤掉是new指令? 说白了就是如果指令集不为空,就不是空方法
AbstractInsnNode insnNode = iterator.next();
int opcode = insnNode.getOpcode();
//-1对应的opcode是NEW这个指令
if (-1 == opcode) {
continue;
} else {
return false;
}
}
return true;
}
/*
这里是空方法反编译后的字节码
public void logClueMethod();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 22: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/done/testlibrary/Utils;
*/
首先空方法的判断不够牛批哈~
接下来看第二个isGetSetMethod
,
private boolean isGetSetMethod() {
int ignoreCount = 0;
ListIterator<AbstractInsnNode> iterator = instructions.iterator();
while (iterator.hasNext()) {
AbstractInsnNode insnNode = iterator.next();
int opcode = insnNode.getOpcode();
if (-1 == opcode) {
continue;
}
if (opcode != Opcodes.GETFIELD
&& opcode != Opcodes.GETSTATIC
&& opcode != Opcodes.H_GETFIELD
&& opcode != Opcodes.H_GETSTATIC
&& opcode != Opcodes.RETURN
&& opcode != Opcodes.ARETURN
&& opcode != Opcodes.DRETURN
&& opcode != Opcodes.FRETURN
&& opcode != Opcodes.LRETURN
&& opcode != Opcodes.IRETURN
&& opcode != Opcodes.PUTFIELD
&& opcode != Opcodes.PUTSTATIC
&& opcode != Opcodes.H_PUTFIELD
&& opcode != Opcodes.H_PUTSTATIC
&& opcode > Opcodes.SALOAD) {
if (isConstructor && opcode == Opcodes.INVOKESPECIAL) {
ignoreCount++;
if (ignoreCount > 1) {
return false;
}
continue;
}
return false;
}
}
return true;
}
最后是判断是不是简单方法
private boolean isSingleMethod() {
ListIterator<AbstractInsnNode> iterator = instructions.iterator();
while (iterator.hasNext()) {
AbstractInsnNode insnNode = iterator.next();
int opcode = insnNode.getOpcode();
if (-1 == opcode) {
continue;
//出现这个指令区间内,都标识调用了其他方法,会出现压栈的情况
// 调用了别的方法,自然就不是简单的方法,不过这里没有判断指令的数量,感觉也不是一定可靠
} else if (Opcodes.INVOKEVIRTUAL <= opcode && opcode <= Opcodes.INVOKEDYNAMIC) {
return false;
}
}
return true;
}
三个过滤的函数都看完了,感觉不一定可取,可以借鉴和参考,但不一定准,可能是在下没有严谨的编译看吧。。。不过有这种思路也还好,可以自己定义所谓的简单方法吧~
插桩代码
这里只简单看一下插桩的代码,具体计算耗时的功能逻辑实现后面会继续介绍,那部分也不在plugin的工程中~
3、针对界面启动耗时,因为要统计从 Activity#onCreate 到 Activity#onWindowFocusChange 间的耗时,所以在插桩过程中需要收集应用内所有 Activity 的实现类,并覆盖 onWindowFocusChange 函数进行打点。
4、为了方便及高效记录函数执行过程,我们为每个插桩的函数分配一个独立 ID,在插桩过程中,记录插桩的函数签名及分配的 ID,在插桩完成后输出一份 mapping,作为数据上报后的解析支持。
插桩对外的类是com.tencent.matrix.trace.MethodTracer,这里就简单看一个插桩source代码的,因为到最后不管是jar还是source,可都是对class文件进行处理~
//com.tencent.matrix.trace.MethodTracer#innerTraceMethodFromSrc
private void innerTraceMethodFromSrc(File input, File output) {
ArrayList<File> classFileList = new ArrayList<>();
if (input.isDirectory()) {
listClassFiles(classFileList, input);
} else {
classFileList.add(input);
}
for (File classFile : classFileList) {
InputStream is = null;
FileOutputStream os = null;
try {
......
if (MethodCollector.isNeedTraceFile(classFile.getName())) {
is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//关注这里插桩访问者访问类,然后里面插桩
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
//调用这里后开始插桩
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
......
} else {
FileUtil.copyFileUsingStream(classFile, changedFileOutput);
}
} catch (Exception e) {
......
} finally {
.......
}
}
}
代码里面关键的实现还是asm的classVisitor,我们只需要关注这里面的实现即可~
这里由于封装的路径还是有个两三层,我就简单说下调用关系,这里代码咱们还是看核心实现哈~
//com.tencent.matrix.trace.MethodTracer.TraceMethodAdapter
//1. 调用com.tencent.matrix.trace.MethodTracer.TraceClassAdapter#visitMethod
//2. 调用com.tencent.matrix.trace.MethodTracer.TraceMethodAdapter#TraceMethodAdapter
//3. 利用AdviceAdapter的方法进入和退出回调插桩
public final static String MATRIX_TRACE_CLASS = "com/tencent/matrix/trace/core/AppMethodBeat";
@Override
protected void onMethodEnter() {
//这里的插桩结果就是在方法进入的时候插入 AppMethodBeat.i(methodid);
TraceMethod traceMethod = collectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
}
}
@Override
protected void onMethodExit(int opcode) {
//方法退出的插桩有判断逻辑
TraceMethod traceMethod = collectedMethodMap.get(methodName);
if (traceMethod != null) {
if (hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) {
TraceMethod windowFocusChangeMethod = TraceMethod.create(-1, Opcodes.ACC_PUBLIC, className,
TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD, TraceBuildConstants.MATRIX_TRACE_ON_WINDOW_FOCUS_METHOD_ARGS);
if (windowFocusChangeMethod.equals(traceMethod)) {
//如果是onWindowFocusChanged,那么还会插入AppMethodBeat.at(activity, isFocus)
traceWindowFocusChangeMethod(mv, className);
}
}
//无论是不是 onWindowFocusChanged,都会插入AppMethodBeat.o(methodid);
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
}
插装部分就是纯工作量的事情了,有了前面的判断和过滤逻辑,这里就非常简单,主要插入 i 方法和 o 方法就行具体可以看上面的代码注释 多余的这里就不展开去看代码了
JavaLib AppMethodBeat 实现
上面就是trace插件在编译时所做的工作,可以看到插桩时期丝毫没有进行任何系统方法的调用,如:SystemClock.time或者System.nanoTime这些获取时间戳的native方法,这样可以理解为一个小优化的点~不通过系统方法获取时间,matrix利用很巧妙的方式来获取时间
考虑到每个方法执行前后都获取系统时间(System.nanoTime)会对性能影响比较大,而实际上,单个函数执行耗时小于 5ms 的情况,对卡顿来说不是主要原因,可以忽略不计,如果是多次调用的情况,则在它的父级方法中可以反映出来,所以为了减少对性能的影响,通过另一条更新时间的线程每 5ms 去更新一个时间变量,而每个方法执行前后只读取该变量来减少性能损耗。
具体就在java lib中去实现,下面我们就分析下java中的实现,其实上面插桩的时候就已经知道具体的实现的核心类是哪个了 => com.tencent.matrix.trace.core.AppMethodBeat
这个类的逻辑还是有一小丢丢绕,因为其中不仅仅包含了计算方法耗时,还兼顾了查看生命周期相关的,包括activity、service的生命周期,目测了下,contentProvider的还没完成,感兴趣的同学可以具体查看下内部关于hook mH相关的代码,这里就贴一下关键性的代码哈~
//com.tencent.matrix.trace.hacker.ActivityThreadHacker#hackSysHandlerCallback
public static void hackSysHandlerCallback() {
try {
Class<?> forName = Class.forName("android.app.ActivityThread");
Field field = forName.getDeclaredField("sCurrentActivityThread");
field.setAccessible(true);
Object activityThreadValue = field.get(forName);
//hook mH这个handler
Field mH = forName.getDeclaredField("mH");
mH.setAccessible(true);
Object handler = mH.get(activityThreadValue);
Class<?> handlerClass = handler.getClass().getSuperclass();
if (null != handlerClass) {
//接着hook系统的callback,方便内部调用从而不影响系统调动过程
Field callbackField = handlerClass.getDeclaredField("mCallback");
callbackField.setAccessible(true);
Handler.Callback originalCallback = (Handler.Callback) callbackField.get(handler);
HackCallback callback = new HackCallback(originalCallback);
callbackField.set(handler, callback);
}
} catch (Exception e) {
}
}
进入正题,我们先从插桩中的方法入口和方法出口来分析
编译期已经对全局的函数进行插桩,在运行期间每个函数的执行前后都会调用 MethodBeat.i/o 的方法,如果是在主线程中执行,则在函数的执行前后获取当前距离 MethodBeat 模块初始化的时间 offset(为了压缩数据,存进一个long类型变量中),并将当前执行的是 MethodBeat i或者o、mehtod id 及时间 offset,存放到一个 long 类型变量中,记录到一个预先初始化好的数组 long[] 中 index 的位置(预先分配记录数据的 buffer 长度为 100w,内存占用约 7.6M)。数据存储如下图:
![方法数据逻辑.png](https://img.haomeiwen.com/i2822814/fecb3594fa74d08c.png?imageMogr2/aut
o-orient/strip%7CimageView2/2/w/820)
在搞清楚内部的逻辑之前,笔者认为直接贴代码不是很好理解,所以我们先来配个图,然后结合图来理解这个过程:
private static final int STATUS_DEFAULT = Integer.MAX_VALUE;
private static final int STATUS_STARTED = 2;
private static final int STATUS_READY = 1;
private static final int STATUS_STOPPED = -1;
private static final int STATUS_EXPIRED_START = -2;
private static final int STATUS_OUT_RELEASE = -3;
public static void i(int methodId) {
......
//正式开始 step 1
if (status == STATUS_DEFAULT) {
synchronized (statusLock) {
if (status == STATUS_DEFAULT) {
realExecute();
status = STATUS_READY;
}
}
}
long threadId = Thread.currentThread().getId();
if (threadId == sMainThreadId) {
//合并方法堆栈 step 2
if (sIndex < Constants.BUFFER_SIZE) {
mergeData(methodId, sIndex, true);
} else {
sIndex = 0;
mergeData(methodId, sIndex, true);
}
++sIndex;
}
}
private static void realExecute() {
//记录开始执行的时间戳
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
sHandler.removeCallbacksAndMessages(null);
//启动计时器
sHandler.postDelayed(sUpdateDiffTimeRunnable, Constants.TIME_UPDATE_CYCLE_MS);
//状态维护,延迟15s后执行,由上可知有i首次进来以后状态是STATUS_READY
//这里得结合looper的监听来说,后面分析捕捉细节再详说,这里记得这个状态维护
sHandler.postDelayed(checkStartExpiredRunnable = new Runnable() {
@Override
public void run() {
synchronized (statusLock) {
MatrixLog.i(TAG, "[startExpired] timestamp:%s status:%s", System.currentTimeMillis(), status);
if (status == STATUS_DEFAULT || status == STATUS_READY) {
status = STATUS_EXPIRED_START;
}
}
}
}, Constants.DEFAULT_RELEASE_BUFFER_DELAY);
//注册监听
LooperMonitor.register(looperMonitorListener);
}
然后可以看到时间更新的方法为,主要工作其实就是更新时间戳,每次更新后睡5s,然后挂起自己,等待被主线程分发消息时候再唤醒:
/**
* 计时器
* update time runnable
*/
private static Runnable sUpdateDiffTimeRunnable = new Runnable() {
@Override
public void run() {
try {
while (true) {
while (!isPauseUpdateTime && status > STATUS_STOPPED) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
SystemClock.sleep(Constants.TIME_UPDATE_CYCLE_MS);
}
synchronized (updateTimeLock) {
updateTimeLock.wait();
}
}
} catch (Exception e) {
MatrixLog.e(TAG, "" + e.toString());
}
}
};
private static void dispatchBegin() {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
isPauseUpdateTime = false;
synchronized (updateTimeLock) {
updateTimeLock.notify();
}
}
最后在合并数据的时候把方法进入退出和方法id以及时间戳带上,组成一个long变量:
/**
* merge trace info as a long data
*
* @param methodId
* @param index
* @param isIn
*/
private static void mergeData(int methodId, int index, boolean isIn) {
//如果是分发的函数过来的,更新一下时刻,以示尊重~ 就是更新时戳,这个时间可不能是简单的5的倍数[手动抠鼻]
if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
}
//可见输入为1输出为0
long trueId = 0L;
if (isIn) {
trueId |= 1L << 63;
}
trueId |= (long) methodId << 43;
//将方法id和时间戳合并成一个long值,以达到8字节存储的目的
trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
sBuffer[index] = trueId;
checkPileup(index);
sLastIndex = index;
}
最后就把数据合并完成了。matrix的慢函数监控至此结束~
网友评论