Asm初探

最近項(xiàng)目中產(chǎn)品要求接入神策埋點(diǎn)氮惯,神策最大的宣傳點(diǎn)應(yīng)該就是所謂無(wú)痕全埋點(diǎn)叮雳。對(duì)于這種"無(wú)痕"或者"無(wú)感知",大部分Android老鳥(niǎo)的第一反應(yīng)肯定就是插樁吧妇汗。幾年前在之前公司做系統(tǒng)開(kāi)發(fā)時(shí)帘不,開(kāi)發(fā)過(guò)一套類(lèi)似Android dumpsys的內(nèi)部dump系統(tǒng),通過(guò)串口命令輸入?yún)?shù)杨箭,獲取當(dāng)前應(yīng)用的相關(guān)指標(biāo)寞焙,當(dāng)時(shí)提供給應(yīng)用開(kāi)發(fā)的sdk就用到了AspectJ,通過(guò)統(tǒng)一插樁的方式告唆,自動(dòng)插入相關(guān)代碼棺弊,減少應(yīng)用開(kāi)發(fā)的接入工作量和出錯(cuò)量晶密。

通過(guò)下載神策的插件并查看源碼,的確和我們預(yù)測(cè)一樣模她,在編譯期通過(guò)插樁方式調(diào)用sdk的相關(guān)方法稻艰,進(jìn)行數(shù)據(jù)采集上報(bào)工作,和我們之前項(xiàng)目使用的AspectJ的原理基本一樣侈净,只不過(guò)它使用的是ASM框架尊勿。

什么是ASMASM是一個(gè)通用的Java字節(jié)碼操作和分析框架畜侦。 它可以用于修改現(xiàn)有類(lèi)或直接以二進(jìn)制形式動(dòng)態(tài)生成類(lèi)元扔。 ASM提供了一些常見(jiàn)的字節(jié)碼轉(zhuǎn)換和分析算法,可以從中構(gòu)建自定義復(fù)雜轉(zhuǎn)換和代碼分析工具旋膳。 ASM提供與其他Java字節(jié)碼框架類(lèi)似的功能澎语,但專(zhuān)注于性能。 因?yàn)樗脑O(shè)計(jì)和實(shí)現(xiàn)盡可能小而且快验懊,所以它非常適合在動(dòng)態(tài)系統(tǒng)中使用(但當(dāng)然也可以以靜態(tài)方式使用擅羞,例如在編譯器中)

都是AOP框架,ASM更加注重的是性能义图,所以成為了各大公司涉及插樁需求項(xiàng)目的首選框架减俏。

首先我們來(lái)看一張經(jīng)典的Android編譯流程圖,ASM的原理是修改字節(jié)碼碱工,也就是修改編譯生成的class文件娃承,所以對(duì)應(yīng)Android編譯流程中的切入時(shí)機(jī)就是.classFiles和dex之間:

本文不做對(duì)ASM框架的深入分析,僅僅通過(guò)一個(gè)例子介紹下整一個(gè)插樁流程:

  • 首先在Android Studio中創(chuàng)建一個(gè)項(xiàng)目怕篷,刪除app module历筝,新建一個(gè)Java library:

    android_compile

  • 建立好之后,刪除包名匙头,只保留Java目錄漫谷,便于執(zhí)行命令仔雷。接著在build.gradle中添加ASM的依賴(lài):

apply plugin: 'java-library'
apply plugin: 'kotlin'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    // asm遠(yuǎn)程依賴(lài)
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"
  • 此時(shí)蹂析,前期準(zhǔn)備工作就完成了。首先說(shuō)明一下本案例的目標(biāo):在一個(gè)testAsm的方法中插入一行代碼System.out.println("Hello Asm");碟婆,由于需要通過(guò)命令編譯电抚,為了方便,我們的target目標(biāo)文件使用Java編寫(xiě)竖共,流程代碼使用Kotlin編寫(xiě)蝙叛。

  • 1.創(chuàng)建一個(gè)目標(biāo)文件Target.java和一個(gè)標(biāo)記需要插樁的注解InjectHello.java

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@interface InjectHello {
}

public class Target {
    @InjectHello
    private void testAsm() {

    }
}
  • 此時(shí)我們先在Target.java所在目錄下打開(kāi)命令行,執(zhí)行javac命令公给,生成對(duì)應(yīng)的Target.class文件:

接下來(lái)我們正式開(kāi)始編寫(xiě)流程代碼
1.創(chuàng)建一個(gè)入口類(lèi)和main方法:

  • 1.讀取剛剛編譯生成的class文件
  • 2.創(chuàng)建一個(gè)ClassReader實(shí)例借帘,讀取文件流
  • 3.創(chuàng)建一個(gè)ClassWriter實(shí)例蜘渣,用于寫(xiě)文件
  • 4.調(diào)用reader.accept進(jìn)行文件訪(fǎng)問(wèn)和修改
  • 5.將修改后的字節(jié)流寫(xiě)入目標(biāo)文件
  • 6.完成對(duì)class文件的修改寫(xiě)入
class Main {
    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            val targetFile = "${System.getProperty("user.dir")}/example/src/main/java/Target.class"
            println("目標(biāo)文件:$targetFile")
            val startTime = System.currentTimeMillis()
            val fis = FileInputStream(targetFile)
            val reader = ClassReader(fis)// 讀取文件
            val writer = ClassWriter(ClassWriter.COMPUTE_FRAMES)// 寫(xiě)文件
            println("開(kāi)始修改文件")
            // HelloClassVisitor為自定義的類(lèi)訪(fǎng)問(wèn)器
            reader.accept(HelloClassVisitor(writer), ClassReader.EXPAND_FRAMES)// 訪(fǎng)問(wèn)class文件并修改
            val bytes = writer.toByteArray()// 獲取修改后的流
            val fos = FileOutputStream(targetFile)
            println("寫(xiě)入文件:$targetFile")
            fos.write(bytes)// 將修改后字節(jié)流寫(xiě)入文件
            fos.flush()
            println("關(guān)閉文件流")
            fis.close()
            fos.close()
            println("本次插樁耗時(shí):${System.currentTimeMillis() - startTime} ms")
        }
    }
}

2.創(chuàng)建一個(gè)自定義class訪(fǎng)問(wèn)器HelloClassVisitor:

class HelloClassVisitor(visitor: ClassVisitor) : ClassVisitor(Opcodes.ASM7, visitor) {

    override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
        val visitMethod = super.visitMethod(access, name, descriptor, signature, exceptions)
        println("訪(fǎng)問(wèn)方法:$name, 描述符:$descriptor")
        return HelloMethodVisitor(visitMethod, access, name, descriptor)
    }
}

3.我們先創(chuàng)建一個(gè)自定義的Method方法訪(fǎng)問(wèn)器HelloMethodVisitor,這里使用AdviceAdapter:

class HelloMethodVisitor(methodVisitor: MethodVisitor, access: Int, name: String?, descriptor: String?) :
        AdviceAdapter(Opcodes.ASM7, methodVisitor, access, name, descriptor) {
    override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
        return super.visitAnnotation(descriptor, visible)
    }

    override fun onMethodEnter() {
        super.onMethodEnter()
    }
}

4.這時(shí)候我們先暫停肺然,看下這兩個(gè)重寫(xiě)方法蔫缸,從方法名我們可以看出,一個(gè)是訪(fǎng)問(wèn)方法注解時(shí)的回調(diào)际起,一個(gè)是進(jìn)入方法時(shí)的回調(diào)拾碌。還記的我們的目標(biāo)嗎,就是往標(biāo)記了注解的方法中插入一行打印代碼街望,所以這兩個(gè)方法就是我們需要實(shí)現(xiàn)自定義邏輯的地方校翔。

5.我們先暫停繼續(xù)編寫(xiě)代碼,在AS中搜索安裝一個(gè)插件ASM Bytecode Outline便于查看字節(jié)碼和ASM代碼文件:

6.我們先編寫(xiě)一個(gè)目標(biāo)文件灾前,也就是我們希望插入打印后的文件Target2.java:

// 我們期望插入打印后的代碼:
public class Target2 {
    @InjectHello
    private void testAsm() {
        System.out.println("Hello Asm");
    }
}

7.選擇我們的Target2防症,右鍵點(diǎn)擊Show Bytecode outline使用插件查看對(duì)應(yīng)的字節(jié)碼:


bytecode.jpg

asmcode.jpg

