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

????我們產(chǎn)線的主流程頁(yè)面中有幾個(gè)比較復(fù)雜的頁(yè)面在版本迭代中流暢度頻繁出現(xiàn)反復(fù),經(jīng)常由于開發(fā)的不注意導(dǎo)致變卡,主要是對(duì)流暢度缺少必要的監(jiān)控和可持續(xù)的優(yōu)化手段,這個(gè)系列是對(duì)上半年實(shí)踐App流暢度監(jiān)控、優(yōu)化過(guò)程中的一點(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的繪制過(guò)程解析

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

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

    根據(jù)上面的原理,設(shè)計(jì)一個(gè)顯示實(shí)時(shí)幀率的工具,可以有效的在開發(fā)階段發(fā)現(xiàn)問(wè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)化頁(yè)面流暢度過(guò)程中自己實(shí)現(xiàn)的一個(gè)方便快速排查高耗時(shí)方法的工具:MethodTraceMan,畢竟保持主流程流暢,避免在主流程執(zhí)行高耗時(shí)方法永遠(yuǎn)是優(yōu)化卡頓最直接的手段,只要我們能快速方便的排查到高耗時(shí)的方法,就可以做針對(duì)性優(yōu)化。

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

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

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

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

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

????要實(shí)現(xiàn)這樣一個(gè)工具,首先想到的就是通過(guò)插樁技術(shù)來(lái)實(shí)現(xiàn),在編譯過(guò)程中對(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界面來(lái)展示這些數(shù)據(jù),并實(shí)現(xiàn)耗時(shí)篩選、線程篩選、方法名搜索等功能,這樣我們就可以快速的找到主線程高耗時(shí)的方法進(jìn)行針對(duì)性的優(yōu)化了。

1. 效果預(yù)覽

我們先來(lái)看下最終實(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é)碼文件的操作。那么我們來(lái)對(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)來(lái)說(shuō),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ì)來(lái)說(shuō)比AspectJ上手難度要高,需要對(duì)Java字節(jié)碼有一定了解,不過(guò)ASM為我們提供了訪問(wèn)者模式來(lái)訪問(wèn)字節(jié)碼文件,這種模式下可以比較簡(jiǎn)單的做一些字節(jié)碼操作,實(shí)現(xiàn)一些功能。同時(shí)ASM可以精確的只注入我們想要注入的代碼,不會(huì)額外生成一些包裝代碼,所以性能上影響比較微小。

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

java字節(jié)碼

我們都知道在java文件的通過(guò)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;
    }
}

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

test.class

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

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

add方法

可以看到包括三部分:

  1. Code: 這里部分就是方法里的JVM指令操作碼,也是最重要的一部分,因?yàn)槲覀兎椒ɡ锏倪壿媽?shí)際上就是一條一條的指令操作碼來(lái)完成的。這里可以看到我們的add方法是通過(guò)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ù)棧+本地變量表+常量池來(lái)執(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可以通過(guò)操作指令碼來(lái)生成字節(jié)碼或者插樁,當(dāng)你可以利用ASM來(lái)接觸到字節(jié)碼,并且可以利用ASM的api來(lái)操控字節(jié)碼時(shí),就有很大的自由度來(lái)進(jìn)行各種字節(jié)碼的生成、修改、操作等等,也就能產(chǎn)生很強(qiáng)大的功能。

三、Gradle plugin + Transform

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

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

????幸虧gradle還為我們提供了另一種干預(yù)編譯轉(zhuǎn)換過(guò)程的方法:Transform.其實(shí)我們稍微翻一下gradle編譯過(guò)程的源碼,就會(huì)發(fā)現(xiàn)一些我們熟知的功能都是通過(guò)Transform來(lái)實(shí)現(xiàn)的。還有一點(diǎn),就是關(guān)于混淆的問(wèn)題,上面我們說(shuō)了如果通過(guò)hook transformClassesWithDex任務(wù)的方式來(lái)實(shí)現(xiàn)插樁,開啟混淆的情況下會(huì)出現(xiàn)問(wèn)題,那么利用Transform的方式會(huì)不會(huì)有混淆的問(wèn)題呢?下面我們從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);
    }

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

public void createPostCompilationTasks(@NonNull final VariantScope variantScope) {
       、、、、、、
    TransformManager transformManager = variantScope.getTransformManager();
      、、、、、
     // ----- External Transforms 這個(gè)就是我們自定義注冊(cè)進(jìn)來(lái)的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í)行的,也就是說(shuō)我們利用自定義Transform的方式對(duì)代碼進(jìn)行插樁是不受混淆影響的。

所以我們最終確定的方案就是 Gradle plugin + Transform +ASM的技術(shù)方案。下面我們正式說(shuō)說(shuō)利用該技術(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ì)說(shuō)了,可以網(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ù)自己的需要來(lái)配置。

接下來(lái)我們看一下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)行插樁

接下來(lái)看看遍歷class文件后如何利用ASM的訪問(wèn)者模式進(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)行訪問(wè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)
        }
    }
}

再來(lái)看看TraceMethodVisitor:

override fun onMethodEnter() {
        super.onMethodEnter()
        //利用ASM在方法進(jìn)入的時(shí)候 通過(guò)插入指令調(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í)候 通過(guò)插入指令調(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è)類也是可配置的,也就是你可以通過(guò)配置決定在方法進(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界面展示

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

????我們使用React實(shí)現(xiàn)了一個(gè)UI展示界面,然后在手機(jī)上搭建了一個(gè)服務(wù)器,這樣在瀏覽器上就可以通過(guò)地址訪問(wèn)到這個(gè)UI展示界面,并且通過(guò)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)說(shuō)起來(lái)比較瑣碎,這里就不詳細(xì)展開了,感興趣的同學(xué)可以看看源碼。

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

5. 總結(jié)

????以上就是我們?cè)趦?yōu)化流暢度的過(guò)程中實(shí)現(xiàn)的一個(gè)協(xié)助我們快速解決問(wèn)題的工具,也簡(jiǎn)單分享了相關(guān)的技術(shù)知識(shí),希望對(duì)也為頁(yè)面流暢度苦惱的同學(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)控到排查問(wèn)題工具再到卡頓解決形成一個(gè)閉環(huán)的方案,讓版本迭代間的流暢度問(wèn)題做到可控、可發(fā)現(xiàn)、易解決,這是我們努力的方向。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評(píng)論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,067評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,184評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,582評(píng)論 1 325
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,794評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,343評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,096評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,291評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,513評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評(píng)論 1 291
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,026評(píng)論 3 396
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,253評(píng)論 2 375

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