滴滴DoKit Android核心原理揭秘之函數(shù)耗時

技術(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文件的解析視圖的:

DoKit

或者是這樣的:

DoKit

(以上兩張圖片來源于網(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的生命周期圖示:


DoKit

我們順便來看一下Transform的工作原理


DoKit

很明顯的一個鏈式結(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的時序圖:

DoKit

既然我們需要在指定的入口函數(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啟動

DoKit

場景二:耗時方法

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ā)蚓炬。
效果如下:

DoKit

可選方案

場景一:App啟動

DoKit

場景二:耗時函數(shù)

DoKit

DoKit

DoKit

DoKit

DoKit

總結(jié)

DoKit一直追求給開發(fā)者提供最便捷和最直觀的開發(fā)體驗,同時我們也十分歡迎社區(qū)中能有更多的人參與到DoKit的建設(shè)中來并給我們提出寶貴的意見或PR。

DoKit的未來需要大家共同的努力躺屁。

最后肯夏,厚臉皮的拉一波star。來都來了,點個star再走唄驯击。DoKit

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末烁兰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子徊都,更是在濱河造成了極大的恐慌沪斟,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暇矫,死亡現(xiàn)場離奇詭異币喧,居然都是意外死亡,警方通過查閱死者的電腦和手機袱耽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來干发,“玉大人朱巨,你說我怎么就攤上這事⊥鞒ぃ” “怎么了冀续?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長必峰。 經(jīng)常有香客問我洪唐,道長,這世上最難降的妖魔是什么吼蚁? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任凭需,我火速辦了婚禮,結(jié)果婚禮上肝匆,老公的妹妹穿的比我還像新娘粒蜈。我一直安慰自己,他們只是感情好旗国,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布枯怖。 她就那樣靜靜地躺著,像睡著了一般能曾。 火紅的嫁衣襯著肌膚如雪度硝。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天寿冕,我揣著相機與錄音蕊程,去河邊找鬼。 笑死蚂斤,一個胖子當著我的面吹牛存捺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼捌治,長吁一口氣:“原來是場噩夢啊……” “哼岗钩!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起肖油,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤兼吓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后森枪,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體视搏,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年县袱,在試婚紗的時候發(fā)現(xiàn)自己被綠了浑娜。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡式散,死狀恐怖筋遭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情暴拄,我是刑警寧澤漓滔,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站乖篷,受9級特大地震影響响驴,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撕蔼,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一豁鲤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧罕邀,春花似錦畅形、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至肾胯,卻和暖如春竖席,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背敬肚。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工毕荐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人艳馒。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓憎亚,卻偏偏與公主長得像员寇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子第美,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355