[轉]自定義Gradle插件 + ASM實現字節碼插樁

1. 什么是插樁?

用通俗的話來講,插樁就是將一段代碼通過某種策略插入到另一段代碼,或替換另一段代碼。這里的代碼可以分為源碼字節碼,而我們所說的插樁一般指字節碼插樁
下圖是Android開發者常見的一張圖,我們編寫的源碼(.java)通過javac編譯成字節碼(.class),然后通過dx/d8編譯成dex文件(.dex)。

我們下面要講的插樁,就是在.class轉為.dex之前,修改.class文件從而達到修改或替換代碼的目的。
那有人肯定會有這樣的疑問?既然插樁是插入或替換代碼,那為何我不自己直接插入或替換呢?為何還要用這么“復雜”的工具?別著急,第二個問題將會給你答案。

2. 插樁的應用場景有哪些?

技術是服務于業務的,一個無法推進業務進步的技術并不值得我們學習。在上面,我們對插樁的理解是:插入,替換代碼。那么,結合這個核心主線我們來挖掘插樁能被應用的場景有哪些?

  • 代碼插入

我們所熟悉的ButterKnife,Dagger這些常用的框架,也是在編譯期間生成了代碼,簡化了程序員的操作。假設有這么一個需求,要監控某些或者所有方法的執行耗時?你會怎么做呢?如果你監控的方法只有十幾個或者幾十個,那么也許通過程序員自身的編碼就能輕松解決;但是如果監控的方法達到百千甚至萬級別,你還通過編碼來解決?那么程序員存在的價值在哪里?面對這樣的重復勞動問題,最先想到的就應該是自動化,也就是我們今天所講的插樁。通過插樁,我們掃描每一個class文件,并針對特定規則進行字節碼修改從而達到監控每個方法耗時的目的。關于如何實現這樣的需求,后面我會詳細講述。

  • 代碼替換

如果遇到這么一個需求,需要將項目中所有使用某個方法(如Dialog.show())的地方替換成自己包裝的方法(MyDialog.show()),那么你該如何解決呢?有人會說,直接使用快捷鍵就能全局替換。那么有兩個問題

  1. 如果有其他類定義了show()方法,并被調用了,直接使用快捷鍵是否會被錯誤替換?
  2. 如果其他引用包使用了該方法,你怎么替換呢?

沒關系,插樁同樣可以解決你的問題。
綜合上面所說的兩點,其實很多業務場景都使用了插樁技術,比如無痕埋點,性能監控等。

3. 掌握插樁應該具備的基礎知識有哪些?

上面講了插樁的應用場景,是否現在想躍躍欲試呢?別著急,想掌握好插樁技術,練就扎實的插樁功底,我們是需要具備一些基礎知識的。

  • 熟練掌握字節碼相關技術。可參考 一文讓你明白Java字節碼

  • Gradle自定義插件,直接參考官網 Writing Custom plugins

  • 如果你想運用在Android項目中,那么還需要掌握Transform API,
    這是android在將class轉成dex之前給我們預留的一個接口,在該接口中我們可以通過插件形式來修改class文件。

  • 字節碼修改工具。如AspectJ,ASM,javasisst。這里我推薦使用ASM,關于ASM相關知識,在下一章我給大家簡單介紹。同樣大家可以參考 Asm官方文檔

  • groovy語言基礎
    如果你具備了上面5塊知識,那么恭喜你,會很順利的完成字節碼插樁技術了。下面,我通過實戰一個很簡單的例子,帶領大家一起領略插樁的風采。

4. 使用ASM進行字節碼插樁

4.1 什么是ASM?

ASM是生成和轉換已編譯的Java類工具,就是我們插樁需要使用的工具。

4.2 兩種API?

ASM提供了兩種API來生成和轉換已編譯類

  • 一個是核心API,以基于事件形式來表示類
  • 一個是樹API,以基于對象形式來表示類

4.3 基于事件形式

我們通過上面的基礎知識,了解到類的結構,類包含字段,方法,指令等;基于事件的API把類看作是一系列事件來表示,每一個類的事件表示一個類的元素。類似解析XML的SAX

