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开关。
TracePlugin的start()方法会调用trace的onStartTrace方法
@Override
public void start() {
//省略...
if (traceConfig.isEvilMethodTraceEnable()) {
evilMethodTracer.onStartTrace();
}
if (traceConfig.isStartupEnable()) {
startupTracer.onStartTrace();
}
//省略...
}
而trace的onStartTrace方法则会调用onAlive方法。所以,我们直接看onAlive方法:
@Override
public void onAlive() {
super.onAlive();
if (isEvilMethodTraceEnable) {
UIThreadMonitor.getMonitor().addObserver(this);
}
}
向UIThreadMonitor中注册监听,肯定是监听三个回调方法:
- dispatchBegin 消息分发开始前
- dispatchEnd 消息分发结束后
- doFrame 每一帧消息都会回调
每个方法的详细参数分析,以及回调的原理参考前面文章。这里就不在详述了。
接着我看回调方法里面做了什么。
通过AppMethodBeat的maskIndex方法,记录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);
}
}
}
- 达到卡顿阈值,开始从AppMethodBeat的
sBuffer数组中获取堆栈区间。得到一个long数组。 - 在matrix的工作线程中开始分析task
- 删除我们之前在
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);
}
}
简单的说
- 获取卡顿现场的信息
- 上报卡顿信息 拼接各种耗时、堆栈、顶层view等抛给上层
我们先看下卡顿的信息怎么获取。
- 获取进程信息: 包含优先级、nice值。 nice值越低表示进程抢占CPU的执行权越强
- 计算主线程CPU占用率
- 得到堆栈的耗时
- 获取堆栈并对堆栈进行裁剪,得到卡顿的唯一标识:stackKey
关于 CPU占用率问题 我们知道不管什么卡顿,其实最终都会反映到CPU的执行耗时上。因此,当发生卡顿的时候我们一般会比较关心三个CPU指标:
- 系统的CPU占用率
- 当前进程的CPU占用率
- 某个线程(如主线程)的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。
在Analye线程中,会依次调
- 调用TraceDataUtils.structuredDataToStack()方法
- 调用TraceDataUtils.trimStack()方法
- 调用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方法来计算在消息处理过程中每个函数的调用耗时。并且根据调用链生成调用堆栈,然后对堆栈进行裁剪,结合进程相关的一些信息抛给上层监听。
数据转换和裁剪的逻辑异常烧脑,撸完感觉自己瘦了,休息一段时间,下篇分享启动耗时监控