8.我們把修改后的testAsm方法部分拷貝出來(lái)如下,這就是我們最終期望的產(chǎn)物:

  // access flags 0x2
  private testAsm()V
  @LInjectHello;() // invisible
   L0
    LINENUMBER 8 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "Hello Asm"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 9 L1
    RETURN
   L2
    LOCALVARIABLE this LTarget2; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

9.現(xiàn)在我們對(duì)照這這個(gè)目標(biāo)class文件哎甲,開(kāi)始實(shí)現(xiàn)我們的HelloMethodVisitor:

class HelloMethodVisitor(methodVisitor: MethodVisitor, access: Int, name: String?, descriptor: String?) :
        AdviceAdapter(Opcodes.ASM7, methodVisitor, access, name, descriptor) {
    private var isInjectHello = false

    override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
        println("訪(fǎng)問(wèn)方法:$name -注解:$descriptor")
        if (descriptor!! == Type.getDescriptor(InjectHello::class.java)) {
            println("標(biāo)記了注解:$descriptor, 需要處理")
            isInjectHello = true
        }
        return super.visitAnnotation(descriptor, visible)
    }

    /**
     * ==========================>
     * Java方法:
     *       private void testAsm() {
     *            System.out.println("Hello Asm");// 準(zhǔn)備插入的代碼
     *       }
     * ==========================>
     * 對(duì)應(yīng)字節(jié)碼:
     *       // access flags 0x2
     *       private testAsm()V
     *       L0
     *       LINENUMBER 7 L0
     *       GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
     *       LDC "Hello Asm"
     *       INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
     *       L1
     *       LINENUMBER 8 L1
     *       RETURN
     *       L2
     *       LOCALVARIABLE this LTarget2; L0 L2 0
     *       MAXSTACK = 2
     *       MAXLOCALS = 1
     * 對(duì)應(yīng)ASM代碼:
     *      mv = cw.visitMethod(ACC_PRIVATE, "testAsm", "()V", null, null);
     *      {
     *      av0 = mv.visitAnnotation("LInjectHello;", false);
     *      av0.visitEnd();
     *      }
     *      mv.visitCode();
     *      Label l0 = new Label();
     *      mv.visitLabel(l0);
     *      mv.visitLineNumber(8, l0);
     *      mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
     *      mv.visitLdcInsn("Hello Asm");
     *      mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
     *      Label l1 = new Label();
     *      mv.visitLabel(l1);
     *      mv.visitLineNumber(9, l1);
     *      mv.visitInsn(RETURN);
     *      Label l2 = new Label();
     *      mv.visitLabel(l2);
     *      mv.visitLocalVariable("this", "LTarget2;", null, l0, l2, 0);
     *      mv.visitMaxs(2, 1);
     *      mv.visitEnd();
     */ 
    override fun onMethodEnter() {
        super.onMethodEnter()
        // 此處為方法開(kāi)頭
        if (isInjectHello) {
            println("開(kāi)始插入代碼: [ System.out.println(\"Hello Asm\"); ]")
            // **********方法1************
            // 對(duì)應(yīng)->GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
            getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"))
            // 對(duì)應(yīng)->LDC "Hello Asm"
            visitLdcInsn("Hello Asm")
            // 對(duì)應(yīng)->INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
            invokeVirtual(Type.getType("Ljava/io/PrintStream;"), Method("println", "(Ljava/lang/String;)V"))
            // **********方法2************
            // 直接從ASMified中復(fù)制代碼告希,和方法1是等價(jià)的:
            // mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            // mv.visitLdcInsn("Hello Asm");
            //mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        }
    }

}

10.好了,現(xiàn)在我們執(zhí)行下Main.Companion#main的入口方法烧给,發(fā)現(xiàn)控制臺(tái)打印出如下:

目標(biāo)文件:C:\Users\seagazer\Desktop\Asm/example/src/main/java/Target.class
開(kāi)始修改文件
訪(fǎng)問(wèn)方法:<init>, 描述符:()V
訪(fǎng)問(wèn)方法:testAsm, 描述符:()V
訪(fǎng)問(wèn)方法:testAsm -注解:LInjectHello;
標(biāo)記了注解:LInjectHello;, 需要處理
開(kāi)始插入代碼: [ System.out.println("Hello Asm"); ]
寫(xiě)入文件:C:\Users\seagazer\Desktop\Asm/example/src/main/java/Target.class
關(guān)閉文件流
本次插樁耗時(shí):36 ms

