腾讯性能监控框架Matrix源码分析(十)TracePlugin 之函数耗时分析 EvilMethodTracer

1,338 阅读11分钟

Evil字面意思是邪恶的、有害的意思。这个方法很邪恶就是该方法存在耗时多的情况,糟老头子坏的很呐!!因此,matrix的目的就是想去统计每个方法的执行耗时。

通过上一章节的分析,我们知道每个方法执行前后通过插桩AppMethodBeat.i AppMethodBeat.o方法,在运行期计算两个的差值就得到方法的耗时。并且把记录存储到了一个Long数组。

基本原理已经清楚。现在我们回到EvilMethodTracer类的分析

public EvilMethodTracer(TraceConfig config) {
    this.config = config;
    // evil阈值 默认是700ms,外部可以设置
    this.evilThresholdMs = config.getEvilThresholdMs();
    //evil 开关
    this.isEvilMethodTraceEnable = config.isEvilMethodTraceEnable();
}

配置了两个变量值: evil阈值evil开关

TracePluginstart()方法会调用traceonStartTrace方法

@Override
public void start() {

//省略...

if (traceConfig.isEvilMethodTraceEnable()) {
    evilMethodTracer.onStartTrace();
}

if (traceConfig.isStartupEnable()) {
    startupTracer.onStartTrace();
}
//省略...
}

traceonStartTrace方法则会调用onAlive方法。所以,我们直接看onAlive方法:

@Override
public void onAlive() {
    super.onAlive();
    if (isEvilMethodTraceEnable) {
        UIThreadMonitor.getMonitor().addObserver(this);
    }
}

UIThreadMonitor中注册监听,肯定是监听三个回调方法:

  • dispatchBegin 消息分发开始前
  • dispatchEnd 消息分发结束后
  • doFrame 每一帧消息都会回调

每个方法的详细参数分析,以及回调的原理参考前面文章。这里就不在详述了。

接着我看回调方法里面做了什么。

通过AppMethodBeatmaskIndex方法,记录looper消息处理开始时的下标index

@Override
public void dispatchBegin(long beginNs, long cpuBeginMs, long token) {
    super.dispatchBegin(beginNs, cpuBeginMs, token);
    indexRecord = AppMethodBeat.getInstance().maskIndex("EvilMethodTracer#dispatchBegin");
}

上一篇文章分析过,index就是函数执行的顺序节点

AppMethodBeat内部会维护一个long[]数组:sBuffer。还记的之前的插桩函数吗? 只要某个方法被调用那么就会回调到AppMethodBeat的i/o方法。

因此,matrix在i和o方法中会把 [ isIn标记(是不是i方法)+当前方法id(methodId)+耗时(duration)],封装为一个长度64位的long数据,在插入到sBuffer数组中。

而插入的下标是成员变量index,在每次插入后都会自增操作。当index自增超过sBuffer的长度后,又会从0开始重新自增。

至此,我们知道上述方法其实就是记录一个index开始值,方便后续获取堆栈。

记录绘制耗时的三个子阶段的耗时: input、animation、traversal。

@Override
public void doFrame(String focusedActivity, long startNs, long endNs, boolean isVsyncFrame, long intendedFrameTimeNs, long inputCostNs, long animationCostNs, long traversalCostNs) {
    queueTypeCosts[0] = inputCostNs;
    queueTypeCosts[1] = animationCostNs;
    queueTypeCosts[2] = traversalCostNs;
}

@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 {
        //达到阈值开始处理 默认是700ms
        if (dispatchCost >= evilThresholdMs) {
            //1,根据indexRecord标记,获取堆栈数据??具体怎么弄?
            // startIndex:之前标记的index。
            //endIndex: 当前index。
            long[] data = AppMethodBeat.getInstance().copyData(indexRecord);
            long[] queueCosts = new long[3];
            //copy 三个阶段的耗时:input、animation、traversal
            System.arraycopy(queueTypeCosts, 0, queueCosts, 0, 3);
            //得到当前页面
            String scene = AppMethodBeat.getVisibleScene();
            //2,post 一个AnalyseTask,开始分析
            MatrixHandlerThread.getDefaultHandler().post(new AnalyseTask(isForeground(), scene, data, queueCosts, cpuEndMs - cpuBeginMs, dispatchCost, endNs / Constants.TIME_MILLIS_TO_NANO));
        }
    } finally {
        //3,删除改节点
        indexRecord.release();
        if (config.isDevEnv()) {
            String usage = Utils.calculateCpuUsage(cpuEndMs - cpuBeginMs, dispatchCost);
            MatrixLog.v(TAG, "[dispatchEnd] token:%s cost:%sms cpu:%sms usage:%s innerCost:%s",
                    token, dispatchCost, cpuEndMs - cpuBeginMs, usage, System.currentTimeMillis() - start);
        }
    }
}
  1. 达到卡顿阈值,开始从AppMethodBeat的sBuffer数组中获取堆栈区间。得到一个long数组。
  2. 在matrix的工作线程中开始分析task
  3. 删除我们之前在dispatchBegin方法中标记的开始节点indexRecord

进入AnalyseTask类的analyse方法:

void analyse() {

    // process
    //获取进程的信息:priority, nice。nice值越低表示进程抢占CPU的执行权越强
    int[] processStat = Utils.getProcessPriority(Process.myPid());
    //根据线程时间和当前花费的时间,计算得到线程(这里是主线程)处于running状态的时间占整个耗时的时间
    //用来干啥? 用来判断主线程在App卡顿的时候是不是处于繁忙的状态。如果主线程的CPU占用率高,则以为着主线程做了太多的工作。
    String usage = Utils.calculateCpuUsage(cpuCost, cost);
    LinkedList<MethodItem> stack = new LinkedList();
    if (data.length > 0) {
        //根据data来构造堆栈集合
        TraceDataUtils.structuredDataToStack(data, stack, true, endMs);
        //裁剪堆栈
        TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {
            @Override
            public boolean isFilter(long during, int filterCount) {
                //耗时小于 30*5=150ms,则过滤
                return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS;
            }

            @Override
            public int getFilterMaxCount() {
                return Constants.FILTER_STACK_MAX_COUNT;
            }

            @Override
            public void fallback(List<MethodItem> stack, int size) {
                MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack);
                Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK));
                while (iterator.hasNext()) {
                    iterator.next();
                    iterator.remove();
                }
            }
        });
    }


    StringBuilder reportBuilder = new StringBuilder();
    StringBuilder logcatBuilder = new StringBuilder();
    //去最大耗时值
    long stackCost = Math.max(cost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));
    //得到stackKey 唯一
    String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);

    MatrixLog.w(TAG, "%s", printEvil(scene, processStat, isForeground, logcatBuilder, stack.size(), stackKey, usage, queueCost[0], queueCost[1], queueCost[2], cost)); // for logcat

    // report
    try {
        TracePlugin plugin = Matrix.with().getPluginByClass(TracePlugin.class);
        if (null == plugin) {
            return;
        }
        JSONObject jsonObject = new JSONObject();
        jsonObject = DeviceUtil.getDeviceInfo(jsonObject, Matrix.with().getApplication());

        jsonObject.put(SharePluginInfo.ISSUE_STACK_TYPE, Constants.Type.NORMAL);
        jsonObject.put(SharePluginInfo.ISSUE_COST, stackCost);
        jsonObject.put(SharePluginInfo.ISSUE_CPU_USAGE, usage);
        jsonObject.put(SharePluginInfo.ISSUE_SCENE, scene);
        jsonObject.put(SharePluginInfo.ISSUE_TRACE_STACK, reportBuilder.toString());
        jsonObject.put(SharePluginInfo.ISSUE_STACK_KEY, stackKey);

        Issue issue = new Issue();
        issue.setTag(SharePluginInfo.TAG_PLUGIN_EVIL_METHOD);
        issue.setContent(jsonObject);
        // 组合数据,格式为json,抛到上层
        plugin.onDetectIssue(issue);

    } catch (JSONException e) {
        MatrixLog.e(TAG, "[JSONException error: %s", e);
    }

}

