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的的数据,这样来维持链表长度不至于过长

相关推荐
CYRUS STUDIO29 分钟前
ARM64汇编寻址、汇编指令、指令编码方式
android·汇编·arm开发·arm·arm64
weixin_449310841 小时前
高效集成:聚水潭采购数据同步到MySQL
android·数据库·mysql
Zender Han1 小时前
Flutter自定义矩形进度条实现详解
android·flutter·ios
白乐天_n3 小时前
adb:Android调试桥
android·adb
姑苏风7 小时前
《Kotlin实战》-附录
android·开发语言·kotlin
数据猎手小k11 小时前
AndroidLab:一个系统化的Android代理框架,包含操作环境和可复现的基准测试,支持大型语言模型和多模态模型。
android·人工智能·机器学习·语言模型
你的小1011 小时前
JavaWeb项目-----博客系统
android
风和先行12 小时前
adb 命令查看设备存储占用情况
android·adb
AaVictory.13 小时前
Android 开发 Java中 list实现 按照时间格式 yyyy-MM-dd HH:mm 顺序
android·java·list
似霰14 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder