技術(shù)背景
在日常的開發(fā)過程中骗露,App的性能和用戶體驗一直是我們關(guān)注的重點,尤其是對于大公司來說每天的日活都是千萬或者上億的量級。操作過程中的不流暢和卡頓將嚴重影響用戶的體驗单芜,甚至可能面臨卸載導(dǎo)致用戶流失展姐。在拉新成本居高不下的現(xiàn)階段躁垛,每一個用戶的流失對于我們來說都是直接的損失。所以想要留住用戶就必須提升用戶體驗圾笨,那么流暢順滑操作過程無卡頓就是我們最基本也是重要的一環(huán)教馆。但是隨著現(xiàn)在移動端App的業(yè)務(wù)功能越來越復(fù)雜,隨之帶來的代碼量劇增擂达。在幾十萬行的代碼中難免會出現(xiàn)效率低下或者不符合開發(fā)規(guī)范的代碼存在土铺,傳統(tǒng)的代碼Review需要消耗大量的人力物力而且也不能保證百分之百的能夠發(fā)現(xiàn)問題,而google官方提供的IDE工具雖然功能強大板鬓,信息健全悲敷,但是操作復(fù)雜,同時還需要我們開發(fā)者連接IDE并且手動的去記錄和截取這段時間內(nèi)的代碼運行片段進行完整的分析俭令。很多初級開發(fā)者對于這樣的操作很排斥后德,在實際的開發(fā)過程中使用的次數(shù)少之又少,甚至大部分同學(xué)根本就沒用過這個功能抄腔。
正是基于開發(fā)過程中的種種痛點瓢湃,DoKit利用AndroidStudio的官方插件功能加上ASM字節(jié)碼操作框架理张,打造了一個開發(fā)者和用戶都方便查看的函數(shù)耗時解決方案。這樣绵患,當我們在開發(fā)過程中只需要設(shè)置好相應(yīng)的配置參數(shù)雾叭,在App運行過程中,符合配置要求的函數(shù)就會在控制臺中被打印出來落蝙,函數(shù)的耗時和當前所在線程以及當前函數(shù)的調(diào)用棧都清晰明日织狐,從而極大的提升了用戶體驗并且降低開發(fā)者的開發(fā)難度。
現(xiàn)有技術(shù)的缺點
現(xiàn)有解決方案的原理
現(xiàn)有方案的原理是基于Android SDK中提供的工具traceview和dmtracedump筏勒。其中traceview會生成.trace文件移迫,該文件記錄了函數(shù)調(diào)用順序,函數(shù)耗時奏寨,函數(shù)調(diào)用次數(shù)等等有用的信息起意。而dmtracedump 工具就是基于trace文件生成報告的工具,具體用法不細說病瞳。dmtracedump 工具大家一般用的多的選項就是生成html報告揽咕,或者生成調(diào)用順序圖片(看起來很不直觀)。首先說說為什么要用traceview套菜,和dmtracedump來作為得到函數(shù)調(diào)用順序的亲善,因為這個工具既然能知道cpu執(zhí)行時間和調(diào)用次數(shù)以及函數(shù)調(diào)用樹(看出函數(shù)調(diào)用順序很費勁)比如在Android Studio是這樣呈現(xiàn).trace文件的解析視圖的:
或者是這樣的:
(以上兩張圖片來源于網(wǎng)絡(luò))
通過以上兩張圖可以發(fā)現(xiàn)雖然官方提供的工具十分強大但是卻有一個很嚴重的問題,那就是信息量太大逗柴,想要在這么繁雜的信息中找出你所需要的性能瓶頸點難度可想而知蛹头,一般的新手根本沒有耐心和經(jīng)驗去操作,有時候甚至到懶得去使用這個工具戏溺。
DoKit的解決方案
想要提升用戶的開發(fā)體驗渣蜗,必須滿足以下兩點:
簡單的操作(傻瓜式操作)
直觀的數(shù)據(jù)展示
(以上兩點也是我們DoKit團隊在規(guī)劃新功能時的重要指標)
本人經(jīng)過一系列的調(diào)研和嘗試,發(fā)現(xiàn)市面上現(xiàn)有的解決方案多多少少都存在的一定的問題旷祸,比如通過AspectJ耕拷、Dexposed、Epic等AOP框架托享,雖然能夠?qū)崿F(xiàn)我們的需求骚烧,但是卻存在一定的兼容性問題,對于DoKit這樣一個已經(jīng)在8000+ App項目中集成使用的穩(wěn)定性研發(fā)工具來說闰围,我們不能保證用戶在他自己的項目中是否也集成過此類框架赃绊,由于兩個AOP框架之間由于版本不一致可能會導(dǎo)致編譯失敗。(其實一開始DoKit也是通過集成AspectJ等第三方框架來作為AOP編程的羡榴,后面社區(qū)反饋兼容性不好碧查,所以針對整個AOP方案進行了優(yōu)化和升級)。
經(jīng)過多次的Demo實驗校仑,最終決定采用Google官方的插件+ASM字節(jié)碼框架作為DoKit的AOP解決方案么夫。
DoKit解決方法的思路
Dokit提供了兩個慢函數(shù)解決方案(通過插件可配置)
1者冤、全量業(yè)務(wù)代碼函數(shù)插裝(代碼量過大會導(dǎo)致編譯時間過長)
2、指定入口函數(shù)并查找N級調(diào)用函數(shù)進行代碼插裝(默認方案)
(下文的分析主要針對第二種解決方案)
尋找指定的代碼插樁節(jié)點
對于開發(fā)者說档痪,我們的目的是為了在項目運行過程中第一時間發(fā)現(xiàn)有哪些函數(shù)耗時過長從而導(dǎo)致UI卡頓,然后對指定的慢函數(shù)進行耗時統(tǒng)計并給出友好的數(shù)據(jù)結(jié)構(gòu)呈現(xiàn)邢滑。所以腐螟,既然要統(tǒng)計一個函數(shù)的耗時,我們就必須要在一個函數(shù)的開始和結(jié)束地方插入統(tǒng)計代碼困后,最后相減即可得出一個函數(shù)方法的耗時時間乐纸。
舉個例子:假如我們需要統(tǒng)計以下函數(shù)的耗時時間:
public void sleepMethod() {
Log.i(TAG, "我是耗時函數(shù)");
}
其實原理很簡單我們只需要在函數(shù)的執(zhí)行前后添加如下代碼:
public void sleepMethod() {
long begin = System.currentTimeMillis();
Log.i(TAG, "我是耗時函數(shù)");
long costTime = System.currentTimeMillis() - begin;
}
其中costTime即為當前函數(shù)的執(zhí)行時間,我們只需要將costTime根據(jù)函數(shù)的類名+函數(shù)名作為key保存在Map中摇予,然后再根據(jù)一定的算法在運行期間去綁定函數(shù)的上下級調(diào)用關(guān)系(上下級調(diào)用關(guān)系會在編譯時通過字節(jié)碼增加框架動態(tài)插入,下文會分析)汽绢。最終在入口函數(shù)執(zhí)行結(jié)束的將結(jié)果在控制臺中打印出來即可。
插入指定的Plugin Transform
Google對于Android的插件開發(fā)提供了一個完整的開發(fā)套件侧戴,它允許我們在Android代碼的編譯期間插入專屬的Transform去讀取編譯后的class文件并搭配相應(yīng)的字節(jié)碼增加工具(ASM宁昭、Javassist)并回調(diào)相應(yīng)的生命周期函數(shù)來讓開發(fā)者在指定的生命周期(比如:開始讀取一個函數(shù)以及函數(shù)讀取結(jié)束等等)函數(shù)中去操作Java字節(jié)碼。
由于AndroidStudio是基于Gradle作為編譯腳本酗宋,所以我們先來了解一下什么是Gradle积仗。
1、Gradle 是基于Groovy的一種領(lǐng)域?qū)S谜Z言(DSL/Domain Specific Launguage)
2蜕猫、每個Gradle腳本文件編程生成的類除了繼承自groovy.lang.script,同時還實現(xiàn)了接口org.gradle.api.script寂曹。
3、Gradle工程build時回右,會執(zhí)行setting.gradle隆圆、build.gradle腳本;setting腳本的代理對象是Setting對象翔烁,build腳本的代理對象是Project對象渺氧。
以下為Gradle的生命周期圖示:
我們順便來看一下Transform的工作原理
很明顯的一個鏈式結(jié)構(gòu)。其中紅色代表自定義的Transform租漂,藍色代表系統(tǒng)自帶的Transform阶女。
每個Transform都是一個Gradle的Task,Android編譯其中的TaskManager會將每個Transform串聯(lián)起來哩治。前一個Transform的執(zhí)行產(chǎn)物將傳遞給下一個Transform作為輸入秃踩。所以我們只需要將自定義的Transform插入到鏈表的最前面,這樣我們就可以拿到j(luò)avac的編譯產(chǎn)物并利用字節(jié)碼框架(ASM)對javac產(chǎn)物做字節(jié)碼修改业筏。
插入耗時統(tǒng)計代碼
Dokit選取了ASM作為Java字節(jié)碼操作框架憔杨,因為ASM更偏向底層操作兼容性更好同時效率也更高。但是由于全量的字節(jié)碼插裝會導(dǎo)致用戶的編譯時間增加尤其對于大型項目來說蒜胖,過長的編譯時間會導(dǎo)致開發(fā)效率偏低消别。所以我們必須針對插樁節(jié)點進行取舍抛蚤,以達到開發(fā)效率和滿足功能需求的平衡點。
以下附上ASM的時序圖:
既然我們需要在指定的入口函數(shù)中去查找調(diào)用的子函數(shù)寻狂,那么如何去確定這個入口函數(shù)呢岁经?DoKit的選擇是將Application的attachBaseContex和onCreate這個兩個方法作為默認的入口函數(shù),即大家最為關(guān)心的App啟動耗時統(tǒng)計蛇券,當然做為一個成熟的框架缀壤,我們也開放了用戶指定入口函數(shù)的配置,具體可以參考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();
}
});
}
});
通過上文我們已經(jīng)拿到了Application類的全路徑名以及入口函數(shù)骗随,那么接下來的操作就是查找attachBaseContex和onCreat中調(diào)用了哪些方法蛤织。其實ASM的AdviceAdapter這個類的visitMethod生命周期函數(shù)會在讀取class文件流時輸出當前函數(shù)的所有字節(jié)碼(關(guān)于visitMethodInsn方法的具體用戶可以參考官方文檔,本文只會介紹相關(guān)原理),所以我們只需要根據(jù)自己的需要過濾出屬于函數(shù)調(diào)用的部分就行蚊锹。為了避免全量字節(jié)碼插入帶來的編譯耗時過長問題瞳筏,我限制函數(shù)插樁調(diào)用層級最大為5級。在每一級函數(shù)的遍歷過程中牡昆,我們需要對函數(shù)的父級進行綁定姚炕。因為只有確定了父級函數(shù),我們才能在下一次Transform中精準的知道需要在哪些子函數(shù)中進行代碼插裝丢烘。
函數(shù)調(diào)用棧查找代碼:
@Override
public void visitMethodInsn(int opcode, String innerClassName, String innerMethodName, String innerDesc, boolean isInterface) {
//全局替換URL的openConnection方法為dokit的URLConnection
//普通方法 內(nèi)部方法 靜態(tài)方法
if (opcode == Opcodes.INVOKEVIRTUAL || opcode == Opcodes.INVOKESTATIC || opcode == Opcodes.INVOKESPECIAL) {
//過濾掉構(gòu)造方法
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);
}
字節(jié)碼插樁代碼:
@Override
protected void onMethodEnter() {
super.onMethodEnter();
try {
if (isStaticMethod) {
//靜態(tài)方法需要插入的代碼
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) {
//靜態(tài)方法需要插入的代碼
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();
}
}
運行時函數(shù)調(diào)用棧綁定
通過第三步我們已經(jīng)在適當?shù)暮瘮?shù)中插入了AOP模板耗時統(tǒng)計代碼柱宦,但是最終還是需要在代碼運行期間才能統(tǒng)計出具體的函數(shù)運行耗時,并對函數(shù)調(diào)用做上下級綁定才能最終呈現(xiàn)出友好的數(shù)據(jù)展示播瞳。
由于在編譯期間我們已經(jīng)知道了函數(shù)的上下級關(guān)系掸刊,并且將每個函數(shù)的調(diào)用等級通過方法參數(shù)的形式插入了AOP模板中,所以接下來我們只需要在函數(shù)運行期間對每一級的函數(shù)進行分類保存赢乓,并通過適當?shù)乃惴ń壎ㄉ舷录夑P(guān)系即可忧侧。
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<>();
/**
* 靜態(tài)內(nèi)部類單例
*/
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 代表靜態(tài)函數(shù)
*/
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 代表靜態(tài)函數(shù)
*/
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);
}
//打印函數(shù)調(diào)用棧
if (level == 0) {
if (methodInvokNode != null) {
toStack(classObj instanceof Application, methodInvokNode);
}
if (classObj instanceof Application) {
//Application 啟動時間統(tǒng)計
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的函數(shù)
if (methodInvokNode.getCostTimeMillis() <= thresholdTime) {
return;
}
MethodInvokNode parentMethodNode;
switch (level) {
case 1:
//設(shè)置父node 并將自己添加到父node中
parentMethodNode = ROOT_METHOD_STACKS.get(getParentMethod(methodInvokNode.getClassName(), methodInvokNode.getMethodName()));
if (parentMethodNode != null) {
methodInvokNode.setParent(parentMethodNode);
parentMethodNode.addChild(methodInvokNode);
}
break;
case 2:
//設(shè)置父node 并將自己添加到父node中
parentMethodNode = LEVEL1_METHOD_STACKS.get(getParentMethod(methodInvokNode.getClassName(), methodInvokNode.getMethodName()));
if (parentMethodNode != null) {
methodInvokNode.setParent(parentMethodNode);
parentMethodNode.addChild(methodInvokNode);
}
break;
case 3:
//設(shè)置父node 并將自己添加到父node中
parentMethodNode = LEVEL2_METHOD_STACKS.get(getParentMethod(methodInvokNode.getClassName(), methodInvokNode.getMethodName()));
if (parentMethodNode != null) {
methodInvokNode.setParent(parentMethodNode);
parentMethodNode.addChild(methodInvokNode);
}
break;
case 4:
//設(shè)置父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函數(shù)調(diào)用棧==========").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;
}
}
最終效果
經(jīng)過以上的四步操作,我們已經(jīng)實現(xiàn)了我們一開始的需求牌芋,下面我們就一起來看下最終的效果:
默認方案
場景一: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()方法由點擊事件觸發(fā)蚓炬。
效果如下:
可選方案
場景一:App啟動
場景二:耗時函數(shù)
總結(jié)
DoKit一直追求給開發(fā)者提供最便捷和最直觀的開發(fā)體驗,同時我們也十分歡迎社區(qū)中能有更多的人參與到DoKit的建設(shè)中來并給我們提出寶貴的意見或PR。
DoKit的未來需要大家共同的努力躺屁。
最后肯夏,厚臉皮的拉一波star。來都來了,點個star再走唄驯击。DoKit