Matrix源码分析(四)之 AppMethodBeat工作原理

在写前几篇文章的过程中发现,很多的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的的数据,这样来维持链表长度不至于过长

相关推荐
爱笑鱼1 小时前
Binder(八):远端进程死了,BinderProxy 为什么还能收到通知?
android
Android-Flutter1 小时前
Kotlin 冷流与热流详解
android·kotlin
zhangphil2 小时前
Android ContentProvider/ContentResolver读图片,跨进程 IPC慢
android
yueqc12 小时前
Android .so 文件压缩
android·so·包体积
龚礼鹏2 小时前
深入解析 Android Automotive (AAOS) 启动流程与 CarService 核心机制(基于 Android 16 最新源码视角)
android
rosmis3 小时前
agent各指标定义
android·java·开发语言
风样滴男人哟4 小时前
PHP特性之反射类ReflectionClass机制
android·开发语言·php
小孔龙5 小时前
RenderNode 与 DisplayList:Android 硬件加速的绘制记录与复用
android·性能优化
qizayaoshuap5 小时前
鸿蒙 ArkTS 实战:搜索列表 城市实时过滤(示例 94)
android·华为·harmonyos
朝星5 小时前
Framework筑基之Handler消息机制5(应用层)
android