Process finished with exit code 0

11.最后燕偶,讓我們?cè)倏聪?code>Target.class文件內(nèi)容,對(duì)比上面的Target.class文件础嫡,說(shuō)明我們成功插入了System.out.println("Hello Asm");

好了指么,到此我們已經(jīng)達(dá)成了我們的目標(biāo),最后附上完整的代碼:https://github.com/seagazer/asm

最后總結(jié)一下榴鼎,目前不論是ASM伯诬,AspectJ或者其他AOP框架,在Android上的主要應(yīng)用都是在插件transform中對(duì)代碼進(jìn)行修改巫财,比如統(tǒng)一的插樁對(duì)方法進(jìn)行耗時(shí)統(tǒng)計(jì)盗似,點(diǎn)擊事件的防抖處理等等,可以減少對(duì)原有代碼的侵入性平项,提供統(tǒng)一的管理赫舒,降低修改工作量和出錯(cuò)概率。
在使用第三方框架的時(shí)候闽瓢,要多些思考接癌,如果我們自己實(shí)現(xiàn)政模,會(huì)怎么去設(shè)計(jì)方案竟宋,或者選擇使用什么方式或者框架去實(shí)現(xiàn)盈蛮,而不是簡(jiǎn)單的引入,完成業(yè)務(wù)需求娱挨,這樣删壮,才能保證自身能力的持續(xù)提升撞芍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末方庭,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子有咨,更是在濱河造成了極大的恐慌咏闪,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摔吏,死亡現(xiàn)場(chǎng)離奇詭異鸽嫂,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)征讲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)据某,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人诗箍,你說(shuō)我怎么就攤上這事癣籽。” “怎么了滤祖?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵筷狼,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我匠童,道長(zhǎng)埂材,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任汤求,我火速辦了婚禮俏险,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扬绪。我一直安慰自己竖独,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布挤牛。 她就那樣靜靜地躺著莹痢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪墓赴。 梳的紋絲不亂的頭發(fā)上竞膳,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音竣蹦,去河邊找鬼顶猜。 笑死,一個(gè)胖子當(dāng)著我的面吹牛痘括,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼纲菌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼挠日!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起翰舌,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嚣潜,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后椅贱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體懂算,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年庇麦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了计技。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡山橄,死狀恐怖垮媒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情航棱,我是刑警寧澤睡雇,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站饮醇,受9級(jí)特大地震影響它抱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜朴艰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一抗愁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧呵晚,春花似錦蜘腌、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至金矛,卻和暖如春芯急,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驶俊。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工娶耍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饼酿。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓榕酒,卻偏偏與公主長(zhǎng)得像胚膊,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子想鹰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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

  • 背景 隨著項(xiàng)目中對(duì) APM (Application Performance Management) 越來(lái)越關(guān)注紊婉,...
    Android高級(jí)工程師閱讀 4,078評(píng)論 2 7
  • ASM 背景 寫(xiě)文章特別喜歡寫(xiě)背景,感覺(jué)如果不寫(xiě)背景就沒(méi)法回憶出來(lái)當(dāng)時(shí)為什么要搞這個(gè)東西辑舷。好了喻犁,因?yàn)橹皡⑴c的一個(gè)...
    火火說(shuō)技術(shù)閱讀 3,182評(píng)論 0 1
  • 一、ASM 的優(yōu)勢(shì)和逆勢(shì) 使用 ASM 操作字節(jié)碼的優(yōu)勢(shì)與逆勢(shì)都 比較明顯何缓,其分別如下所示肢础。 1、ASM 的優(yōu)勢(shì) ...
    waiwaaa閱讀 360評(píng)論 0 1
  • *本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布 前言 第一次看到插樁碌廓,是在Android開(kāi)發(fā)...
    帶心情去旅行閱讀 45,232評(píng)論 25 218
  • 1 什么是插樁传轰? 聽(tīng)到關(guān)于“插樁”的詞語(yǔ),第一眼覺(jué)得會(huì)很高深氓皱,那到底什么是插樁呢路召?用通俗的話(huà)來(lái)講,插樁就是將一段代...
    楓葉1234閱讀 2,652評(píng)論 1 7