4.4 基于對象形式

基于對象的API將類表示成一棵對象樹,每個對象表示類的一部分。類似解析XML的DOM

4.5 優缺點比較

事件形式 對象形式
內存占用
實現難度

通過上面表格,我們清楚的了解到:

  • 事件API內存占用少于對象API,因為事件API不需要在內存中創建和存儲對象樹
  • 事件API實現難度比對象API大,因為事件API在任意時刻類中只有一個元素可使用,但是對象API能獲得整個類。

那么接下來,我們就通過比較容易實現的對象API入手,一起完成上面的需求。

我們Android的構建工具是Gradle,因此我們結合transform和Gradle插件方式來完成該需求,接下來我們來看看gradle官方提供的3種插件形式

4.6 Gradle插件的3種形式

插件形式 說明
Build script 直接在build script中寫插件代碼,不可復用
buildSrc 獨立項目結構,只能在本構建體系中復用,無法提供給其他項目
Standalone 獨立項目結構,發布到倉庫,可以復用

由于我們是demo,并不需要共享給其他項目,因此采用buildSrc方式即可,但是正常項目中都采用Standalone形式。

5. 實踐

5.1 目標

實現自定義gradle插件,通過ASM實現在MainActivity的onCreate中插入Log打印語句

5.2 自定義Gradle插件實現

gradle實現自定義插件一般有三種方式,考慮到靈活性,我們選擇第三種Standalone方式實現自定義插件

5.2.1 創建module

創建一個新的module,刪除不必要的文件,只留下build.gradle, src/main這兩個文件和文件夾


5.2.2 創建目錄和配置文件

  • 創建代碼目錄
    src/main下創建 java和groovy目錄以及 resources/META-INF/gradle-plugins目錄,META-INF和gradle-plugins均為package

  • 創建配置文件
    resources/META-INF/gradle-plugins下創建xxxx.properties文件, xxxx為apply plugin時用到的名字,文件中內容為implementation-class = 插件的完整路徑,這里可以先空著,后面創建了插件后再填入

5.2.3 build.gradle編寫

build.gradle中引入groovy和maven插件,然后引入gradle插件,asm以及gradle api和庫,最后進行編譯

注意:此處的定義的group + 此module名(或者archivesBaseName定義) + version 即是插件的依賴地址,下方倉庫設置中的pom.groupId, pom.artifactId, pom.version是同樣的效果,同時設置會生成兩個不同地址

apply plugin: 'groovy'
apply plugin: 'maven'
 
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
 
    implementation gradleApi()
    implementation localGroovy()
 
    implementation 'com.android.tools.build:gradle:3.5.3'
}
 
group='danny.lifecycle.plugin'
version='1.0.0'
 
uploadArchives {
    repositories {
        mavenDeployer {
//            pom.groupId = 'com.xxx.plugin.gradle'   //groupId
//            pom.artifactId = 'xxx'  //artifactId
//            pom.version = '1.0.2' //版本號
            //本地的Maven地址設置
            repository(url: uri('../asm_lifecycle_repo'))
        }
    }
}

5.2.4 編寫插件

編譯完成后在groovy中添加package,創建實現Plugin<>接口的類文件,先使用java文件便于包和類的引入,具體實現如下,編寫完成后將.java后綴改成.groovy,這就是自定義插件的入口

public class LifeCyclePlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
    
    }
}

同級目錄下創建繼承Transform的類文件,同樣使用java文件導入包和引用類,具體實現如下,編寫完成后后綴改為.groovy,Transform的作用是可以在項目構建過程中.class文件轉換成.dex文件期間獲取到.class文件進行讀取修改操作

public class LifeCycleTransform extends Transform {
    @Override
    public String getName() {
        return "LifeCycleTransform";
    }
 
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }
 
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.PROJECT_ONLY;
    }
 
    @Override
    public boolean isIncremental() {
        return false;
    }
 
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    
    }
}

getName():這里可以指定此task的名字,不過最終名字需要做一些拼接,transformClassesWith名字ForDebug/Release

