App流暢度優(yōu)化:利用字節(jié)碼插樁實(shí)現(xiàn)一個(gè)快速排查高耗時(shí)方法的工具

????我們產(chǎn)線的主流程頁面中有幾個(gè)比較復(fù)雜的頁面在版本迭代中流暢度頻繁出現(xiàn)反復(fù),經(jīng)常由于開發(fā)的不注意導(dǎo)致變卡,主要是對(duì)流暢度缺少必要的監(jiān)控和可持續(xù)的優(yōu)化手段,這個(gè)系列是對(duì)上半年實(shí)踐App流暢度監(jiān)控、優(yōu)化過程中的一點(diǎn)總結(jié)币砂,希望可以給需要的同學(xué)一點(diǎn)小參考。

當(dāng)然App內(nèi)存上的優(yōu)化玻侥,盡量減少內(nèi)存抖動(dòng)也能顯著提升流暢度道伟,內(nèi)存優(yōu)化方面可以參考之前的文章:實(shí)踐App內(nèi)存優(yōu)化:如何有序地做內(nèi)存分析與優(yōu)化

整個(gè)系列將主要包括以下幾部分:

  1. 卡頓與View的繪制過程解析

    這部分內(nèi)容比較多,主要是從源碼層面解析一下整個(gè)過程,也是我們后面做流暢度監(jiān)控與優(yōu)化的基礎(chǔ)

  2. Debug階段如何對(duì)實(shí)時(shí)幀率進(jìn)行監(jiān)控和顯示

    根據(jù)上面的原理蜜徽,設(shè)計(jì)一個(gè)顯示實(shí)時(shí)幀率的工具,可以有效的在開發(fā)階段發(fā)現(xiàn)問題

  3. 如何實(shí)現(xiàn)流暢度自動(dòng)化測(cè)試

    實(shí)現(xiàn)一個(gè)流暢度UI自動(dòng)化測(cè)試票摇,在上線前跑一下UI自動(dòng)化并生成流暢度報(bào)表郵件給相關(guān)人員

  4. 線上的用戶流暢度的監(jiān)控方案

    實(shí)時(shí)反映真實(shí)用戶的流暢度體驗(yàn)拘鞋,線上龐大的數(shù)據(jù)可以敏感的反應(yīng)出版本迭代間流暢度的變化

  5. 實(shí)現(xiàn)一個(gè)方便排查高耗時(shí)方法的工具

    利用自定義gradle plugin+ASM插樁實(shí)現(xiàn)快速而準(zhǔn)確的找出耗時(shí)的方法,進(jìn)行針對(duì)性的優(yōu)化

  6. 分享提升app流暢度的一些經(jīng)驗(yàn)

    分享一些成本小收益高的提升流暢度的方案


?
?

工欲善其事必先利其器矢门,今天首先分享一下在優(yōu)化頁面流暢度過程中自己實(shí)現(xiàn)的一個(gè)方便快速排查高耗時(shí)方法的工具:MethodTraceMan盆色,畢竟保持主流程流暢,避免在主流程執(zhí)行高耗時(shí)方法永遠(yuǎn)是優(yōu)化卡頓最直接的手段祟剔,只要我們能快速方便的排查到高耗時(shí)的方法隔躲,就可以做針對(duì)性優(yōu)化。

實(shí)現(xiàn)一個(gè)方便排查高耗時(shí)方法的工具

????平常我們用來排查Android卡頓的比較熟悉的工具有TraceView物延、systrace等宣旱,一般分為兩種模式:instrumentsample。但是這些工具不管是哪種模式都有各自不足的地方叛薯,比如instruement模式浑吟,可以獲得所有函數(shù)的調(diào)用過程,信息比較豐富耗溜,但是會(huì)帶來極大的性能開銷组力,導(dǎo)致統(tǒng)計(jì)的耗時(shí)與實(shí)際不符;而sample模式是通過采樣的方式進(jìn)行分析的抖拴,所以信息豐富度上就大打折扣燎字,像systrace就屬于sample型的,它只能監(jiān)控一些系統(tǒng)調(diào)用的耗時(shí)情況阿宅。

