在平常的开发过程当中,App的性能和用户体验一直是咱们关注的重点,尤为是对于大公司来讲天天的日活都是千万或者上亿的量级。操做过程当中的不流畅和卡顿将严重影响用户的体验,甚至可能面临卸载致使用户流失。在拉新成本居高不下的现阶段,每个用户的流失对于咱们来讲都是直接的损失。因此想要留住用户就必须提高用户体验,那么流畅顺滑操做过程无卡顿就是咱们最基本也是重要的一环。可是随着如今移动端App的业务功能愈来愈复杂,随之带来的代码量剧增。在几十万行的代码中不免会出现效率低下或者不符合开发规范的代码存在,传统的代码Review须要消耗大量的人力物力并且也不能保证百分之百的可以发现问题,而google官方提供的IDE工具虽然功能强大,信息健全,可是操做复杂,同时还须要咱们开发者链接IDE而且手动的去记录和截取这段时间内的代码运行片断进行完整的分析。不少初级开发者对于这样的操做很排斥,在实际的开发过程当中使用的次数少之又少,甚至大部分同窗根本就没用过这个功能。 正是基于开发过程当中的种种痛点,DoKit利用AndroidStudio的官方插件功能加上ASM字节码操做框架,打造了一个开发者和用户都方便查看的函数耗时解决方案。这样,当咱们在开发过程当中只须要设置好相应的配置参数,在App运行过程当中,符合配置要求的函数就会在控制台中被打印出来,函数的耗时和当前所在线程以及当前函数的调用栈都清晰明日,从而极大的提高了用户体验而且下降开发者的开发难度。html
现有解决方案的原理java
现有方案的原理是基于Android SDK中提供的工具traceview和dmtracedump。其中traceview会生成.trace文件,该文件记录了函数调用顺序,函数耗时,函数调用次数等等有用的信息。而dmtracedump 工具就是基于trace文件生成报告的工具,具体用法不细说。dmtracedump 工具你们通常用的多的选项就是生成html报告,或者生成调用顺序图片(看起来很不直观)。首先说说为何要用traceview,和dmtracedump来做为获得函数调用顺序的,由于这个工具既然能知道cpu执行时间和调用次数以及函数调用树(看出函数调用顺序很费劲)好比在Android Studio是这样呈现.trace文件的解析视图的:node
或者是这样的:android
(以上两张图片来源于网络)git
经过以上两张图能够发现虽然官方提供的工具十分强大可是却有一个很严重的问题,那就是信息量太大,想要在这么繁杂的信息中找出你所须要的性能瓶颈点难度可想而知,通常的新手根本没有耐心和经验去操做,有时候甚至到懒得去使用这个工具。github
想要提高用户的开发体验,必须知足如下两点:算法
简单的操做(傻瓜式操做)编程
直观的数据展现json
(以上两点也是咱们DoKit团队在规划新功能时的重要指标)api
本人通过一系列的调研和尝试,发现市面上现有的解决方案多多少少都存在的必定的问题,好比经过AspectJ、Dexposed、Epic等AOP框架,虽然可以实现咱们的需求,可是却存在必定的兼容性问题,对于DoKit这样一个已经在8000+ App项目中集成使用的稳定性研发工具来讲,咱们不能保证用户在他本身的项目中是否也集成过此类框架,因为两个AOP框架之间因为版本不一致可能会致使编译失败。(其实一开始DoKit也是经过集成AspectJ等第三方框架来做为AOP编程的,后面社区反馈兼容性很差,因此针对整个AOP方案进行了优化和升级)。
通过屡次的Demo实验,最终决定采用Google官方的插件+ASM字节码框架做为DoKit的AOP解决方案。
Dokit提供了两个慢函数解决方案(经过插件可配置)
一、全量业务代码函数插装(代码量过大会致使编译时间过长)
二、指定入口函数并查找N级调用函数进行代码插装(默认方案)
(下文的分析主要针对第二种解决方案)
寻找指定的代码插桩节点
对于开发者说,咱们的目的是为了在项目运行过程当中第一时间发现有哪些函数耗时过长从而致使UI卡顿,而后对指定的慢函数进行耗时统计并给出友好的数据结构呈现。因此,既然要统计一个函数的耗时,咱们就必需要在一个函数的开始和结束地方插入统计代码,最后相减便可得出一个函数方法的耗时时间。
举个例子:假如咱们须要统计如下函数的耗时时间:
public void sleepMethod() {
Log.i(TAG, "我是耗时函数");
}
复制代码
其实原理很简单咱们只须要在函数的执行先后添加以下代码:
public void sleepMethod() {
long begin = System.currentTimeMillis();
Log.i(TAG, "我是耗时函数");
long costTime = System.currentTimeMillis() - begin;
}
复制代码
其中costTime即为当前函数的执行时间,咱们只须要将costTime根据函数的类名+函数名做为key保存在Map中,而后再根据必定的算法在运行期间去绑定函数的上下级调用关系(上下级调用关系会在编译时经过字节码增长框架动态插入,下文会分析)。最终在入口函数执行结束的将结果在控制台中打印出来便可。
插入指定的Plugin Transform
Google对于Android的插件开发提供了一个完整的开发套件,它容许咱们在Android代码的编译期间插入专属的Transform去读取编译后的class文件并搭配相应的字节码增长工具(ASM、Javassist)并回调相应的生命周期函数来让开发者在指定的生命周期(好比:开始读取一个函数以及函数读取结束等等)函数中去操做Java字节码。
因为AndroidStudio是基于Gradle做为编译脚本,因此咱们先来了解一下什么是Gradle。
一、Gradle 是基于Groovy的一种领域专用语言(DSL/Domain Specific Launguage) 二、每一个Gradle脚本文件编程生成的类除了继承自groovy.lang.script,同时还实现了接口org.gradle.api.script。 三、Gradle工程build时,会执行setting.gradle、build.gradle脚本;setting脚本的代理对象是Setting对象,build脚本的代理对象是Project对象。
如下为Gradle的生命周期图示:
咱们顺便来看一下Transform的工做原理
很明显的一个链式结构。其中红色表明自定义的Transform,蓝色表明系统自带的Transform。 每一个Transform都是一个Gradle的Task,Android编译其中的TaskManager会将每一个Transform串联起来。前一个Transform的执行产物将传递给下一个Transform做为输入。因此咱们只须要将自定义的Transform插入到链表的最前面,这样咱们就能够拿到javac的编译产物并利用字节码框架(ASM)对javac产物作字节码修改。
插入耗时统计代码
Dokit选取了ASM做为Java字节码操做框架,由于ASM更偏向底层操做兼容性更好同时效率也更高。可是因为全量的字节码插装会致使用户的编译时间增长尤为对于大型项目来讲,过长的编译时间会致使开发效率偏低。因此咱们必须针对插桩节点进行取舍,以达到开发效率和知足功能需求的平衡点。 如下附上ASM的时序图:
既然咱们须要在指定的入口函数中去查找调用的子函数,那么如何去肯定这个入口函数呢?DoKit的选择是将Application的attachBaseContex和onCreate这个两个方法做为默认的入口函数,即你们最为关心的App启动耗时统计,固然作为一个成熟的框架,咱们也开放了用户指定入口函数的配置,具体能够参考Android接入指南。
那么咱们该如何找到用户自定义的Application呢?你们都知道咱们的Application是须要在AndroidManifest.xml中注册才能使用的,并且AndroidManifest.xml中就包含了Application的全路径名。因此咱们只要在编译时找到AndroidManifest.xml的文件路径,而后再针对xml文件进行解析就能够获得Application的全路径名。具体的示例代码以下:
appExtension.getApplicationVariants().all(applicationVariant -> {
if (applicationVariant.getName().contains("debug")) {
VariantScopeKt.getMergedManifests(BaseVariantKt.getScope(applicationVariant))
.forEach(file -> {
try {
String manifestPath = file.getPath() + "/AndroidManifest.xml";
//System.out.println("Dokit==manifestPath=>" + manifestPath);
File manifest = new File(manifestPath);
if (manifest.exists()) {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
CommHandler handler = new CommHandler();
parser.parse(manifest, handler);
DoKitExtUtil.getInstance().setApplications(handler.getApplication());
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
});
复制代码
经过上文咱们已经拿到了Application类的全路径名以及入口函数,那么接下来的操做就是查找attachBaseContex和onCreat中调用了哪些方法。其实ASM的AdviceAdapter这个类的visitMethod生命周期函数会在读取class文件流时输出当前函数的全部字节码(关于visitMethodInsn方法的具体用户能够参考官方文档,本文只会介绍相关原理),因此咱们只须要根据本身的须要过滤出属于函数调用的部分就行。为了不全量字节码插入带来的编译耗时过长问题,我限制函数插桩调用层级最大为5级。在每一级函数的遍历过程当中,咱们须要对函数的父级进行绑定。由于只有肯定了父级函数,咱们才能在下一次Transform中精准的知道须要在哪些子函数中进行代码插装。
函数调用栈查找代码:
@Override
public void visitMethodInsn(int opcode, String innerClassName, String innerMethodName, String innerDesc, boolean isInterface) {
//全局替换URL的openConnection方法为dokit的URLConnection
//普通方法 内部方法 静态方法
if (opcode == Opcodes.INVOKEVIRTUAL || opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) {
//过滤掉构造方法
if (innerMethodName.equals("<init>")) {
super.visitMethodInsn(opcode, innerClassName, innerMethodName, innerDesc, isInterface);
return;
}
MethodStackNode methodStackNode = new MethodStackNode();
methodStackNode.setClassName(innerClassName);
methodStackNode.setMethodName(innerMethodName);
methodStackNode.setDesc(innerDesc);
methodStackNode.setParentClassName(className);
methodStackNode.setParentMethodName(methodName);
methodStackNode.setParentDesc(desc);
switch (level) {
case MethodStackNodeUtil.LEVEL_0:
methodStackNode.setLevel(MethodStackNodeUtil.LEVEL_1);
MethodStackNodeUtil.addFirstLevel(methodStackNode);
break;
case MethodStackNodeUtil.LEVEL_1:
methodStackNode.setLevel(MethodStackNodeUtil.LEVEL_2);
MethodStackNodeUtil.addSecondLevel(methodStackNode);
break;
case MethodStackNodeUtil.LEVEL_2:
methodStackNode.setLevel(MethodStackNodeUtil.LEVEL_3);
MethodStackNodeUtil.addThirdLevel(methodStackNode);
break;
case MethodStackNodeUtil.LEVEL_3:
methodStackNode.setLevel(MethodStackNodeUtil.LEVEL_3);
MethodStackNodeUtil.addFourthlyLevel(methodStackNode);
break;
case MethodStackNodeUtil.LEVEL_4:
methodStackNode.setLevel(MethodStackNodeUtil.LEVEL_3);
MethodStackNodeUtil.addFifthLevel(methodStackNode);
break;
default:
break;
}
}
super.visitMethodInsn(opcode, innerClassName, innerMethodName, innerDesc, isInterface);
}
复制代码
字节码插桩代码:
@Override
protected void onMethodEnter() {
super.onMethodEnter();
try {
if (isStaticMethod) {
//静态方法须要插入的代码
mv.visitMethodInsn(INVOKESTATIC, "com/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil", "getInstance", "()Lcom/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil;", false);
mv.visitIntInsn(SIPUSH, thresholdTime);
mv.visitInsn(level + ICONST_0);
mv.visitLdcInsn(className);
mv.visitLdcInsn(methodName);
mv.visitLdcInsn(desc);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil", "recodeStaticMethodCostStart", "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false);
} else {
//普通方法插入的代码
mv.visitMethodInsn(INVOKESTATIC, "com/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil", "getInstance", "()Lcom/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil;", false);
mv.visitIntInsn(SIPUSH, thresholdTime);
mv.visitInsn(level + ICONST_0);
mv.visitLdcInsn(className);
mv.visitLdcInsn(methodName);
mv.visitLdcInsn(desc);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil", "recodeObjectMethodCostStart", "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
try {
if (isStaticMethod) {
//静态方法须要插入的代码
mv.visitMethodInsn(INVOKESTATIC, "com/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil", "getInstance", "()Lcom/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil;", false);
mv.visitIntInsn(SIPUSH, thresholdTime);
mv.visitInsn(level + ICONST_0);
mv.visitLdcInsn(className);
mv.visitLdcInsn(methodName);
mv.visitLdcInsn(desc);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil", "recodeStaticMethodCostEnd", "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false);
} else {
//普通方法插入的代码
mv.visitMethodInsn(INVOKESTATIC, "com/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil", "getInstance", "()Lcom/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil;", false);
mv.visitIntInsn(SIPUSH, thresholdTime);
mv.visitInsn(level + ICONST_0);
mv.visitLdcInsn(className);
mv.visitLdcInsn(methodName);
mv.visitLdcInsn(desc);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKEVIRTUAL, "com/didichuxing/doraemonkit/aop/method_stack/MethodStackUtil", "recodeObjectMethodCostEnd", "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false);
}
} catch (Exception e) {
e.printStackTrace();
}
}
复制代码
运行时函数调用栈绑定
经过第三步咱们已经在适当的函数中插入了AOP模板耗时统计代码,可是最终仍是须要在代码运行期间才能统计出具体的函数运行耗时,并对函数调用作上下级绑定才能最终呈现出友好的数据展现。
因为在编译期间咱们已经知道了函数的上下级关系,而且将每一个函数的调用等级经过方法参数的形式插入了AOP模板中,因此接下来咱们只须要在函数运行期间对每一级的函数进行分类保存,并经过适当的算法绑定上下级关系便可。 AOP模板代码以下:
public class MethodStackUtil {
private static final String TAG = "MethodStackUtil";
/**
* key className&methodName
*/
private ConcurrentHashMap<String, MethodInvokNode> ROOT_METHOD_STACKS = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, MethodInvokNode> LEVEL1_METHOD_STACKS = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, MethodInvokNode> LEVEL2_METHOD_STACKS = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, MethodInvokNode> LEVEL3_METHOD_STACKS = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, MethodInvokNode> LEVEL4_METHOD_STACKS = new ConcurrentHashMap<>();
/**
* 静态内部类单例
*/
private static class Holder {
private static MethodStackUtil INSTANCE = new MethodStackUtil();
}
public static MethodStackUtil getInstance() {
return MethodStackUtil.Holder.INSTANCE;
}
/**
* @param level
* @param methodName
* @param classObj null 表明静态函数
*/
public void recodeObjectMethodCostStart(int thresholdTime, int level, String className, String methodName, String desc, Object classObj) {
try {
MethodInvokNode methodInvokNode = new MethodInvokNode();
methodInvokNode.setStartTimeMillis(System.currentTimeMillis());
methodInvokNode.setCurrentThreadName(Thread.currentThread().getName());
methodInvokNode.setClassName(className);
methodInvokNode.setMethodName(methodName);
if (level == 0) {
methodInvokNode.setLevel(0);
ROOT_METHOD_STACKS.put(String.format("%s&%s", className, methodName), methodInvokNode);
} else if (level == 1) {
methodInvokNode.setLevel(1);
LEVEL1_METHOD_STACKS.put(String.format("%s&%s", className, methodName), methodInvokNode);
} else if (level == 2) {
methodInvokNode.setLevel(2);
LEVEL2_METHOD_STACKS.put(String.format("%s&%s", className, methodName), methodInvokNode);
} else if (level == 3) {
methodInvokNode.setLevel(3);
LEVEL3_METHOD_STACKS.put(String.format("%s&%s", className, methodName), methodInvokNode);
} else if (level == 4) {
methodInvokNode.setLevel(4);
LEVEL4_METHOD_STACKS.put(String.format("%s&%s", className, methodName), methodInvokNode);
}
//特殊断定
if (level == 0) {
if (classObj instanceof Application) {
if (methodName.equals("onCreate")) {
TimeCounterManager.get().onAppCreateStart();
}
if (methodName.equals("attachBaseContext")) {
TimeCounterManager.get().onAppAttachBaseContextStart();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* @param level
* @param className
* @param methodName
* @param desc
* @param classObj null 表明静态函数
*/
public void recodeObjectMethodCostEnd(int thresholdTime, int level, String className, String methodName, String desc, Object classObj) {
synchronized (MethodCostUtil.class) {
try {
MethodInvokNode methodInvokNode = null;
if (level == 0) {
methodInvokNode = ROOT_METHOD_STACKS.get(String.format("%s&%s", className, methodName));
} else if (level == 1) {
methodInvokNode = LEVEL1_METHOD_STACKS.get(String.format("%s&%s", className, methodName));
} else if (level == 2) {
methodInvokNode = LEVEL2_METHOD_STACKS.get(String.format("%s&%s", className, methodName));
} else if (level == 3) {
methodInvokNode = LEVEL3_METHOD_STACKS.get(String.format("%s&%s", className, methodName));
} else if (level == 4) {
methodInvokNode = LEVEL4_METHOD_STACKS.get(String.format("%s&%s", className, methodName));
}
if (methodInvokNode != null) {
methodInvokNode.setEndTimeMillis(System.currentTimeMillis());
bindNode(thresholdTime, level, methodInvokNode);
}
//打印函数调用栈
if (level == 0) {
if (methodInvokNode != null) {
toStack(classObj instanceof Application, methodInvokNode);
}
if (classObj instanceof Application) {
//Application 启动时间统计
if (methodName.equals("onCreate")) {
TimeCounterManager.get().onAppCreateEnd();
}
if (methodName.equals("attachBaseContext")) {
TimeCounterManager.get().onAppAttachBaseContextEnd();
}
}
//移除对象
ROOT_METHOD_STACKS.remove(className + "&" + methodName);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private String getParentMethod(String currentClassName, String currentMethodName) {
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
int index = 0;
for (int i = 0; i < stackTraceElements.length; i++) {
StackTraceElement stackTraceElement = stackTraceElements[i];
if (currentClassName.equals(stackTraceElement.getClassName().replaceAll("\\.", "/")) && currentMethodName.equals(stackTraceElement.getMethodName())) {
index = i;
break;
}
}
StackTraceElement parentStackTraceElement = stackTraceElements[index + 1];
return String.format("%s&%s", parentStackTraceElement.getClassName().replaceAll("\\.", "/"), parentStackTraceElement.getMethodName());
}
private void bindNode(int thresholdTime, int level, MethodInvokNode methodInvokNode) {
if (methodInvokNode == null) {
return;
}
//过滤掉小于10ms的函数
if (methodInvokNode.getCostTimeMillis() <= thresholdTime) {
return;
}
MethodInvokNode parentMethodNode;
switch (level) {
case 1:
//设置父node 并将本身添加到父node中
parentMethodNode = ROOT_METHOD_STACKS.get(getParentMethod(methodInvokNode.getClassName(), methodInvokNode.getMethodName()));
if (parentMethodNode != null) {
methodInvokNode.setParent(parentMethodNode);
parentMethodNode.addChild(methodInvokNode);
}
break;
case 2:
//设置父node 并将本身添加到父node中
parentMethodNode = LEVEL1_METHOD_STACKS.get(getParentMethod(methodInvokNode.getClassName(), methodInvokNode.getMethodName()));
if (parentMethodNode != null) {
methodInvokNode.setParent(parentMethodNode);
parentMethodNode.addChild(methodInvokNode);
}
break;
case 3:
//设置父node 并将本身添加到父node中
parentMethodNode = LEVEL2_METHOD_STACKS.get(getParentMethod(methodInvokNode.getClassName(), methodInvokNode.getMethodName()));
if (parentMethodNode != null) {
methodInvokNode.setParent(parentMethodNode);
parentMethodNode.addChild(methodInvokNode);
}
break;
case 4:
//设置父node 并将本身添加到父node中
parentMethodNode = LEVEL3_METHOD_STACKS.get(getParentMethod(methodInvokNode.getClassName(), methodInvokNode.getMethodName()));
if (parentMethodNode != null) {
methodInvokNode.setParent(parentMethodNode);
parentMethodNode.addChild(methodInvokNode);
}
break;
default:
break;
}
}
public void recodeStaticMethodCostStart(int thresholdTime, int level, String className, String methodName, String desc) {
recodeObjectMethodCostStart(thresholdTime, level, className, methodName, desc, new StaicMethodObject());
}
public void recodeStaticMethodCostEnd(int thresholdTime, int level, String className, String methodName, String desc) {
recodeObjectMethodCostEnd(thresholdTime, level, className, methodName, desc, new StaicMethodObject());
}
private void jsonTravel(List<MethodStackBean> methodStackBeans, List<MethodInvokNode> methodInvokNodes) {
if (methodInvokNodes == null) {
return;
}
for (MethodInvokNode methodInvokNode : methodInvokNodes) {
MethodStackBean methodStackBean = new MethodStackBean();
methodStackBean.setCostTime(methodInvokNode.getCostTimeMillis());
methodStackBean.setFunction(methodInvokNode.getClassName() + "&" + methodInvokNode.getMethodName());
methodStackBean.setChildren(new ArrayList<MethodStackBean>());
jsonTravel(methodStackBean.getChildren(), methodInvokNode.getChildren());
methodStackBeans.add(methodStackBean);
}
}
private void stackTravel(StringBuilder stringBuilder, List<MethodInvokNode> methodInvokNodes) {
if (methodInvokNodes == null) {
return;
}
for (MethodInvokNode methodInvokNode : methodInvokNodes) {
stringBuilder.append(String.format("%s%s%s%s%s", methodInvokNode.getLevel(), SPACE_0, methodInvokNode.getCostTimeMillis() + "ms", getSpaceString(methodInvokNode.getLevel()), methodInvokNode.getClassName() + "&" + methodInvokNode.getMethodName())).append("\n");
stackTravel(stringBuilder, methodInvokNode.getChildren());
}
}
public void toJson() {
List<MethodStackBean> methodStackBeans = new ArrayList<>();
for (MethodInvokNode methodInvokNode : ROOT_METHOD_STACKS.values()) {
MethodStackBean methodStackBean = new MethodStackBean();
methodStackBean.setCostTime(methodInvokNode.getCostTimeMillis());
methodStackBean.setFunction(methodInvokNode.getClassName() + "&" + methodInvokNode.getMethodName());
methodStackBean.setChildren(new ArrayList<MethodStackBean>());
jsonTravel(methodStackBean.getChildren(), methodInvokNode.getChildren());
methodStackBeans.add(methodStackBean);
}
String json = GsonUtils.toJson(methodStackBeans);
LogUtils.json(json);
}
private static final String SPACE_0 = "********";
private static final String SPACE_1 = "*************";
private static final String SPACE_2 = "*****************";
private static final String SPACE_3 = "*********************";
private static final String SPACE_4 = "*************************";
public void toStack(boolean isAppStart, MethodInvokNode methodInvokNode) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("=========DoKit函数调用栈==========").append("\n");
stringBuilder.append(String.format("%s %s %s", "level", "time", "function")).append("\n");
stringBuilder.append(String.format("%s%s%s%s%s", methodInvokNode.getLevel(), SPACE_0, methodInvokNode.getCostTimeMillis() + "ms", getSpaceString(methodInvokNode.getLevel()), methodInvokNode.getClassName() + "&" + methodInvokNode.getMethodName())).append("\n");
stackTravel(stringBuilder, methodInvokNode.getChildren());
Log.i(TAG, stringBuilder.toString());
if (isAppStart && methodInvokNode.getLevel() == 0) {
if (methodInvokNode.getMethodName().equals("onCreate")) {
STR_APP_ON_CREATE = stringBuilder.toString();
}
if (methodInvokNode.getMethodName().equals("attachBaseContext")) {
STR_APP_ATTACH_BASECONTEXT = stringBuilder.toString();
}
}
}
public static String STR_APP_ON_CREATE;
public static String STR_APP_ATTACH_BASECONTEXT;
private String getSpaceString(int level) {
if (level == 0) {
return SPACE_0;
} else if (level == 1) {
return SPACE_1;
} else if (level == 2) {
return SPACE_2;
} else if (level == 3) {
return SPACE_3;
} else if (level == 4) {
return SPACE_4;
}
return SPACE_0;
}
}
复制代码
通过以上的四步操做,咱们已经实现了咱们一开始的需求,下面咱们就一块儿来看下最终的效果:
场景一:App启动
场景二:耗时方法
private fun test1() {
try {
Thread.sleep(1000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
test2()
}
private fun test2() {
try {
Thread.sleep(200)
} catch (e: InterruptedException) {
e.printStackTrace()
}
test3()
}
private fun test3() {
try {
Thread.sleep(200)
} catch (e: InterruptedException) {
e.printStackTrace()
}
test4()
}
private fun test4() {
try {
Thread.sleep(200)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
复制代码
其中test1()方法由点击事件触发。 效果以下:
场景一:App启动
场景二:耗时函数
DoKit一直追求给开发者提供最便捷和最直观的开发体验,同时咱们也十分欢迎社区中能有更多的人参与到DoKit的建设中来并给咱们提出宝贵的意见或PR。
DoKit的将来须要你们共同的努力。
最后,厚脸皮的拉一波star。来都来了,点个star再走呗。DoKit