最通俗易懂的字節(jié)碼插樁實(shí)戰(zhàn)(Gradle + ASM)—— 優(yōu)雅的打印方法執(zhí)行時(shí)間

前言

做項(xiàng)目優(yōu)化時(shí),我們通常會先打印出方法的執(zhí)行時(shí)間,再根據(jù)方法的耗時(shí)情況對其進(jìn)行優(yōu)化拐纱。代碼如下:

public static void main(String[] args) {
        long startTime = System.currentTimeMillis(); 
        //...
        long endTime = System.currentTimeMillis(); 
        System.out.println("程序運(yùn)行時(shí)間: " + (endTime - startTime) + "ms");
    }

如果是一兩個(gè)方法我們手動插入代碼沒有問題铜异,但是整個(gè)項(xiàng)目的方法何其多,都要我們手動去插入的話秸架,估計(jì)能把C揍庄、V兩鍵扣廢掉。那么有沒有一種優(yōu)雅的方式實(shí)現(xiàn)耗時(shí)打印呢东抹?當(dāng)然有的蚂子,這就是今天要介紹的主角 ASM (字節(jié)碼插樁)。

有同學(xué)到這里可能就會問缭黔,我不會寫ASM代碼該怎么辦呢食茎?

悄悄的跟你說,其實(shí)我也不會寫ASM代碼馏谨。

那這會影響到我們的開發(fā)嗎别渔?

當(dāng)然不會了,如果有影響就不會有這篇文章了惧互。

ASM Bytecode Viewer

ASM Bytecode Viewer是一款能 查看字節(jié)碼生成ASM代碼 的插件哎媚,是幫助我們學(xué)習(xí)ASM的利器,剩下就是對ASM的熟悉和使用可以說是so easy喊儡。

  • 在Android Studio中搜索 ASM Bytecode Viewer Support Kotlin 找到并安裝拨与。
  • 代碼右鍵 ASM Bytecode Viewer 便能自動生成ASM插樁代碼。

ASMASM Bytecode Viewer 我在之前的文章 最通俗易懂的字節(jié)碼插樁實(shí)戰(zhàn)(Gradle + ASM)—— 自動埋點(diǎn) 已經(jīng)介紹過了管宵,有不了解的同學(xué)可以翻看一下截珍。具體使用方法我會在后面的編碼階段詳細(xì)介紹。

實(shí)戰(zhàn)

至此我們已經(jīng)做了大量的準(zhǔn)備工作箩朴,現(xiàn)在就正式進(jìn)入實(shí)戰(zhàn)環(huán)節(jié)岗喉。
首先創(chuàng)建一個(gè)module作為插件開發(fā),再刪除掉多余的文件炸庞,然后創(chuàng)建groovy目錄供代碼編寫……
PS:由于gradle插件開發(fā)并不是我們今天的任務(wù)钱床,這里就不過多的展開說明了,具體代碼可在 github 上查看埠居,module目錄結(jié)構(gòu)如下:

1查牌、StatisticPlugin

我們本次編寫的插件,在apply 方法的注冊 MethodTimerTransform滥壕,并讀取 build.gradle 里面配置信息纸颜。

class StatisticPlugin implements Plugin<Project> {

    public static List<MethodTimerEntity> METHOD_TIMER_LIST

    @Override
    void apply(Project project) {
        def android = project.extensions.findByType(AppExtension)
        // 注冊Transform
        android.registerTransform(new MethodTimerTransform())
        // 獲取gradle里面配置的埋點(diǎn)信息
        def statisticExtension = project.extensions.create('statistic', StatisticExtension)
        project.afterEvaluate {
            // 獲取方法計(jì)時(shí)信息,將其保存在METHOD_TIMER_LIST方便調(diào)用
            METHOD_TIMER_LIST = new ArrayList<>()
            def methodTimer = statisticExtension.getMethodTimer()
            if (methodTimer != null) {
                methodTimer.each { Map<String, Object> map ->
                    MethodTimerEntity entity = new MethodTimerEntity()
                    if (map.containsKey("time")) {
                        entity.time = map.get("time")
                    }
                    if (map.containsKey("owner")) {
                        entity.owner = map.get("owner")
                    }
                    METHOD_TIMER_LIST.add(entity)
                }
            }
        }
    }
}
2绎橘、MethodTimerTransform