????除了上面說的工具候衍,著名的JackWharton也實(shí)現(xiàn)了一個(gè)可以打印出出方法耗時(shí)的工具hugo,它是基于注解觸發(fā)的,在一個(gè)方法上加上特定注解即可打印出該方法的耗時(shí)等信息家夺,但是如果我們想排查高耗時(shí)方法脱柱,顯然在所有方法上一個(gè)一個(gè)加注解太費(fèi)勁了。
?

那么我們?cè)谧隹D優(yōu)化的過程中需要一個(gè)什么樣的工具呢拉馋?

  • 可以方便地統(tǒng)計(jì)所有方法的耗時(shí)
  • 對(duì)性能影響微小榨为,能準(zhǔn)確統(tǒng)計(jì)出方法的精確耗時(shí)
  • 支持耗時(shí)篩選、線程篩選煌茴、方法名搜索等功能随闺,能快速發(fā)現(xiàn)主線程高耗時(shí)方法

????要實(shí)現(xiàn)這樣一個(gè)工具,首先想到的就是通過插樁技術(shù)來實(shí)現(xiàn)蔓腐,在編譯過程中對(duì)所有的方法進(jìn)行插樁矩乐,在方法進(jìn)入和方法結(jié)束的地方進(jìn)行打點(diǎn),就可以在對(duì)性能影響很小的方式下統(tǒng)計(jì)到每個(gè)方法的耗時(shí)。統(tǒng)計(jì)到每個(gè)方法的耗時(shí)數(shù)據(jù)后散罕,我們?cè)賹?shí)現(xiàn)一個(gè)UI界面來展示這些數(shù)據(jù)分歇,并實(shí)現(xiàn)耗時(shí)篩選、線程篩選欧漱、方法名搜索等功能职抡,這樣我們就可以快速的找到主線程高耗時(shí)的方法進(jìn)行針對(duì)性的優(yōu)化了。

1. 效果預(yù)覽

我們先來看下最終實(shí)現(xiàn)的效果預(yù)覽:

輸出所有的方法耗時(shí)误甚,高耗時(shí)方法以紅色預(yù)警缚甩,同時(shí)支持對(duì)耗時(shí)篩選,線程篩選窑邦,方法名搜索等擅威,比如想篩出主線程耗時(shí)大于50ms的方法,就可以很方便的找出冈钦。
?

詳細(xì)的集成以及使用文檔詳見:MethodTraceMan

效果預(yù)覽

2. 技術(shù)選型

????插樁技術(shù)其實(shí)充斥在我們平常開發(fā)中的方方面面郊丛,可以幫助我們實(shí)現(xiàn)很多繁瑣復(fù)雜的功能,還可以幫助我們提高功能的穩(wěn)定性派继,比如ButterKnife宾袜、Protocol Buffers等都會(huì)在編譯時(shí)期生成代碼,當(dāng)然插樁技術(shù)也分很多種驾窟,比如ButterKnife是利用APT在編譯的開始階段對(duì)java文件進(jìn)行操作庆猫,而像AscpectJ、ASM等則是在java文件編譯為字節(jié)碼文件后绅络,對(duì)字節(jié)碼進(jìn)行操作月培,當(dāng)然還有一些可以在字節(jié)碼文件被編譯為dex文件后對(duì)dex進(jìn)行操作的框架。
由于我們的需求是在編譯期對(duì)所有的方法的進(jìn)入和結(jié)束的地方插樁進(jìn)行耗時(shí)統(tǒng)計(jì)恩急,所以最終的技術(shù)選型鎖定在對(duì)字節(jié)碼文件的操作杉畜。那么我們來對(duì)比一下AspectJ和ASM兩種字節(jié)碼插樁的框架:

一. AspectJ

????AspectJ是老牌的字節(jié)碼處理框架了,其優(yōu)點(diǎn)就是使用簡(jiǎn)單上手容易衷恭,不需要了解字節(jié)碼相關(guān)知識(shí)也可以在項(xiàng)目中集成使用此叠,只要指定簡(jiǎn)單的規(guī)則就可以完成對(duì)代碼的插樁,比如我們現(xiàn)在要實(shí)現(xiàn)對(duì)所有方法的進(jìn)入和退出時(shí)進(jìn)行插樁随珠,十分簡(jiǎn)單灭袁,如下:

@Before("execution(* **(..))")
public void beforeMethod(JoinPoint joinPoint) {
    //TODO 耗時(shí)統(tǒng)計(jì)
}

@After("execution(* **(..))")
public void afterMethod() {
    //TODO 耗時(shí)統(tǒng)計(jì)
}

當(dāng)然相對(duì)于優(yōu)點(diǎn)來說,AspectJ的缺點(diǎn)是窗看,由于其基于規(guī)則茸歧,所以其切入點(diǎn)相對(duì)固定,對(duì)于字節(jié)碼文件的操作自由度以及開發(fā)的掌控度就大打折扣显沈。還有就是我們要實(shí)現(xiàn)的是對(duì)所有方法進(jìn)行插樁软瞎,所以代碼注入后的性能也是我們需要關(guān)注的一個(gè)重要的點(diǎn)逢唤,我們希望只插入我們想插入的代碼,而AspectJ會(huì)額外生成一些包裝代碼涤浇,對(duì)性能以及包大小有一定影響鳖藕。

二. ASM

????ASM是一個(gè)十分強(qiáng)大的字節(jié)碼處理框架,基本上可以實(shí)現(xiàn)任何對(duì)字節(jié)碼的操作只锭,也就是自由度和開發(fā)的掌控度很高吊奢,但是其相對(duì)來說比AspectJ上手難度要高,需要對(duì)Java字節(jié)碼有一定了解纹烹,不過ASM為我們提供了訪問者模式來訪問字節(jié)碼文件,這種模式下可以比較簡(jiǎn)單的做一些字節(jié)碼操作召边,實(shí)現(xiàn)一些功能铺呵。同時(shí)ASM可以精確的只注入我們想要注入的代碼,不會(huì)額外生成一些包裝代碼隧熙,所以性能上影響比較微小片挂。

上面說了很多,對(duì)于java字節(jié)碼贞盯,這里做一些簡(jiǎn)單的介紹:

java字節(jié)碼

我們都知道在java文件的通過javac編譯后會(huì)生成十六進(jìn)制的class文件音念,比如我們先編寫一個(gè)簡(jiǎn)單的Test.java文件:

public class Test {
    private int m = 1;

    public int add() {
        int j = 2;
        int k = m + j;
        return k;
    }
}

然后我們通過 javac Test.java -g來編譯為Test.class,用文本編輯器打開如下:

test.class

可以看到是一堆十六進(jìn)制數(shù),但是其實(shí)這一堆十六進(jìn)制數(shù)是按嚴(yán)格的結(jié)構(gòu)拼接在一起的躏敢,按順序分別是:魔數(shù)(cafe babe)闷愤、java版本號(hào)、常量池件余、訪問權(quán)限標(biāo)志讥脐、當(dāng)前類索引、父類索引啼器、接口索引旬渠、字段表、方法表端壳、附加屬性等十個(gè)部分告丢,這些部分以十六進(jìn)制的形式表達(dá)出來并緊湊的拼接在一起,就是上面看到的class字節(jié)碼文件损谦。

當(dāng)然上面的十六進(jìn)制文件顯然不具備可閱讀性岖免,所以我們可以通過 javap -verbose Test來反編譯,有興趣的可以自己試一試成翩,就可以看到上面說的十個(gè)部分觅捆,由于我們做字節(jié)碼插樁一般和方法表關(guān)聯(lián)比較大,所以我們下面著重看一下方法表麻敌,下面是反編譯后的add()方法:

add方法

可以看到包括三部分:

  1. Code: 這里部分就是方法里的JVM指令操作碼栅炒,也是最重要的一部分,因?yàn)槲覀兎椒ɡ锏倪壿媽?shí)際上就是一條一條的指令操作碼來完成的。這里可以看到我們的add方法是通過9條指令操作碼完成的赢赊。當(dāng)然插樁重點(diǎn)操作的也是這一塊乙漓,只要能修改指令,也就能操控任何代碼了释移。
  2. LineNumberTable: 這個(gè)是表示行號(hào)表叭披。是我們的java源碼與指令行的行號(hào)對(duì)應(yīng)。比如我們上面的add方法java源碼里總共有三行玩讳,也就是上圖中的line10涩蜘、line11、line12,這三行對(duì)應(yīng)的JVM指令行數(shù)熏纯。有了這樣的對(duì)應(yīng)關(guān)系后同诫,就可以實(shí)現(xiàn)比如Debug調(diào)試的功能,指令執(zhí)行的時(shí)候樟澜,我們就可以定位到該指令對(duì)應(yīng)的源碼所在的位置误窖。
  3. LocalVariableTable:本地變量表,主要包括This和方法里的局部變量秩贰。從上圖可以看到add方法里有this霹俺、j、k三個(gè)局部變量毒费。

由于JVM指令集是基于棧的丙唧,上面我們已經(jīng)了解到了add方法的邏輯編譯為class文件后變成了9個(gè)指令操作碼,下面我們簡(jiǎn)單看看這些指令操作碼是如何配合操作數(shù)棧+本地變量表+常量池來執(zhí)行add方法的邏輯的:

指令操作

按順序執(zhí)行9條指令操作碼:

  • 0:把數(shù)字2入棧
  • 1:將2賦值給本地變量表中的j
  • 2蝗罗、3:獲取常量池中的m入棧
  • 6:將本地變量表中的j入棧
  • 7艇棕、8:將m和j相加,然后賦值給本地變量表中的k
  • 9串塑、10:將本地變量表中的k入棧沼琉,并return

好的,關(guān)于java字節(jié)碼的暫時(shí)就簡(jiǎn)單介紹這些桩匪,主要是讓我們基本了解字節(jié)碼文件的結(jié)構(gòu)打瘪,以及編譯后代碼時(shí)如何運(yùn)行的。而ASM可以通過操作指令碼來生成字節(jié)碼或者插樁傻昙,當(dāng)你可以利用ASM來接觸到字節(jié)碼闺骚,并且可以利用ASM的api來操控字節(jié)碼時(shí),就有很大的自由度來進(jìn)行各種字節(jié)碼的生成妆档、修改僻爽、操作等等,也就能產(chǎn)生很強(qiáng)大的功能贾惦。

三胸梆、Gradle plugin + Transform

????上面對(duì)于插樁框架的選擇敦捧,我們通過對(duì)比最終選擇了ASM,但是ASM只負(fù)責(zé)操作字節(jié)碼碰镜,我們還需要通過自定義gradle plugin的形式來干預(yù)編譯過程兢卵,在編譯過程中獲取到所有的class文件和jar包,然后遍歷他們绪颖,利用ASM來修改字節(jié)碼,達(dá)到插樁的目的苫耸。