简单的说

  1. 获取卡顿现场的信息
  2. 上报卡顿信息 拼接各种耗时、堆栈、顶层view等抛给上层

我们先看下卡顿的信息怎么获取。

  • 获取进程信息: 包含优先级、nice值。 nice值越低表示进程抢占CPU的执行权越强
  • 计算主线程CPU占用率
  • 得到堆栈的耗时
  • 获取堆栈并对堆栈进行裁剪,得到卡顿的唯一标识:stackKey

关于 CPU占用率问题 我们知道不管什么卡顿,其实最终都会反映到CPU的执行耗时上。因此,当发生卡顿的时候我们一般会比较关心三个CPU指标:

  1. 系统的CPU占用率
  2. 当前进程的CPU占用率
  3. 某个线程(如主线程)的CPU占用率

1,系统的CPU占用率:

可以通过在T1、T2两个时间点分别访问系统/proc/stat中的CPU执行信息,计算得到T1~T2这段时间内CPU总的执行时间total和空闲idle的时间。两者相减得到占用时间used。 used/total就得到系统的CPU占用率。不过很遗憾,Android 8后,Google已经关闭了访问权限。暂时没有找到好的方法,有大神知道麻烦告诉我~~

2,进程的CPU占用率:

同理,我们分别去采样系统的/proc/stat(无法访问)和/proc/pid/status(可以访问)。得到总的CPU执行时间total和当前进程的CPU执行时间pUsed。 pUsed/total就得到当前进程的CPU占用率。matrix 也是用的这种。也是因为权限问题,所以计算一直都是0。

3,主线程的CPU占用率

matrix通过 SystemClock.currentThreadTimeMillis()方法,该方法会返回当前线程处于running状态的毫秒时间。因此,在dispatcheBegin和dispatcheEnd两个节点处,计算得到真个消息处理的过程红的主线程处于running耗时cpuCost。

  //dispatchCost就是消息执行的总体耗时
String usage = Utils.calculateCpuUsage(cpuEndMs - cpuBeginMs, dispatchCost);
public static String calculateCpuUsage(long threadMs, long ms) {
    if (threadMs <= 0) {
        return ms > 1000 ? "0%" : "100%";
    }

    if (threadMs >= ms) {
        return "100%";
    }

    return String.format("%.2f", 1.f * threadMs / ms * 100) + "%";
}

因此可以得到卡顿过程中主线程处于running态的CPU时间占用。通过这个指标可以看出卡顿发生时候,如果占用率高则说明主线程是否有很多耗时的工作?需要引起我们注意。

AnalyseTask中比较耗脑的就是原始数据sBuffer如何进行整合以及裁剪,如何生成能够代表卡顿的key,方便进行聚合。代码的尽头终究是数据结构和算法!!!下面开始分析。

这部分在Matrix-Wiki中也有一点介绍。摘抄如下 堆栈聚类问题: 如果将收集的原始数据进行上报,数据量很大而且后台很难聚类有问题的堆栈,所以在上报之前需要对采集的数据进行简单的整合及裁剪,并分析出一个能代表卡顿堆栈的 key,方便后台聚合。

通过遍历采集的 buffer ,相邻 i 与 o 为一次完整函数执行,计算出一个调用树及每个函数执行耗时,并对每一级中的一些相同执行函数做聚合,最后通过一个简单策略,分析出主要耗时的那一级函数,作为代表卡顿堆栈的key。

matrix_stack.webp

在Analye线程中,会依次调

  1. 调用TraceDataUtils.structuredDataToStack()方法
  2. 调用TraceDataUtils.trimStack()方法
  3. 调用TraceDataUtils.getTreeKey()方法

具体怎么实现呢?跟着我的代码注释走!