通過transform 方法的 Collection<TransformInput> inputs 對 .class文件遍歷拿到所有方法胁孙。

class MethodTimerTransform extends Transform {

    ...省略中間非關(guān)鍵代碼,詳細(xì)請到github中查看...

    /**
     *
     * @param context
     * @param inputs 有兩種類型,一種是目錄涮较,一種是 jar 包稠鼻,要分開遍歷
     * @param outputProvider 輸出路徑
     */
    @Override
    void transform(
            @NonNull Context context,
            @NonNull Collection<TransformInput> inputs,
            @NonNull Collection<TransformInput> referencedInputs,
            @Nullable TransformOutputProvider outputProvider,
            boolean isIncremental
    ) throws IOException, TransformException, InterruptedException {
        if (!incremental) {
            //不是增量更新刪除所有的outputProvider
            outputProvider.deleteAll()
        }
        inputs.each { TransformInput input ->
            //遍歷目錄
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectoryInput(directoryInput, outputProvider)
            }
            // 遍歷jar 第三方引入的 class
            input.jarInputs.each { JarInput jarInput ->
                handleJarInput(jarInput, outputProvider)
            }
        }
    }

}
3、MethodTimerClassVisitor

通過visitMethod拿到方法進(jìn)行修改狂票。

class MethodTimerClassVisitor extends ClassVisitor {

    ...省略中間非關(guān)鍵代碼候齿,詳細(xì)請到github中查看...

    /**
     * 掃描類的方法進(jìn)行調(diào)用
     * @param access 修飾符
     * @param name 方法名字
     * @param descriptor 方法簽名
     * @param signature 泛型信息
     * @param exceptions 拋出的異常
     * @return
     */
    @Override
    MethodVisitor visitMethod(int methodAccess, String methodName, String methodDescriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, methodDescriptor, signature, exceptions)
        if ((methodAccess & Opcodes.ACC_INTERFACE) == 0 && "<init>" != methodName && "<clinit>" != methodName) {
            methodVisitor = new MethodTimerAdviceAdapter(api, methodVisitor, methodAccess, methodName, methodDescriptor)
        }
        return methodVisitor
    }

}
4、MethodTimerAdviceAdapter

這里就是我們插入打印方法耗時(shí)的地方了闺属,可以看到代碼沒有很多慌盯。

  • onMethodEnter在方法進(jìn)入時(shí)調(diào)用,我們先在這里插入一個(gè)時(shí)間戳屋剑,標(biāo)記方法開始的時(shí)間润匙。
  • onMethodExit在方法退出前調(diào)用诗眨,這里我們也插入一個(gè)時(shí)間戳唉匾,標(biāo)記方法結(jié)束的時(shí)間。最后把兩個(gè)時(shí)間戳相減得到方法耗時(shí)時(shí)間并打印匠楚。

聽完解釋后是不是覺得非常簡單呢巍膘。

大家最關(guān)心的編(sheng)寫(cheng)ASM代碼,今天它來了芋簿。
  1. 首先我們創(chuàng)建一個(gè)Test類峡懈,先用java代碼來實(shí)現(xiàn)我們的需求与斤,代碼如下:
public class Test {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        String str = "--- I'm the code line ---";
        long endTime = System.currentTimeMillis();
        long time = endTime - startTime;
        if(time > 500){
            System.out.println("程序運(yùn)行時(shí)間: " + time + "ms");
        }
    }

}