getInputTypes():處理的文件類型,此處為class文件

getScopes():作用范圍,此處為只處理當前項目文件

isIncremental():是否支持增量編譯

transform(TransformInvocation transformInvocation):主要處理文件和jar包的方法

編寫完transform后,在plugin中進行注冊,AppExtension就是指的build.gradle中的android{}閉包

public class LifeCyclePlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        def extension = project.extensions.getByType(AppExtension)
 
        LifeCycleTransform transform = new LifeCycleTransform();
        extension.registerTransform(transform)
    }
}

5.2.5 生成倉庫

此時在gradle任務中生成了uploadArchives任務,雙擊后即可生成插件倉庫

5.2.6 自定義gradle插件引入和使用

在項目的根目錄build.gradle中添加倉庫路徑,然后在dependencies中添加classpath,引入插件

buildscript {
    repositories {
        google()
        jcenter()
        maven {
            url uri('./asm_lifecycle_repo')
        }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        classpath 'danny.lifecycle.plugin:asm_lifecycle_plugin3:1.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

接著在要使用的module的build.gradle中通過apply plugin引入插件使用

apply plugin: 'com.android.application'
apply plugin: 'danny.asm.lifecycle'

這樣,就完成了自定義gradle插件的編寫和使用

5.3. ASM實現字節碼插樁

5.3.1 引入ASM

完成自定義gradle插件后,在插件的build.gradle中添加ASM依賴

apply plugin: 'groovy'
apply plugin: 'maven'
 
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
 
    implementation gradleApi()
    implementation localGroovy()
 
    implementation 'com.android.tools.build:gradle:3.5.3'
 
    //ASM相關依賴
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
}
 
group='danny.lifecycle.plugin'
version='1.0.0'
 
uploadArchives {
    repositories {
        mavenDeployer {
//            pom.groupId = 'com.xxx.plugin.gradle'   //groupId
//            pom.artifactId = 'xxx'  //artifactId
//            pom.version = '1.0.2' //版本號
            //本地的Maven地址設置
            repository(url: uri('../asm_lifecycle_repo'))
        }
    }
}

5.3.2 創建Visitor

在自定義插件module中的src/main/java下添加package,然后創建繼承ClassVisitor的類文件,實現visit,visitMethod方法

public class LifecycleClassVisitor extends ClassVisitor {
    private String className;
    private String superName;
 
    public LifecycleClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }
 
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        this.superName = superName;
    }
 
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
        if (className.equals("com/example/lifecycledemo/MainActivity") && superName.equals("androidx/appcompat/app/AppCompatActivity")) {
            if (name.startsWith("onCreate")) {
                return new LifeCycleMethodVisitor(Opcodes.ASM5, methodVisitor, access, name, descriptor, className, superName);
            }
        }
        return methodVisitor;
    }
 
    @Override
    public void visitEnd() {
        super.visitEnd();
        System.out.println("ClassVisitor visitEnd()");
    }
}

在visit方法中獲取類名,超類名,在visitMethod方法中篩選類名MainActivity,超類AppCompatActivity的文件,接著篩選onCreate方法,最后返回一個繼承自 AdviceAdater的類

public class LifeCycleMethodVisitor extends AdviceAdapter {
    private String className;
    private String methodName;
 
    protected LifeCycleMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superName) {
        super(api, methodVisitor, access, name, descriptor);
        this.className = className;
        this.superName = superName;
        System.out.println("MethodVisitor Constructor");
    }
 
    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        System.out.println("MethodVisitor visitCode========");
 
        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn(className + "---->" + methodName);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }
 
    @Override
    protected void onMethodExit(int opcode) {
        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn("this is end");
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
        super.onMethodExit(opcode);
    }
}

在繼承了AdviceAdapter的類中,實現onMethodEnteronMethodExit方法,對應onCreate方法的開始和結束節點,在這兩個節點通過MethodVisitor的一系列api調用插入Log打印語句

visitLdcInsn(final Object value)對應LDC指令

visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor, final boolean isInterface)是訪問方法指令,此處用到的五個參數

  • opcode: 對應字節碼指令操作碼,此處傳入了 調用類方法的指令

  • owner: 方法所在包

  • name: 方法名

  • descriptor: 方法描述符,前一個Ljava/lang/String:Ljava/lang/String指明方法有兩個String類型參數,最后的 I 表示方法返回int類型

  • isInterface: 是否是接口類的實現方法

visitInsn()對應空操作數指令,比如POP, DUP

注:此處也可直接繼承MethodVisitor,實現visitCode方法插入代碼,但要實現在方法結束前插入代碼需要另外實現visitInsn(int opcode)方法,根據opcode == RETURN來判斷指令執行到方法末尾了,插入代碼后再調用super方法即可

5.3.3 讀取class文件數據

在自定義繼承Transform類的transform方法中進行操作,通過getInputs()獲取輸入的class文件和jar包的路徑,outputProvider管理輸出路徑,接著遍歷inputs,directoryInputs獲取到class文件的路徑集合,再次遍歷,篩選出class文件,通過ClassReader進行讀取,ClassWriter進行寫入,將classWriter傳入自定義的ClassVisitor中,接著調用classReader的accept方法正式對class文件進行讀取并調用classVisitor中的方法,比如visit(),visitMethod()等,我們在里面對MainActivity的onCreate方法中加入了一行Log打印語句,然后通過classWriter的toByteArray()方法輸出修改后的class文件btye數組,覆蓋掉原來的class文件,最后將修改后的class文件目錄整個copy新的目錄下,這個新目錄是根據輸入的內容,作用范圍等信息生成的,供下一個Task使用

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        Collection<TransformInput> inputs = transformInvocation.getInputs()
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
 
        inputs.each {TransformInput input ->
            input.directoryInputs.each {DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                    dir.traverse (type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        println("find class: " + file.name)
                        //對class文件進行讀取
                        ClassReader classReader = new ClassReader(file.bytes)
                        //對class文件的寫入
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        //訪問class文件相應的內容,解析到某一個結構就會通知到classVisitor相應的方法
                        println("before visit")
                        ClassVisitor visitor = new LifecycleClassVisitor(classWriter)
                        println("after visit")
                        //依次調用ClassVisitor接口的各個方法
                        classReader.accept(visitor, ClassReader.EXPAND_FRAMES)
                        println("after accept")
                        //toByteArray方法會將最終修改的字節碼以byte數組形式返回
                        byte[] bytes = classWriter.toByteArray()
                        //通過文件流寫入方式覆蓋掉原先的內容,實現class文件的改寫
                        FileOutputStream fileOutputStream = new FileOutputStream(file.path)
                        fileOutputStream.write(bytes)
                        fileOutputStream.close()
                    }
                }
 
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }
    }

5.3.4 運行

代碼都全部編寫之后,再次點擊uploadArchives任務生成本地倉庫,然后就可以運行項目檢測插樁是否成功了,這是項目中的MainActivity文件,可以看到只在onCreate中第一行打印了一個log

public class MainActivity extends AppCompatActivity {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i("TAG", "is this the first log?");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

項目運行后,在LogCat中篩選TAG,可以看到打印語句的輸出,onMethodEnter中插入的語句最先輸出,接著是在onCreate方法開頭的語句,最后是在onMethodExit中插入的語句


5.4. 總結

自定義Gradle插件遵循一定的規則,手動實現幾次就能掌握,插件和ASM的銜接在Transform中完成,插件負責輸入數據,ASM接收數據后進行字節碼修改,最后再重新輸出,ASM的使用主要還是流程和api的掌握,比較不好編寫的是最終插入和修改字節碼的api,如果對字節碼指令不太熟悉的話可以安裝一個ASM Bytecode Viewer插件,將相關操作在java文件中完成,運用插件編譯成字節碼和ASM指令格式,照搬過來就行

轉自:
http://www.lxweimin.com/p/13d18c631549
https://blog.csdn.net/tushiba/article/details/106361871

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容