/**
 *
 * 根据之前 data 查到的 methodId ,拿到对应插桩函数的执行时间、执行深度,将每个函数的信息封装成 MethodItem,然后存储到 stack 链表当中
 *
 * @param buffer
 * @param result
 * @param isStrict
 * @param endTime
 */
public static void structuredDataToStack(long[] buffer, LinkedList<MethodItem> result, boolean isStrict, long endTime) {
   
    long lastInId = 0L;
    //记录调用栈深度 后序转换多叉树的关键属性
    int depth = 0;
    //long数组是一个包含i和o有序序列,我们的目的就是把相关函数的i和o配对,组装成一个对象MethodItem,如何配对呢?
    //首先需要我们需要一个i的容器。正常情况一定是i先走,之后在数组中拿到o我们就和容器中的i进行查找,一旦配对就代表找到了数据,开始组装
    LinkedList<Long> rawData = new LinkedList<>();
    boolean isBegin = !isStrict;

    for (long trueId : buffer) {
        //无效数据,直接过滤
        if (0 == trueId) {
            continue;
        }
        //校验准备 校验只有从消息dispatchStart开始才算为有效数据
        if (isStrict) {
            if (isIn(trueId) && AppMethodBeat.METHOD_ID_DISPATCH == getMethodId(trueId)) {
                isBegin = true;
            }

            if (!isBegin) {
                MatrixLog.d(TAG, "never begin! pass this method[%s]", getMethodId(trueId));
                continue;
            }

        }
            
        //如果是 i 方法记录的数据 表示开始计时,存入rawData链表
        if (isIn(trueId)) {
            //获取methodId
            lastInId = getMethodId(trueId);
            if (lastInId == AppMethodBeat.METHOD_ID_DISPATCH) { //如果是 handler 的 dispatchMessage 方法 depth 置为0
                depth = 0;
            }
            depth++;
            //加入到链表中
            rawData.push(trueId);
        } else {// 如果是 0 方法记录的数据 代表一个函数结束,这个时候我们需要马上到i链表中查找,查的时候是一个退栈的过程,要时刻记录深度
            //获取methodId
            int outMethodId = getMethodId(trueId);
            if (!rawData.isEmpty()) {
                //拿到i 方法中记录的数据
                long in = rawData.pop();
                depth--;
                int inMethodId;
                //配对以后还需要把没有找到的数据塞回去,所以创建了tmp
                LinkedList<Long> tmp = new LinkedList<>();
                tmp.add(in);
                //如果  inMethodId 不等于 outMethodId 调用深度建议
                while ((inMethodId = getMethodId(in)) != outMethodId && !rawData.isEmpty()) {
                    MatrixLog.w(TAG, "pop inMethodId[%s] to continue match ouMethodId[%s]", inMethodId, outMethodId);
                    in = rawData.pop();
                    depth--;
                    tmp.add(in);
                }
                
                //可以走到这里正常情况下inMethodId和outMethodId刚好相等,配对成功
                //但是存在极端情况,比如插桩错误,数据丢失,这里额外做了过滤
                if (inMethodId != outMethodId && inMethodId == AppMethodBeat.METHOD_ID_DISPATCH) {
                    MatrixLog.e(TAG, "inMethodId[%s] != outMethodId[%s] throw this outMethodId!", inMethodId, outMethodId);
                    rawData.addAll(tmp);
                    depth += rawData.size();
                    continue;
                }
                
                //到这里就是真正的配对成功了,开始组装数据
                //获取到 方法执行完的时间
                long outTime = getTime(trueId);
                // 获取方法开始执行的时间
                long inTime = getTime(in);
                //该方法执行时间
                long during = outTime - inTime;
                if (during < 0) {
                    MatrixLog.e(TAG, "[structuredDataToStack] trace during invalid:%d", during);
                    rawData.clear();
                    result.clear();
                    return;
                }
                //创建一个 methodItem 并加入
                MethodItem methodItem = new MethodItem(outMethodId, (int) during, depth);
                //存入组装好的数据
                addMethodItem(result, methodItem);
            } else {
                MatrixLog.w(TAG, "[structuredDataToStack] method[%s] not found in! ", outMethodId);
            }
        }
    }
    
    //如果是严格模式,会考虑到 在我们统计耗时区间很有可能只有开头,没有结尾,因为函数的结尾还没执行到,这个时候的数据还是会记录,时间以 getDiffTime偏移量 和end Time为准
    while (!rawData.isEmpty() && isStrict) {
        long trueId = rawData.pop();
        int methodId = getMethodId(trueId);
        boolean isIn = isIn(trueId);
        long inTime = getTime(trueId) + AppMethodBeat.getDiffTime();
        MatrixLog.w(TAG, "[structuredDataToStack] has never out method[%s], isIn:%s, inTime:%s, endTime:%s,rawData size:%s",
                methodId, isIn, inTime, endTime, rawData.size());
        if (!isIn) {
            MatrixLog.e(TAG, "[structuredDataToStack] why has out Method[%s]? is wrong! ", methodId);
            continue;
        }
        MethodItem methodItem = new MethodItem(methodId, (int) (endTime - inTime), rawData.size());
        addMethodItem(result, methodItem);
    }
    TreeNode root = new TreeNode(null, null);
    //将链表转为树 进行整理数据,root是根节点
    stackToTree(result, root);
    //清空 result
    result.clear();
    //将 整理过的 数据 保存到 result中
    treeToStack(root, result);
}

