监控主线程
TraceCanary 模块只能在 API 16 以上的设备运行,内部分为 ANR、帧率、慢方法、启动四个监测模块,核心接口是 LooperObserver。
LooperObserver 是一个抽象类,顾名思义,它是 Looper 的观察者,在 Looper 分发消息、刷新 UI 时回调,这几个回调方法也是 ANR、慢方法等模块的判断依据:
public abstract class LooperObserver {
// 分发消息前
@CallSuper
public void dispatchBegin(long beginMs, long cpuBeginMs, long token) {
}
// UI 刷新
public void doFrame(String focusedActivityName, long start, long end, long frameCostMs,
long inputCostNs, long animationCostNs, long traversalCostNs) {
}
// 分发消息后
@CallSuper
public void dispatchEnd(long beginMs, long cpuBeginMs, long endMs, long cpuEndMs, long token, boolean isBelongFrame) {
}
}
Looper 监控
Looper 的监控是由类 LooperMonitor 实现的,原理很简单,为主线程 Looper 设置一个 Printer 即可,但值得一提的是,LooperMonitor 不会直接设置 Printer,而是先获取旧对象,并创建代理对象,避免影响到其它用户设置的 Printer:
private synchronized void resetPrinter() {
Printer originPrinter = ReflectUtils.get(looper.getClass(), "mLogging", looper);;
looper.setMessageLogging(printer = new LooperPrinter(originPrinter));
}
class LooperPrinter implements Printer {
@Override
public void println(String x) {
if (null != origin) {
origin.println(x); // 保证原对象正常执行
}
dispatch(x.charAt(0) == '>', x); // 分发,通过第一个字符判断是开始分发,还是结束分发
}
}
UI 刷新监控
UI 刷新监控是基于 Choreographer 实现的,TracePlugin 初始化时,UIThreadMoniter 就会通过反射的方式往 Choreographer 添加回调:
public class UIThreadMonitor implements BeatLifecycle, Runnable {
// Choreographer 中一个内部类的方法,用于添加回调
private static final String ADD_CALLBACK = "addCallbackLocked";
// 回调类型,分别为输入事件、动画、View 绘制三种
public static final int CALLBACK_INPUT = 0;
public static final int CALLBACK_ANIMATION = 1;
public static final int CALLBACK_TRAVERSAL = 2;
public void init(TraceConfig config) {
choreographer = Choreographer.getInstance();
// 回调队列
callbackQueues = reflectObject(choreographer, "mCallbackQueues");
// 反射,找到在 Choreographer 上添加回调的方法
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);
}
}
之所以通过反射的方式实现,而不是通过 postCallback,是为了把我们的 callback 放到头部,这样才能计算系统提交的输入事件、动画、View 绘制等事件的耗时。
这样,等 Choreographer 监听到 vsync 信号时,UIThreadMonitor 和系统添加的回调都会被执行(比如在绘制 View 的时候,系统会往 Choreographer 添加一个 traversal callback):
public final class Choreographer {
private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
@Override
public void run() {
doFrame(mTimestampNanos, mFrame);
}
}
void doFrame(long frameTimeNanos, int frame) {
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
...
}
}
因为 UIThreadMonitor 添加的回调在队列头部,可用于记录开始时间,而其它系统方法,比如 View 的 postOnAnimation 添加的回调在后面,因此所有同类型回调执行完毕后,就可以计算对应的事件(输入事件、动画、View 绘制等)的耗时。
ANR 监控
ANR 监控原理:在 Looper 分发消息时,往后台线程插入一个延时(5s 后执行)任务,Looper 消息分发完毕后就删除,如果过了 5s,该任务未被删除,就认为出现了 ANR。
public class AnrTracer extends Tracer {
// onAlive 时初始化,onDead 时退出
private Handler anrHandler;
private volatile AnrHandleTask anrTask;
public void dispatchBegin(long beginMs, long cpuBeginMs, long token) {
// 插入方法结点,如果出现了 ANR,就从该结点开始收集方法执行记录
anrTask = new AnrHandleTask(AppMethodBeat.getInstance().maskIndex("AnrTracer#dispatchBegin"), token);
// 5 秒后执行
// token 和 beginMs 相等,因此后一个减式用于减去回调该方法过程中所消耗的时间
anrHandler.postDelayed(anrTask, Constants.DEFAULT_ANR - (SystemClock.uptimeMillis() - token));
}
@Override
public void dispatchEnd(long beginMs, long cpuBeginMs, long endMs, long cpuEndMs, long token, boolean isBelongFrame) {
if (null != anrTask) {
anrTask.getBeginRecord().release();
anrHandler.removeCallbacks(anrTask);
}
}
}
如果 5s 后该任务未被删除,那么 AnrTracer 就会开始收集进程、线程、内存、堆栈等信息,并上报。
启动监控
应用的启动监控以第一个执行的方法为起点:
public class AppMethodBeat implements BeatLifecycle {
private static volatile int status = STATUS_DEFAULT;
// 该方法会被插入到每一个方法的开头执行
public static void i(int methodId) {
if (status == STATUS_DEFAULT) { // 如果是默认状态,则说明是第一个方法
realExecute();
status = STATUS_READY;
}
}
private static void realExecute() {
// 记录时间戳
ActivityThreadHacker.hackSysHandlerCallback();
// 开始监控主线程 Looper
LooperMonitor.register(looperMonitorListener);
}
}
记录了第一个方法开始执行时的时间戳后,Matrix 还会通过反射的方式,接管 ActivityThread 的 Handler 的 Callback:
public class ActivityThreadHacker {
public static void hackSysHandlerCallback() {
// 记录时间戳,作为应用启用的开始时间
sApplicationCreateBeginTime = SystemClock.uptimeMillis();
// 反射 ActivityThread,接管 Handler
Class<?> forName = Class.forName("android.app.ActivityThread");
...
}
}
这样就能知道第一个 Activity 或 Service 或 Receiver 启动的具体时间了,这个时间戳可以作为 Application 启动的结束时间:
private final static class HackCallback implements Handler.Callback {
private static final int LAUNCH_ACTIVITY = 100;
private static final int CREATE_SERVICE = 114;
private static final int RECEIVER = 113;
private static boolean isCreated = false;
@Override
public boolean handleMessage(Message msg) {
boolean isLaunchActivity = isLaunchActivity(msg);
// 如果是第一个启动的 Activity 或 Service 或 Receiver,则以该时间戳作为 Application 启动的结束时间
if (!isCreated) {
if (isLaunchActivity || msg.what == CREATE_SERVICE || msg.what == RECEIVER) { // todo for provider
ActivityThreadHacker.sApplicationCreateEndTime = SystemClock.uptimeMillis();
ActivityThreadHacker.sApplicationCreateScene = msg.what;
isCreated = true;
}
}
}
}
最后以主 Activity(闪屏页之后的第一个 Activity)的 onWindowFocusChange 方法作为终点,记录时间戳——Activity 的启动耗时可以通过 onWindowFocusChange 方法回调时的时间戳减去其启动时的时间戳。收集到上述信息之后即可统计启动耗时:
firstMethod.i LAUNCH_ACTIVITY onWindowFocusChange LAUNCH_ACTIVITY onWindowFocusChange
^ ^ ^ ^ ^
| | | | |
|---------app---------|---|---firstActivity---|---------...---------|---careActivity---|
|<--applicationCost-->|
|<--------------firstScreenCost-------------->|
|<---------------------------------------coldCost------------------------------------->|
. |<-----warmCost---->|
如果冷启动/暖启动耗时超过某个阈值(可通过 IDynamicConfig 设置,默认分别为 10s、4s),那么就会从 AppMethodBeat 收集启动过程中的方法执行记录并上报,否则只会简单地上报耗时信息。
慢方法监控
慢方法监测的原理是在 Looper 分发消息时,计算分发耗时(endMs - beginMs),如果大于阈值(可通过 IDynamicConfig 设置,默认为 700ms),就收集信息并上报。
ublic class EvilMethodTracer extends Tracer {
@Override
public void dispatchBegin(long beginMs, long cpuBeginMs, long token) {
super.dispatchBegin(beginMs, cpuBeginMs, token);
// 插入方法结点,如果出现了方法执行过慢的问题,就从该结点开始收集方法执行记录
indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");
}
@Override
public void dispatchEnd(long beginMs, long cpuBeginMs, long endMs, long cpuEndMs, long token, boolean isBelongFrame) {
long dispatchCost = endMs - beginMs;
// 耗时大于慢方法阈值
if (dispatchCost >= evilThresholdMs) {
long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(...);
}
}
private class AnalyseTask implements Runnable {
void analyse() {
// 收集进程与 CPU 信息
int[] processStat = Utils.getProcessPriority(Process.myPid());
String usage = Utils.calculateCpuUsage(cpuCost, cost);
// 从插入结点开始收集并整理方法执行记录
TraceDataUtils.structuredDataToStack(data, stack, true, endMs);
TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK,
new TraceDataUtils.IStructuredDataFilter() { ... }
// 上报问题
TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
plugin.onDetectIssue(issue);
}
}
}
帧率监控
帧率监测的原理是监听 Choreographer,在所有回调都执行完毕后计算当前总共花费的时间,从而计算掉帧数及掉帧程度,当同一个 Activity/Fragment 掉帧程度超过阈值时,就上报问题。关键源码如下:
private class FPSCollector extends IDoFrameListener {
@Override
public void doFrameAsync(String visibleScene, long taskCost, long frameCostMs,
int droppedFrames, boolean isContainsFrame) {
// 使用 Map 保存同一 Activity/Fragment 的掉帧信息
FrameCollectItem item = map.get(visibleScene);
if (null == item) {
item = new FrameCollectItem(visibleScene);
map.put(visibleScene, item);
}
// 累计
item.collect(droppedFrames, isContainsFrame);
// 如果掉帧程度超过一定阈值,就上报问题,并重新计算
// 总掉帧时间 sumFrameCost = 掉帧数 * 16.66ms
// 掉帧上报阈值 timeSliceMs 可通过 IDynamicConfig 设置,默认为 10s
if (item.sumFrameCost >= timeSliceMs) { // report
map.remove(visibleScene);
item.report();
}
}
}
但这里存在一个问题,那就是 Matrix 计算 UI 刷新耗时时,每次都会在掉帧数的基础上加 1:
private class FrameCollectItem {
void collect(int droppedFrames, boolean isContainsFrame) {
// 即使掉帧数为 0,这个值也会不断增加
sumFrameCost += (droppedFrames + 1) * frameIntervalCost / Constants.TIME_MILLIS_TO_NANO;
}
}
而且,doFrame 方法不是只在 UI 刷新时回调,而是每次 Looper 分发消息完毕后都会回调,而 Lopper 分发消息的频率可能远远大于帧率,这就导致即使实际上没有出现掉帧的情况,但由于 Looper 不断分发消息的缘故,sumFrameCost 的值也会不断累加,很快就突破了上报的阈值,进而频繁地上报:
private void dispatchEnd() {
...
synchronized (observers) {
for (LooperObserver observer : observers) {
if (observer.isDispatchBegin()) {
observer.doFrame(...);
}
}
}
}
解决方法是在 PluginListener 中手动过滤,或者修改源码。
总结
TraceCanary 分为慢方法、启动、ANR、帧率四个模块,每个模块的功能都是通过监听接口 LooperObserver 实现的,LooperObserver 用于对主线程的 Looper 和 Choreographer 进行监控。
Looper 的监控是通过 Printer 实现的,每次事件分发都会回调 LooperObserver 的 dispatchBegin、dispatchEnd 方法,计算这两个方法的耗时可以检测慢方法和 ANR 等问题。
Choreographer 的监控则是通过添加 input、animation、traversal 等各个类型的回调到 Choreographer 头部实现的,vsync 信号触发后,Choreographer 中各个类型的回调会被执行,两种类型的回调的开始时间的间隔就相当于第一种类型的事件的耗时(即 input.cost = animation.begin - input.begiin),最后一种事件(traversal)执行完毕后,Looper 的 diaptchEnd 方法也会被执行,因此 traversal.cost = Looper.dispatchEnd -traversal.begin。
各个模块的实现原理如下:
-
ANR:在 Looper 开始分发消息时,往后台线程插入一个延时(5s 后执行)任务,Looper 消息分发完毕后就删除,如果过了 5s,该任务未被删除,就认为出现了 ANR,收集信息,报告问题
-
慢方法:在 Looper 分发消息时,计算分发耗时(endMs - beginMs),如果大于阈值(可通过 IDynamicConfig 设置,默认为 700ms),就收集信息并上报
-
启动:以第一个执行的方法为起点记录时间戳,接着记录第一个 Activity 或 Service 或 Receiver 启动时的时间戳,作为 Application 启动的结束时间。最后以主 Activity(闪屏页之后的第一个 Activity)的 onWindowFocusChange 方法作为终点,记录时间戳。Activity 的启动耗时可以通过 onWindowFocusChange 方法回调时的时间戳减去其启动时的时间戳。收集到上述信息之后即可统计启动耗时。
-
掉帧:监听 Choreographer,doFrame 回调时统计 UI 刷新耗时,计算掉帧数及掉帧程度,当同一个 Activity/Fragment 掉帧程度超过阈值时,就上报。但 Matrix 的计算方法存在问题,可能出现频繁上报的情况,需要自行手动过滤。