????那么干預(yù)編譯的過程甸祭,我們的第一個(gè)念頭可能就是,對(duì)class轉(zhuǎn)為dex的任務(wù)進(jìn)行hook蹂午,在class轉(zhuǎn)為dex之前拿到所有的class文件裙秋,然后利用ASM對(duì)這些字節(jié)碼文件進(jìn)行插樁挺勿,然后再把處理過的字節(jié)碼文件作為transformClassesWithDex任務(wù)的輸入即可猜敢。這種方案的好處是易于控制吐葱,我們明確的知道操作的字節(jié)碼文件是最終的字節(jié)碼,因?yàn)槲覀兪窃?code>transformClassesWithDex任務(wù)的前一刻拿到字節(jié)碼文件的糜俗。缺點(diǎn)就是,如果項(xiàng)目開啟了混淆曲饱,那么在transformClassesWithDex任務(wù)的前一刻拿到的字節(jié)碼文件顯然是經(jīng)過了混淆了的悠抹,所以利用ASM操作字節(jié)碼的時(shí)候還需要mapping文件進(jìn)行配合才能找到正確的插樁點(diǎn),這一點(diǎn)比較麻煩扩淀。

????幸虧gradle還為我們提供了另一種干預(yù)編譯轉(zhuǎn)換過程的方法:Transform.其實(shí)我們稍微翻一下gradle編譯過程的源碼楔敌,就會(huì)發(fā)現(xiàn)一些我們熟知的功能都是通過Transform來實(shí)現(xiàn)的。還有一點(diǎn)驻谆,就是關(guān)于混淆的問題卵凑,上面我們說了如果通過hook transformClassesWithDex任務(wù)的方式來實(shí)現(xiàn)插樁,開啟混淆的情況下會(huì)出現(xiàn)問題胜臊,那么利用Transform的方式會(huì)不會(huì)有混淆的問題呢勺卢?下面我們從gradle源碼上面找一下答案:

我們從com.android.build.gradle.internal.TaskManager類里的createCompileTask()方法看起,顯然這是一個(gè)創(chuàng)建編譯任務(wù)的方法:

protected void createCompileTask(@NonNull VariantScope variantScope) {
        //創(chuàng)建一個(gè)將java文件編譯為class文件的任務(wù)
        JavaCompile javacTask = createJavacTask(variantScope);
        addJavacClassesStream(variantScope);
        setJavaCompilerTask(javacTask, variantScope);
        
        //創(chuàng)建一些在編譯為class文件后執(zhí)行的額外任務(wù),比如一些Transform等
        createPostCompilationTasks(variantScope);
    }

接下來我們看看createPostCompilationTasks()方法象对,這個(gè)方法比較長(zhǎng)黑忱,下面只保留重要的幾個(gè)代碼:

public void createPostCompilationTasks(@NonNull final VariantScope variantScope) {
       、勒魔、甫煞、、冠绢、抚吠、
    TransformManager transformManager = variantScope.getTransformManager();
      、弟胀、楷力、喊式、、
     // ----- External Transforms 這個(gè)就是我們自定義注冊(cè)進(jìn)來的Transform-----
     // apply all the external transforms.
        List<Transform> customTransforms = extension.getTransforms();
        List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();
        弥雹、垃帅、、剪勿、贸诚、、
        厕吉、酱固、、头朱、运悲、班眯、
        // ----- Minify next  這個(gè)就是混淆代碼的Transform-----
        CodeShrinker shrinker = maybeCreateJavaCodeShrinkerTransform(variantScope);
        、署隘、亚隙、、阿弃、、
        渣淳、脾还、、入愧、荠呐、、
    }

????其實(shí)這個(gè)方法里有很多其他Transform砂客,這里都省略了泥张,我們重點(diǎn)只看我們自定義注冊(cè)的Transform和混淆代碼的Transform,從上面的代碼上我們自定義的Transform是在混淆Transform之前添加進(jìn)TransformManager,所以執(zhí)行的時(shí)候我們自定義的Transform也會(huì)在混淆之前執(zhí)行的鞠值,也就是說我們利用自定義Transform的方式對(duì)代碼進(jìn)行插樁是不受混淆影響的媚创。

所以我們最終確定的方案就是 Gradle plugin + Transform +ASM的技術(shù)方案。下面我們正式說說利用該技術(shù)方案進(jìn)行具體實(shí)現(xiàn)彤恶。

3. 具體實(shí)現(xiàn)

