查看原文
其他

滴滴DoKit Android核心原理揭秘之函数耗时

金台 极客人生THE GEEKS 2022-09-09
小编推荐:在拉新成本不断高涨的今天,对于互联网企业来说想要吸引和留住用户,除了独特的业务模式以外,良好的用户体验也是其中很重要的一部分。那你是否已经受够了官方的繁杂操作定位。现在dokit推出了零侵入“傻瓜式”的解决方案,帮你定位app生命周期中的函数耗时。
1.
技术背景

日常的开发过程中,App的性能和用户体验一直是我们关注的重点,尤其是对于大公司来说每天的日活都是千万或者上亿的量级。操作过程中的不流畅和卡顿将严重影响用户的体验,甚至可能面临卸载导致用户流失。在拉新成本居高不下的现阶段,每一个用户的流失对于我们来说都是直接的损失。所以想要留住用户就必须提升用户体验,那么流畅顺滑操作过程无卡顿就是我们最基本也是重要的一环。但是随着现在移动端App的业务功能越来越复杂,随之带来的代码量剧增。在几十万行的代码中难免会出现效率低下或者不符合开发规范的代码存在,传统的代码Review需要消耗大量的人力物力而且也不能保证百分之百的能够发现问题,而google官方提供的IDE工具虽然功能强大,信息健全,但是操作复杂,同时还需要我们开发者连接IDE并且手动的去记录和截取这段时间内的代码运行片段进行完整的分析。很多初级开发者对于这样的操作很排斥,在实际的开发过程中使用的次数少之又少,甚至大部分同学根本就没用过这个功能。正是基于开发过程中的种种痛点,DoKit利用AndroidStudio的官方插件功能加上ASM字节码操作框架,打造了一个开发者和用户都方便查看的函数耗时解决方案。这样,当我们在开发过程中只需要设置好相应的配置参数,在App运行过程中,符合配置要求的函数就会在控制台中被打印出来,函数的耗时和当前所在线程以及当前函数的调用栈都清晰明日,从而极大的提升了用户体验并且降低开发者的开发难度。

2.

现有技术的缺点

现有解决方案的原理

现有方案的原理是基于Android SDK中提供的工具traceview和dmtracedump。其中traceview会生成.trace文件,该文件记录了函数调用顺序,函数耗时,函数调用次数等等有用的信息。而dmtracedump 工具就是基于trace文件生成报告的工具,具体用法不细说。dmtracedump 工具大家一般用的多的选项就是生成html报告,或者生成调用顺序图片(看起来很不直观)。首先说说为什么要用traceview,和dmtracedump来作为得到函数调用顺序的,因为这个工具既然能知道cpu执行时间和调用次数以及函数调用树(看出函数调用顺序很费劲)比如在Android Studio是这样呈现.trace文件的解析视图的:

或者是这样的:

(以上两张图片来源于网络)
通过以上两张图可以发现虽然官方提供的工具十分强大但是却有一个很严重的问题,那就是信息量太大,想要在这么繁杂的信息中找出你所需要的性能瓶颈点难度可想而知,一般的新手根本没有耐心和经验去操作,有时候甚至到懒得去使用这个工具。
3.

DoKit的解决方案

想要提升用户的开发体验,必须满足以下两点:

简单的操作(傻瓜式操作)

直观的数据展示

(以上两点也是我们DoKit团队在规划新功能时的重要指标)

本人经过一系列的调研和尝试,发现市面上现有的解决方案多多少少都存在的一定的问题,比如通过AspectJ、Dexposed、Epic等AOP框架,虽然能够实现我们的需求,但是却存在一定的兼容性问题,对于DoKit这样一个已经在8000+ App项目中集成使用的稳定性研发工具来说,我们不能保证用户在他自己的项目中是否也集成过此类框架,由于两个AOP框架之间由于版本不一致可能会导致编译失败。(其实一开始DoKit也是通过集成AspectJ等第三方框架来作为AOP编程的,后面社区反馈兼容性不好,所以针对整个AOP方案进行了优化和升级)。

经过多次的Demo实验,最终决定采用Google官方的插件+ASM字节码框架作为DoKit的AOP解决方案。

DoKit解决方法的思路

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; }}

4.

最终效果

经过以上的四步操作,我们已经实现了我们一开始的需求,下面我们就一起来看下最终的效果:

默认方案

场景一: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启动

场景二:耗时函数

5.

总结

DoKit一直追求给开发者提供最便捷和最直观的开发体验,同时我们也十分欢迎社区中能有更多的人参与到DoKit的建设中来并给我们提出宝贵的意见或PR。

DoKit的未来需要大家共同的努力。

--------- DIDI ---------

普惠泛前端技术团队招聘

-
滴滴出行旗下的普惠产品技术部-泛前端技术团队。
是一个以“让技术成为业务发展的核心驱动力”为愿景的泛前端技术团队。
是一个有趣、有料,充满朝气、追求技术极致的泛前端技术团队。
是一个充满技术氛围、业务氛围、生活氛围的泛前端技术团队。
致力于沉淀可以惠及世界的技术成果,以技术为核心驱动业务发展,支持两轮车、代驾、跑腿等多条业务线的大泛前端技术团队。
涉及的泛前端技术栈以及框架有:Node.Js、Vue、React、ReactNative、CML、Hummer、Flutter、微信小程序等。
目前在杭州和北京长期招聘岗位,包括部分实习岗位
长期招聘岗位:
  • 高级/资深/专家 前端开发工程师
  • 高级/资深/专家 Android开发工程师
  • 高级/资深/专家 iOS开发工程师
  • 高级/资深/专家 NodeJs开发工程师
  • 高级/资深/专家 全栈开发工程师
  • 2021届毕业生(毕业时间:2020.11月-2021.10月)实习生
投递简历:puhuiwork@didiglobal.com
邮件主题请命名为【姓名+应聘部门+技术方向】
期待你的投递,我们会给与最快的处理和回应!

我们认真对待每一份简历,因每一份简历背后都有可能藏着一个改变世界的你~


--------- PUHUI TECH ---------

本文作者
-
金台
滴滴 | 高级软件开发工程师

我是来自浙江台州爱好看电影、唱歌、爬山并对于代码有轻微洁癖的狮子座程序员同时也是开源项目的死忠粉。人生格言:当你停止尝试时候,就是失败的时候。

编辑 | 谢园
-
推荐阅读


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存