在写前几篇文章的过程中发现,很多的Tracker 中都有用到 AppMethodBeat ,但是由于篇幅问题又不能将 AppMethodBeat ,由于他的逻辑特别的绕,所以还是在这里详细说一下他的作用
先来说一下 AppMethodBeat 是用来干什么的,以及他的工作原来,我们再来分析代码速度会快很多
问题1 AppMethodBeat 是如何保存和查找任务线的
arduino
public static final int BUFFER_SIZE = 100 * 10000; // 7.6M
private static long[] sBuffer = new long[Constants.BUFFER_SIZE];
在 AppMethodBeat 存在一个这样的数组,在我们使用 ASM 向方法内插入插入开始方法与结束方法的时候,AppMethodBeat 就会在 这个 sBuffer 中存一个数据,并且他的index 会增大1 ,如果这个 sBuffer 没有被放入过数据,那么 他的内容就像下面这样
当我们一直向 sBuffer 插入数据,总会将 sBuffer 的所有位置都放入相对应的数据,那么此时我们就需要从 0开始重新放入数据,原因就是我们在收集这些方法的耗时的区间是通过 MainThread 的 Looper 每开始处理一个 Message ,就会向 sBuffer 中写入一个特定的方法,当我们发现某些主线程方法有耗时操作的时候,只需要找到当前已经已经写完的节点,一直向前找到Looper 开始处理当前消息的开始节点,就是当前所有的方法的路径,大致如下图
那么为什么说每次向上查找都是找这个 onDispatchBegin 呢, 在 EvilMethodTracer 的 onDispatchBegin 会调用如下方法
ini
indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");
在结束的时候会调用收集的方法,将这个消息内所有的方法都获取一遍
ini
@Override
public void onDispatchEnd(String log, long beginNs, long endNs) {
long dispatchCost = (endNs - beginNs) / Constants.TIME_MILLIS_TO_NANO;
try {
if (dispatchCost >= evilThresholdMs) {
long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
String scene = AppActiveMatrixDelegate.INSTANCE.getVisibleScene();
MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, dispatchCost, endNs));
}
} finally {
indexRecord.release();
}
}
说完了 sBuffer 整体的运行逻辑,我们再来接着说说 sBuffer 中每一个节点的数据是如何构成的
问题2: sBuffer 中每一个节点都是一个long 类型,在java 中long 占8个字节,也就是 64位长度 ,我们结合代码与图片来一起分析一下这 64位 Matrix 是如何应用的
ini
long trueId = 0L;
if (isIn) {
trueId |= 1L << 63;
}
trueId |= (long) methodId << 43;
trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
这一段代码是 sBuffer 中每一个节点的初始化,为了更有效的利用到这64位控件, 他使用如下图的存储方法
至于为什么最后方法的执行时间需要与 0x7FFFFFFFFFFL 取 & ,我们使用 程序员计算器来看一下
0x7FFFFFFFFFFL 他的后43位都是1 , 在与 time 取 & 时,如果time 大于 0x7FFFFFFFFFFL,大于的部分可以被忽略
问题3: AppMethodBeat 的时间线线是如何来维护的
在 Matrix 中,他为我们的每一个开始和结束方法都插入了一个开始方法 i 与结束方法 o,如果每次这两个方法执行的过程中,我们都去获取当前时间,对于整个应用进程来说消耗还是非常大的,那么如何做到不频分的获取又需要保证时间的准确定呢,
java
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());
}
}
};
在 AppMethodBeat 中,会创建一个 HandlerThread ,使用它的Looper 来实现一个每隔 5ms 的循环来更新时间,同样的为了保证时间的准确定,在我们每次调用 dispatchBegin 的时候同样会更新一下时间,
ini
if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
}
那么我为什么一直说 methodId == AppMethodBeat.METHOD_ID_DISPATCH 就是 dispatchBegin 呢,证据如下
scss
private void dispatchBegin() {
token = dispatchTimeMs[0] = System.nanoTime();
dispatchTimeMs[2] = SystemClock.currentThreadTimeMillis();
if (config.isAppMethodBeatEnable()) {
AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH);
}
synchronized (observers) {
for (LooperObserver observer : observers) {
if (!observer.isDispatchBegin()) {
observer.dispatchBegin(dispatchTimeMs[0], dispatchTimeMs[2], token);
}
}
}
if (config.isDevEnv()) {
MatrixLog.d(TAG, "[dispatchBegin#run] inner cost:%sns", System.nanoTime() - token);
}
}
在 UIThreadMonitor 的 dispatchBegin方法时会调用 AppMethodBeat.i(AppMethodBeat.METHOD_ID_DISPATCH)
好了到了这里我们就可以开始愉快的分析代码了
在Matrix 中 所有的Track 都是从 onStart 开始的,我们也先从 onStart 开始分析
java
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;
@Override
public void onStart() {
synchronized (statusLock) {
if (status < STATUS_STARTED && status >= STATUS_EXPIRED_START) {
sHandler.removeCallbacks(checkStartExpiredRunnable);
MatrixHandlerThread.getDefaultHandler().removeCallbacks(realReleaseRunnable);
if (sBuffer == null) {
throw new RuntimeException(TAG + " sBuffer == null");
}
MatrixLog.i(TAG, "[onStart] preStatus:%s", status, Utils.getStack());
status = STATUS_STARTED;
} else {
MatrixLog.w(TAG, "[onStart] current status:%s", status);
}
}
}
从上面代码中可以看到, 如果当前状态是 STATUS_READY 或者 STATUS_STOPPED 在或者是 STATUS_EXPIRED_START 的状态下才会走 onStart 中逻辑,但是由于 这个start 的默认值是 STATUS_DEFAULT,所以并不会走他的逻辑,那么接下来该如何分析呢
在ASM 字节码插装的过程中,Matrix 会向我们方法和结束的节点插入i/o方法,我们看一下i 方法
ini
public static void i(int methodId) {
if (status <= STATUS_STOPPED) {
return;
}
if (methodId >= METHOD_ID_MAX) {
return;
}
if (status == STATUS_DEFAULT) {
synchronized (statusLock) {
if (status == STATUS_DEFAULT) {
realExecute();
status = STATUS_READY;
}
}
}
long threadId = Thread.currentThread().getId();
if (sMethodEnterListener != null) {
sMethodEnterListener.enter(methodId, threadId);
}
if (threadId == sMainThreadId) {
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 = 0;
mergeData(methodId, sIndex, true);
}
++sIndex;
assertIn = false;
}
}
在 i 方法中,如果状态是 STATUS_DEFAULT,就会走到 realExecute ,同时将状态置位 STATUS_READY,我们先来看看 realExecute 这个方法
scss
private static void realExecute() {
MatrixLog.i(TAG, "[realExecute] timestamp:%s", System.currentTimeMillis());
// 更新时间
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
//移除所有定时,包括自动销毁定时已经自动更新时间定时
sHandler.removeCallbacksAndMessages(null);
// 启动自动同步时间定时
sHandler.postDelayed(sUpdateDiffTimeRunnable, Constants.TIME_UPDATE_CYCLE_MS);
// 启动一个 10秒的延时,将状态修改为 STATUS_EXPIRED_START
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);
// 修复 25>= SDK_INT>=21 使用sp来保存系统状态导致的anr
ActivityThreadHacker.hackSysHandlerCallback();
// 开始接受 dispatchBegin 与 dispatchEnd 方法
LooperMonitor.register(looperMonitorListener);
}
在调用完 realExecute 之后,由于状态符合 onStart 的执行逻辑了,那么就会执行 onStart 方法,来移除定时销毁的任务,同时将状态修改为 STATUS_STARTED ,已经运行起来了,
关于 i 方法是如何保存这个方法的,已经这个方法在 sBuffer 中插入的位置是如何来确定的,我们在前面问题1 与问题2中已经说过了,这里就贴一下主要逻辑,
ini
if (threadId == sMainThreadId) {
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 = 0;
mergeData(methodId, sIndex, true);
}
++sIndex;
assertIn = false;
}
在方法 i 中,如果是主线程执行的方法 ,就会将数据插入 sBuffer 中,如果index 小于数组最大长度,就继续向后添加,如果不是则从第0个重新开始添加,我们再来看看 mergeData 的过程
ini
private static void mergeData(int methodId, int index, boolean isIn) {
if (methodId == AppMethodBeat.METHOD_ID_DISPATCH) {
sCurrentDiffTime = SystemClock.uptimeMillis() - sDiffTime;
}
try {
long trueId = 0L;
if (isIn) {
trueId |= 1L << 63;
}
trueId |= (long) methodId << 43;
trueId |= sCurrentDiffTime & 0x7FFFFFFFFFFL;
sBuffer[index] = trueId;
checkPileup(index);
sLastIndex = index;
} catch (Throwable t) {
MatrixLog.e(TAG, t.getMessage());
}
}
这个里面比较难理解的就是地方我们在前面问题2中已经做了相应的说明了,再回过头来看就比较简单了,关于 checkPileup 这个方法我有一个地方没有想通,我们先来看 maskIndex 与 copyData 这对组合是如何使用的, 原因是 checkPileup 方法是修正链表的数据,但是这个连边怎么来的我们需要知道
ini
public IndexRecord maskIndex(String source) {
if (sIndexRecordHead == null) {
sIndexRecordHead = new IndexRecord(sIndex - 1);
sIndexRecordHead.source = source;
return sIndexRecordHead;
} else {
IndexRecord indexRecord = new IndexRecord(sIndex - 1);
indexRecord.source = source;
IndexRecord record = sIndexRecordHead;
IndexRecord last = null;
while (record != null) {
if (indexRecord.index <= record.index) {
if (null == last) {
IndexRecord tmp = sIndexRecordHead;
sIndexRecordHead = indexRecord;
indexRecord.next = tmp;
} else {
IndexRecord tmp = last.next;
last.next = indexRecord;
indexRecord.next = tmp;
}
return indexRecord;
}
last = record;
record = record.next;
}
last.next = indexRecord;
return indexRecord;
}
}
先来说一下这个方法的思想是什么意思,那就是将每个需要标记的方法 也就是 dispatchBegin 方法,用他的index 组成一个链表,这个链表中index小的数据在前面,index大的数据在后面,这样当发现任意一个耗时方法,我们就可以非常快速的找到这个方法的前后方法了,
接下来再来看 copyData 这个方法
ini
private long[] copyData(IndexRecord startRecord, IndexRecord endRecord) {
long current = System.currentTimeMillis();
long[] data = new long[0];
try {
if (startRecord.isValid && endRecord.isValid) {
int length;
int start = Math.max(0, startRecord.index);
int end = Math.max(0, endRecord.index);
if (end > start) {
length = end - start + 1;
data = new long[length];
System.arraycopy(sBuffer, start, data, 0, length);
} else if (end < start) {
length = 1 + end + (sBuffer.length - start);
data = new long[length];
System.arraycopy(sBuffer, start, data, 0, sBuffer.length - start);
System.arraycopy(sBuffer, 0, data, sBuffer.length - start, end + 1);
}
return data;
}
return data;
} catch (Throwable t) {
MatrixLog.e(TAG, t.toString());
return data;
} finally {
MatrixLog.i(TAG, "[copyData] [%s:%s] length:%s cost:%sms", Math.max(0, startRecord.index), endRecord.index, data.length, System.currentTimeMillis() - current);
}
}
copyData 方法是依赖于 maskIndex 方法的,如果没有 maskIndex标记开始,那么我们如何来确定向前找多少个方法呢,而且如果在获取方法时如果跨越了 Looper 处理的 Msg ,就会给人非常大的误导, copyData 的逻辑相对 maskIndex 来说就比较简单了,就是取他们两个之间的有效数据
我们最后来分析checkPileup(index); 这个方法
ini
private static void checkPileup(int index) {
IndexRecord indexRecord = sIndexRecordHead;
while (indexRecord != null) {
if (indexRecord.index == index || (indexRecord.index == -1 && sLastIndex == Constants.BUFFER_SIZE - 1)) {
indexRecord.isValid = false;
MatrixLog.w(TAG, "[checkPileup] %s", indexRecord.toString());
sIndexRecordHead = indexRecord = indexRecord.next;
} else {
break;
}
}
}
这里的逻辑也是看了好几遍,由于我们是循环重复的给 sBuffer 中添加数据,每次添加完数据后,如果他的添加的数据又从0开始重新添加的话,需要重新整理链表中的数据,但是从代码上分析他每次只处理了头结点,后续节点好像受影响不大,就会导致这个链表过长,感觉这里有问题,如果大家看懂了这里的逻辑可以留言交流一下,
到了这里就分析完了,其实比较难理解的地方有以下几点
1: sBuffer 存储的每个long 类型的含义,以及他的用法,
2:整个sBuffer 是循环存储的
3:每个被标记的 Record,都会被maskIndex方法组成到链表内,
4:至于他是如何清理链表的,希望大家能留言多多沟通,如果不清理这个链表,会不会插入的过于频繁而导致链表过长,这样会增加遍历的时长,感觉应该弄个奇数和偶数的概念,奇数清理标记为偶数的链表index小于当前index的的数据,这样来维持链表长度不至于过长