long数组是一个包含i和o有序序列,我们的目的就是把相关函数的i和o配对,组装成一个对象MethodItem放入栈

注意添加的细节,一个函数可能调用多次,那么会累加时间和调用次数

private static int addMethodItem(LinkedList<MethodItem> resultStack, MethodItem item) {
    if (AppMethodBeat.isDev) {
        Log.v(TAG, "method:" + item);
    }
    MethodItem last = null;
    if (!resultStack.isEmpty()) {
        //获取第一个元素
        last = resultStack.peek();
    }
    //一个杉树调用多次的情况
    if (null != last && last.methodId == item.methodId && last.depth == item.depth && 0 != item.depth) {
        item.durTime = item.durTime == Constants.DEFAULT_ANR ? last.durTime : item.durTime;
        last.mergeMore(item.durTime);
        return last.durTime;
    } else {//添加到链表中
        resultStack.push(item);
        return item.durTime;
    }
}
public class MethodItem {

    public int methodId;
    public int durTime;
    public int depth;
    public int count = 1;

    public MethodItem(int methodId, int durTime, int depth) {
        this.methodId = methodId;
        this.durTime = durTime;
        this.depth = depth;
    }

    @Override
    public String toString() {
        return depth + "," + methodId + "," + count + "," + durTime;
    }

    public void mergeMore(long cost) {
        count++;
        durTime += cost;
    }

    public String print() {
        StringBuffer inner = new StringBuffer();
        for (int i = 0; i < depth; i++) {
            inner.append('.');
        }
        return inner.toString() + methodId + " " + count + " " + durTime;
    }
}

有了栈和深度属性就可以转换成上面所画的多叉树

多叉树需要知道自己的父亲和增加自己的儿子 ,自己的儿子可能有多个,所以是List,父亲只有一个

public static final class TreeNode {
    MethodItem item;
    TreeNode father;

    LinkedList<TreeNode> children = new LinkedList<>();

    TreeNode(MethodItem item, TreeNode father) {
        this.item = item;
        this.father = father;
    }

    private int depth() {
        return null == item ? 0 : item.depth;
    }

    private void add(TreeNode node) {
        children.addFirst(node);
    }

    private boolean isLeaf() {
        return children.isEmpty();
    }
}

宇宙的尽头是数据结构和算法