細(xì)心的同學(xué)會發(fā)現(xiàn)代碼中有一段分割線字符串 String str = "--- I'm the code line ---";
前面說過方法進(jìn)入時(shí)和方法退出前分別是 onMethodEnteronMethodExit肪康,因此我們通過分割線字符串來判斷代碼插入的時(shí)機(jī)撩穿。
分割線字符之前的代碼在 onMethodEnter 插入雾狈,分割線字符之后的代碼在onMethodExit插入。

  1. 代碼右鍵 ASM Bytecode Viewer 自動生成ASM插樁代碼呻畸,生成代碼如下:
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
            methodVisitor.visitParameter("args", 0);
            methodVisitor.visitCode();
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            methodVisitor.visitVarInsn(LSTORE, 1);
            methodVisitor.visitLdcInsn("--- I'm the code line ---");
            methodVisitor.visitVarInsn(ASTORE, 3);
            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            methodVisitor.visitVarInsn(LSTORE, 4);
            methodVisitor.visitVarInsn(LLOAD, 4);
            methodVisitor.visitVarInsn(LLOAD, 1);
            methodVisitor.visitInsn(LSUB);
            methodVisitor.visitVarInsn(LSTORE, 6);
            methodVisitor.visitVarInsn(LLOAD, 6);
            methodVisitor.visitLdcInsn(new Long(500L));
            methodVisitor.visitInsn(LCMP);
            Label label0 = new Label();
            methodVisitor.visitJumpInsn(IFLE, label0);
            methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
            methodVisitor.visitInsn(DUP);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            methodVisitor.visitLdcInsn("\u7a0b\u5e8f\u8fd0\u884c\u65f6\u95f4\uff1a ");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitVarInsn(LLOAD, 6);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitLdcInsn("ms");
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            methodVisitor.visitLabel(label0);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(4, 8);
            methodVisitor.visitEnd();
        }

我們把 methodVisitor.visitCode(); 之后 methodVisitor.visitLdcInsn("--- I'm the code line ---"); 之前的代碼插入到 onMethodEnter 伤为。把 methodVisitor.visitLdcInsn("--- I'm the code line ---"); 之后 methodVisitor.visitInsn(RETURN); 之前的代碼插入到 onMethodExit 咒循。

最終的 MethodTimerAdviceAdapter 代碼如下:

class MethodTimerAdviceAdapter extends AdviceAdapter {

    int slotIndex

    ...省略中間非關(guān)鍵代碼,詳細(xì)請到github中查看...

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter()
        for (MethodTimerEntity entity : StatisticPlugin.METHOD_TIMER_LIST) {
            if (methodOwner.contains(entity.getOwner())) {
                slotIndex = newLocal(Type.LONG_TYPE)
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
                mv.visitVarInsn(LSTORE, slotIndex)
            }
        }
    }

    @Override
    void onMethodExit(int opcode) {
        for (MethodTimerEntity entity : StatisticPlugin.METHOD_TIMER_LIST) {
            if (methodOwner.contains(entity.getOwner())) {
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
                mv.visitVarInsn(LLOAD, slotIndex)
                mv.visitInsn(LSUB)
                mv.visitVarInsn(LSTORE, slotIndex)
                mv.visitVarInsn(LLOAD, slotIndex)
                mv.visitLdcInsn(new Long(entity.getTime()))
                mv.visitInsn(LCMP)
                Label label0 = new Label()
                mv.visitJumpInsn(IFLE, label0)
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")
                mv.visitTypeInsn(NEW, "java/lang/StringBuilder")
                mv.visitInsn(DUP)
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
                mv.visitLdcInsn(methodOwner + "/" + methodName + " --> execution time : (")
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
                mv.visitVarInsn(LLOAD, slotIndex)
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
                mv.visitLdcInsn("ms)")
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false)
                mv.visitLabel(label0)
            }
        }
        super.onMethodExit(opcode)
    }

}
---這里畫個(gè)重點(diǎn)---

局部變量表(Local Variable Table) 是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)定義的局部變量剑鞍。具體的順序是 this-方法接收的參數(shù)-方法內(nèi)定義的局部變量 昨凡。而我們通過 ASM Bytecode Viewer 生成的ASM代碼是1,2蚁署,3按順序?qū)懰赖谋慵梗晕覀兺ㄟ^ newLocal(type) 來重新獲取壓入的位置 slotIndex 把參數(shù)壓入到局部變量表中。

