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

目標

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

 

1. 自定義Gradle插件實現

gradle實現自定義插件一般有三種方式:

      1. build.gradle中直接編寫,項目內使用

      2. 創建buildSrc module,項目內使用

      3. 創建獨立module,可發佈遠程倉庫

考慮到靈活性,選擇第三種方式實現自定義插件

 

1.1 創建module

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

1.2 創建目錄和配置文件

1.2.1 在src/main下創建java和groovy目錄以及resources/META-INF/gradle-plugins
目錄,META-INF和gradle-plugins均爲package

1.2.2 在resources/META-INF/gradle-plugins下創建xxxx.properties文件, xxxx爲apply plugin時用到的名字,文件中內容爲implementation-class = 插件的完整路徑,這裏可以先空着,後面創建了插件後再填入

1.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'))
        }
    }
}

1.4 編寫插件

編譯完成後在groovy中添加package,創建實現Plugi<>接口的類文件,先使用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)
    }
}

1.5 生成倉庫

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

1.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插件的編寫和使用

 

2. ASM實現字節碼插樁

2.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'))
        }
    }
}

 2.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的類中,實現onMethodEnter和onMethodExit方法,對應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方法即可

 

2.3 讀取class文件數據

在自定義繼承Transform類的transform方法中進行操作,通過getInputs()獲取輸入的class文件和jar包的路徑,outputProvider管理輸出路徑,接着遍歷inputs,directoryInputs獲取到class文件的路徑集合,再次遍歷,篩選出class文件,通過ClassReader進行讀取,ClassWriter進行寫入,將classWiter傳入自定義的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)
            }
        }
    }

2.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中插入的語句

 

3. 總結

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

 

參考: Android工程師進階

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章