在平常的開發過程當中,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