這里具體實(shí)現(xiàn)只挑重點(diǎn)實(shí)現(xiàn)步驟講钞钙,詳細(xì)的可以看具體源碼,文章結(jié)尾提供了項(xiàng)目的github地址鳄橘。

一、自定義gradle plugin

關(guān)于如何創(chuàng)建一個(gè)自定義gradle plugin的項(xiàng)目芒炼,這邊就不細(xì)說了瘫怜,可以網(wǎng)上搜索,或者直接看MethodTraceMan項(xiàng)目的源碼也行本刽,自定義gradle plgin繼承自Plugin類鲸湃,入口是apply方法,我們的apply方法里很簡(jiǎn)單子寓,就是創(chuàng)建一個(gè)自定義擴(kuò)展配置暗挑,然后就是注冊(cè)一下我們自定義的Transform:

@Override
    void apply(Project project) {

        println '*****************MethodTraceMan Plugin apply*********************'
        project.extensions.create("traceMan", TraceManConfig)

        def android = project.extensions.getByType(AppExtension)
        android.registerTransform(new TraceManTransform(project))
    }

二、自定義Transform實(shí)現(xiàn)

這里我們創(chuàng)建了一個(gè)名叫traceMan的擴(kuò)展斜友,這樣我們可以再使用這個(gè)plugin的時(shí)候進(jìn)行一些配置炸裆,比如配置插樁的范圍,配置是否開啟插樁等鲜屏,這樣我們就可以根據(jù)自己的需要來配置烹看。

接下來我們看一下TraceManTransform的實(shí)現(xiàn):

public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        println '[MethodTraceMan]: transform()'
        def traceManConfig = project.traceMan
        String output = traceManConfig.output
        if (output == null || output.isEmpty()) {
            traceManConfig.output = project.getBuildDir().getAbsolutePath() + File.separator + "traceman_output"
        }

        if (traceManConfig.open) {
            //讀取配置
            Config traceConfig = initConfig()
            traceConfig.parseTraceConfigFile()


            Collection<TransformInput> inputs = transformInvocation.inputs
            TransformOutputProvider outputProvider = transformInvocation.outputProvider
            if (outputProvider != null) {
                outputProvider.deleteAll()
            }

            //遍歷,分為class文件變量和jar包的遍歷
            inputs.each { TransformInput input ->
                input.directoryInputs.each { DirectoryInput directoryInput ->
                    traceSrcFiles(directoryInput, outputProvider, traceConfig)
                }

                input.jarInputs.each { JarInput jarInput ->
                    traceJarFiles(jarInput, outputProvider, traceConfig)
                }
            }
        }
    }

三洛史、利用ASM進(jìn)行插樁

接下來看看遍歷class文件后如何利用ASM的訪問者模式進(jìn)行插樁:

static void traceSrcFiles(DirectoryInput directoryInput, TransformOutputProvider outputProvider, Config traceConfig) {
        if (directoryInput.file.isDirectory()) {
            directoryInput.file.eachFileRecurse { File file ->
                def name = file.name
                //根據(jù)配置的插樁范圍決定要對(duì)某個(gè)class文件進(jìn)行處理
                if (traceConfig.isNeedTraceClass(name)) {
                    //利用ASM的api對(duì)class文件進(jìn)行訪問
                    ClassReader classReader = new ClassReader(file.bytes)
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor cv = new TraceClassVisitor(Opcodes.ASM5, classWriter, traceConfig)
                    classReader.accept(cv, EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(
                            file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }

        //處理完輸出給下一任務(wù)作為輸入
        def dest = outputProvider.getContentLocation(directoryInput.name,
                directoryInput.contentTypes, directoryInput.scopes,
                Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

可以看到,最終是TraceClassVisitor類里對(duì)class文件進(jìn)行處理的,我們看一下TraceClassVisitor

class TraceClassVisitor(api: Int, cv: ClassVisitor?, var traceConfig: Config) : ClassVisitor(api, cv) {

    private var className: String? = null
    private var isABSClass = false
    private var isBeatClass = false
    private var isConfigTraceClass = false

    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)

        this.className = name
        //抽象方法或者接口
        if (access and Opcodes.ACC_ABSTRACT > 0 || access and Opcodes.ACC_INTERFACE > 0) {
            this.isABSClass = true
        }

        //插樁代碼所屬類
        val resultClassName = name?.replace(".", "/")
        if (resultClassName == traceConfig.mBeatClass) {
            this.isBeatClass = true
        }

        //是否是配置的需要插樁的類
        name?.let { className ->
            isConfigTraceClass = traceConfig.isConfigTraceClass(className)
        }
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        desc: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        val isConstructor = MethodFilter.isConstructor(name)
        //抽象方法、構(gòu)造方法陕习、不是插樁范圍內(nèi)的方法该镣,則不進(jìn)行插樁
        return if (isABSClass || isBeatClass || !isConfigTraceClass || isConstructor) {
            super.visitMethod(access, name, desc, signature, exceptions)
        } else {
            //TraceMethodVisitor中對(duì)方法進(jìn)行插樁
            val mv = cv.visitMethod(access, name, desc, signature, exceptions)
            TraceMethodVisitor(api, mv, access, name, desc, className, traceConfig)
        }
    }
}

再來看看TraceMethodVisitor:

override fun onMethodEnter() {
        super.onMethodEnter()
        //利用ASM在方法進(jìn)入的時(shí)候 通過插入指令調(diào)用耗時(shí)統(tǒng)計(jì)的方法:start()
        mv.visitLdcInsn(generatorMethodName())
        mv.visitMethodInsn(INVOKESTATIC, traceConfig.mBeatClass, "start", "(Ljava/lang/String;)V", false)

    }

    override fun onMethodExit(opcode: Int) {
        //利用ASM在方法進(jìn)入的時(shí)候 通過插入指令調(diào)用耗時(shí)統(tǒng)計(jì)的方法:end()
        mv.visitLdcInsn(generatorMethodName())
        mv.visitMethodInsn(INVOKESTATIC, traceConfig.mBeatClass, "end", "(Ljava/lang/String;)V", false)
    }

這樣省艳,我們就可以在所有配置的在插樁范圍內(nèi)的方法都在方法進(jìn)入的時(shí)候調(diào)用TraceMan.start()方法跋炕,在方法退出的時(shí)候調(diào)用TraceMan.end()方法進(jìn)行耗時(shí)統(tǒng)計(jì)辐烂。而TraceMan這個(gè)類也是可配置的纠修,也就是你可以通過配置決定在方法進(jìn)入和退出的時(shí)候調(diào)用哪個(gè)類的哪個(gè)方法扣草。

至于TraceMan.start()TraceMan.end()是如何實(shí)現(xiàn)對(duì)一個(gè)方法的耗時(shí)統(tǒng)計(jì)德召,如何輸出所有方法的耗時(shí)上岗,可以具體看源碼里TraceMan類的具體實(shí)現(xiàn),這里就不具體展開了敬锐。

4. UI界面展示

????通過上面的方法插樁台夺,以及耗時(shí)數(shù)據(jù)的處理颤介,我們已經(jīng)可以獲取到所有方法的耗時(shí)統(tǒng)計(jì)滚朵,那么為了這個(gè)工具的易用性前域,我們?cè)賮韺?shí)現(xiàn)一個(gè)UI展示界面匿垄,可以讓方法的耗時(shí)數(shù)據(jù)可以實(shí)時(shí)的展示在瀏覽器上椿疗,并且支持耗時(shí)篩選、線程篩選芽狗、方法名搜索等功能童擎。

????我們使用React實(shí)現(xiàn)了一個(gè)UI展示界面顾复,然后在手機(jī)上搭建了一個(gè)服務(wù)器,這樣在瀏覽器上就可以通過地址訪問到這個(gè)UI展示界面萧芙,并且通過socket進(jìn)行數(shù)據(jù)傳輸双揪,我們的插樁代碼產(chǎn)生方法耗時(shí)數(shù)據(jù)渔期,然后React實(shí)現(xiàn)的UI界面接收數(shù)據(jù)疯趟、消費(fèi)數(shù)據(jù)信峻、展示數(shù)據(jù)瓮床。

????UI界面展示這部分的實(shí)現(xiàn)說起來比較瑣碎隘庄,這里就不詳細(xì)展開了峭沦,感興趣的同學(xué)可以看看源碼吼鱼。

該項(xiàng)目的源碼和詳細(xì)的集成以及使用方法菇肃,我在github上維護(hù)了詳細(xì)的文檔琐谤,歡迎提供意見MethodTraceMan

5. 總結(jié)

????以上就是我們?cè)趦?yōu)化流暢度的過程中實(shí)現(xiàn)的一個(gè)協(xié)助我們快速解決問題的工具斗忌,也簡(jiǎn)單分享了相關(guān)的技術(shù)知識(shí),希望對(duì)也為頁面流暢度苦惱的同學(xué)提供一點(diǎn)點(diǎn)想法眶蕉。之后將分享其他的幾個(gè)部分造挽,主要包括:Android View繪制原理饭入、幀率流暢度監(jiān)控谐丢、幀率自動(dòng)化測(cè)試毁欣、流暢度優(yōu)化實(shí)用技巧等等凭疮。當(dāng)然對(duì)于卡頓以及流暢度的監(jiān)控及優(yōu)化還有很多需要做的工作,我們的主要目標(biāo)是希望從監(jiān)控到排查問題工具再到卡頓解決形成一個(gè)閉環(huán)的方案寞肖,讓版本迭代間的流暢度問題做到可控、可發(fā)現(xiàn)右蕊、易解決饶囚,這是我們努力的方向萝风。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末规惰,一起剝皮案震驚了整個(gè)濱河市揩晴,隨后出現(xiàn)的幾起案子文狱,更是在濱河造成了極大的恐慌瞄崇,老刑警劉巖苏研,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件筹燕,死亡現(xiàn)場(chǎng)離奇詭異撒踪,居然都是意外死亡制妄,警方通過查閱死者的電腦和手機(jī)耕捞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門俺抽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來磷斧,“玉大人,你說我怎么就攤上這事孩哑〈滂耄” “怎么了丛晌?”我有些...
    開封第一講書人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵澎蛛,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我毁兆,道長(zhǎng)阴挣,這世上最難降的妖魔是什么茎芭? 我笑而不...
    開封第一講書人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任梅桩,我火速辦了婚禮摘投,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘虹蓄。我一直安慰自己犀呼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開白布薇组。 她就那樣靜靜地躺著外臂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪律胀。 梳的紋絲不亂的頭發(fā)上宋光,一...
    開封第一講書人閱讀 52,337評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音炭菌,去河邊找鬼。 笑死酌毡,一個(gè)胖子當(dāng)著我的面吹牛掰曾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼漱办,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼洞辣!你這毒婦竟也來了著瓶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤窑睁,失蹤者是張志新(化名)和其女友劉穎裳朋,沒想到半個(gè)月后绑莺,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谚殊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年置尔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了差导。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡掀泳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出韭邓,到底是詐尸還是另有隱情,我是刑警寧澤菜拓,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布逗宁,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏导帝。R本人自食惡果不足惜悦陋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一塘幅、第九天 我趴在偏房一處隱蔽的房頂上張望庆亡。 院中可真熱鬧珍促,春花似錦猪叙、人聲如沸犬第。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春厦坛,著一層夾襖步出監(jiān)牢的瞬間五垮,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來泰國打工杜秸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留放仗,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓撬碟,卻偏偏與公主長(zhǎng)得像诞挨,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子呢蛤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容