5光戈、 如何使用哪痰?
5.1、 先打包插件到本地倉庫進(jìn)行引用
5.2久妆、 在項(xiàng)目的根build.gradle加入插件的依賴
    repositories {
        google()
        mavenCentral()
        jcenter()
        maven{
            url uri('repos')
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:$gradle_version"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.meituan.android.walle:plugin:1.1.7'
        // 使用自定義插件
        classpath 'com.example.plugin:statistic:1.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
5.3晌杰、 在app的build.gradle中使用并配置參數(shù)
plugins {
    id 'com.android.application'
    id 'statistic'
}

statistic {
        methodTimer = [
            [
                    // 打印大于time的方法
                    'time'  : 500L,
                    // 需要打印方法的范圍
                    'owner': 'com/example/fragment',
            ],
            [
                    'time'  : 5000L,
                    'owner': 'com/google',
            ]
    ]
}
6、 運(yùn)行項(xiàng)目查看輸出日志
2021-07-20 11:31:51.915 12028-12060/com.example.fragment.project.debug I/System.out: com/example/fragment/library/base/http/SimpleHttp$get$2/invokeSuspend --> execution time : (2066ms)
2021-07-20 11:31:52.565 12028-12028/com.example.fragment.project.debug I/System.out: com/example/fragment/library/common/utils/WanHelper/setTreeList --> execution time : (1184ms)
2021-07-20 11:31:52.565 12028-12028/com.example.fragment.project.debug I/System.out: com/example/fragment/project/model/MainViewModel$getTree$1/invokeSuspend --> execution time : (1184ms)
2021-07-20 11:31:53.768 12028-12028/com.example.fragment.project.debug I/System.out: com/example/fragment/library/common/utils/WanHelper/setTreeList --> execution time : (1186ms)
2021-07-20 11:31:53.768 12028-12028/com.example.fragment.project.debug I/System.out: com/example/fragment/module/system/model/SystemViewModel$getTree$1/invokeSuspend --> execution time : (1186ms)

Thanks

以上就是本篇文章的全部內(nèi)容筷弦,如有問題歡迎指出肋演,我們一起進(jìn)步。
如果喜歡的話希望點(diǎn)個(gè)贊吧烂琴,您的鼓勵(lì)是我前進(jìn)的動力爹殊。
謝謝~~

項(xiàng)目地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市奸绷,隨后出現(xiàn)的幾起案子梗夸,更是在濱河造成了極大的恐慌,老刑警劉巖号醉,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件反症,死亡現(xiàn)場離奇詭異,居然都是意外死亡畔派,警方通過查閱死者的電腦和手機(jī)铅碍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來父虑,“玉大人该酗,你說我怎么就攤上這事∈亢浚” “怎么了呜魄?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長莱衩。 經(jīng)常有香客問我爵嗅,道長,這世上最難降的妖魔是什么笨蚁? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任睹晒,我火速辦了婚禮趟庄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伪很。我一直安慰自己戚啥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布锉试。 她就那樣靜靜地躺著猫十,像睡著了一般。 火紅的嫁衣襯著肌膚如雪呆盖。 梳的紋絲不亂的頭發(fā)上拖云,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機(jī)與錄音应又,去河邊找鬼宙项。 笑死,一個(gè)胖子當(dāng)著我的面吹牛株扛,可吹牛的內(nèi)容都是我干的尤筐。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼席里,長吁一口氣:“原來是場噩夢啊……” “哼叔磷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起奖磁,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎繁疤,沒想到半個(gè)月后咖为,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡稠腊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年躁染,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片架忌。...
    茶點(diǎn)故事閱讀 38,809評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡吞彤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出叹放,到底是詐尸還是另有隱情饰恕,我是刑警寧澤,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布井仰,位于F島的核電站埋嵌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏俱恶。R本人自食惡果不足惜雹嗦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一范舀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧了罪,春花似錦锭环、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吱七,卻和暖如春汽久,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背踊餐。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工景醇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吝岭。 一個(gè)月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓三痰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親窜管。 傳聞我的和親對象是個(gè)殘疾皇子散劫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評論 2 351

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