public static int stackToTree(LinkedList<MethodItem> resultStack, TreeNode root) {
    //树的形成先从根开始
    TreeNode lastNode = null;
    ListIterator<MethodItem> iterator = resultStack.listIterator(0);
    int count = 0;
    while (iterator.hasNext()) {
        TreeNode node = new TreeNode(iterator.next(), lastNode);
        count++;
        //不存在这种情况
        if (null == lastNode && node.depth() != 0) {
            MatrixLog.e(TAG, "[stackToTree] begin error! why the first node'depth is not 0!");
            return 0;
        }
        int depth = node.depth();
        //代表是根节点
        if (lastNode == null || depth == 0) {
            root.add(node);
            //当前节点深度更小,证明父亲在上层,需要往上查到他所在的层数
        } else if (lastNode.depth() >= depth) {
            while (null != lastNode && lastNode.depth() > depth) {
                lastNode = lastNode.father;
            }
            //找到所在的层数就可以挂到相关层节点了
            if (lastNode != null && lastNode.father != null) {
                node.father = lastNode.father;
                lastNode.father.add(node);
            }
        } else {
            //层数大于当前节点,就直接添加到下层
            lastNode.add(node);
        }
        lastNode = node;
    }
    return count;
}

有了树的结构就可以进行真正的排序了 从树的上层到下层,是包含关系,上层的耗时一定最长 同层就不一定了

private static void treeToStack(TreeNode root, LinkedList<MethodItem> list) {

    for (int i = 0; i < root.children.size(); i++) {
        TreeNode node = root.children.get(i);
        if (null == node) continue;
        //添加本层数据
        if (node.item != null) {
            list.add(node.item);
        }
        //如果存在儿子,继续递归
        if (!node.children.isEmpty()) {
            treeToStack(node, list);
        }
    }
}

是不是非常烧脑,我们已经翻过了最难的代码,胜利的曙光就在眼前,有了一定排序的数据就可以开始过滤剪裁了

TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {
    @Override
    public boolean isFilter(long during, int filterCount) {
        return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS;
    }

    @Override
    public int getFilterMaxCount() {
        return Constants.FILTER_STACK_MAX_COUNT;
    }

    @Override
    public void fallback(List<MethodItem> stack, int size) {
        MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack);
        Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK));
        while (iterator.hasNext()) {
            iterator.next();
            iterator.remove();
        }
    }
});

stack的顺序是由函数时间从大到小顺序排序的,剪裁就优先倒序从时间最短的开始

public static void trimStack(List<MethodItem> stack, int targetCount, IStructuredDataFilter filter) {
    //无效参数
    if (0 > targetCount) {
        stack.clear();
        return;
    }

    int filterCount = 1;
    int curStackSize = stack.size();
    //curStackSize当前数量 targetCount目标裁剪后的数量
    while (curStackSize > targetCount) {
        //为什么转迭代器呢,因为迭代器删除不会并发异常
        //倒序,从时间最小的开始删
        ListIterator<MethodItem> iterator = stack.listIterator(stack.size());
        while (iterator.hasPrevious()) {
            MethodItem item = iterator.previous();
            //裁剪之前会把循环的耗时干扰因素加进去,一次循环5ms
            if (filter.isFilter(item.durTime, filterCount)) {
                iterator.remove();
                curStackSize--;
                if (curStackSize <= targetCount) {
                    return;
                }
            }
        }
        curStackSize = stack.size();
        filterCount++;
        //删除成功
        if (filter.getFilterMaxCount() < filterCount) {
            break;
        }
    }
    int size = stack.size();
    //如果还是没有删除完,走自定义fallback逻辑
    if (size > targetCount) {
        filter.fallback(stack, size);
    }
}

往下走通过getTreeKey生成自己的唯一key

if (data.length > 0) {
    TraceDataUtils.structuredDataToStack(data, stack, true, endMs);
    TraceDataUtils.trimStack(stack, Constants.TARGET_EVIL_METHOD_STACK, new TraceDataUtils.IStructuredDataFilter() {
        @Override
        public boolean isFilter(long during, int filterCount) {
            return during < filterCount * Constants.TIME_UPDATE_CYCLE_MS;
        }

        @Override
        public int getFilterMaxCount() {
            return Constants.FILTER_STACK_MAX_COUNT;
        }

        @Override
        public void fallback(List<MethodItem> stack, int size) {
            MatrixLog.w(TAG, "[fallback] size:%s targetSize:%s stack:%s", size, Constants.TARGET_EVIL_METHOD_STACK, stack);
            Iterator iterator = stack.listIterator(Math.min(size, Constants.TARGET_EVIL_METHOD_STACK));
            while (iterator.hasNext()) {
                iterator.next();
                iterator.remove();
            }
        }
    });
}


StringBuilder reportBuilder = new StringBuilder();
StringBuilder logcatBuilder = new StringBuilder();
long stackCost = Math.max(cost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder));
String stackKey = TraceDataUtils.getTreeKey(stack, stackCost);

getTreeKey

//获取主要 耗时 方法id
public static String getTreeKey(List<MethodItem> stack, long stackCost) {
    StringBuilder ss = new StringBuilder();
    //耗时超过总是耗时占比30%我们认为需要关注
    long allLimit = (long) (stackCost * Constants.FILTER_STACK_KEY_ALL_PERCENT);
        
    LinkedList<MethodItem> sortList = new LinkedList<>();

    //过滤出主要耗时方法
    for (MethodItem item : stack) {
        if (item.durTime >= allLimit) {
            sortList.add(item);
        }
    }

    //排序
    Collections.sort(sortList, new Comparator<MethodItem>() {
        @Override
        public int compare(MethodItem o1, MethodItem o2) {
            return Integer.compare((o2.depth + 1) * o2.durTime, (o1.depth + 1) * o1.durTime);
        }
    });

    if (sortList.isEmpty() && !stack.isEmpty()) {//没有主要的耗时方法,就用第一个代替
        MethodItem root = stack.get(0);
        sortList.add(root);
    } else if (sortList.size() > 1 && sortList.peek().methodId == AppMethodBeat.METHOD_ID_DISPATCH) {//如果第一个是 handler.dipatchMessage 那就去掉
        sortList.removeFirst();
    }

    //拼接字符串
    for (MethodItem item : sortList) {
        ss.append(item.methodId + "|");
        break;
    }
    return ss.toString();
}

可能会有人对matrix的堆栈格式感到疑惑。我们从源码中找到答案

//去最大耗时值
long stackCost = Math.max(cost, TraceDataUtils.stackToString(stack, reportBuilder, logcatBuilder)); 

public static long stackToString(LinkedList<MethodItem> stack, StringBuilder reportBuilder, StringBuilder logcatBuilder) {
    logcatBuilder.append("|*\t\tTraceStack:").append("\n");
    logcatBuilder.append("|*\t\t[id count cost]").append("\n");
    Iterator<MethodItem> listIterator = stack.iterator();
    long stackCost = 0; // fix cost
    while (listIterator.hasNext()) {
        MethodItem item = listIterator.next();
        //拼接堆栈数据 
        reportBuilder.append(item.toString()).append('\n');
        logcatBuilder.append("|*\t\t").append(item.print()).append('\n');

        if (stackCost < item.durTime) {
            stackCost = item.durTime;
        }
    }
    return stackCost;
} 

这里会拼接堆栈数据,我们继续看下item.toString()方法:

@Override
public String toString() {
       //调用深度+ 逗号+ 方法id+逗号+调用次数+逗号+耗时
    return depth + "," + methodId + "," + count + "," + durTime;
} 

总结

总的来说,有了前面文章的铺垫,EvilMethodTracer类显得简单很多了。它主要职责是通过编译期插桩的i/o方法来计算在消息处理过程中每个函数的调用耗时。并且根据调用链生成调用堆栈,然后对堆栈进行裁剪,结合进程相关的一些信息抛给上层监听。

数据转换和裁剪的逻辑异常烧脑,撸完感觉自己瘦了,休息一段时间,下篇分享启动耗时监控