ASM和自定義Transform的應用實踐

上篇文章學習了下如何自定義gradle插件《自定義gradle》,在實際應用中一般都會結合自定義的Transform和字節碼操作框架(像ASM、Javassist)通過操作字節碼來實現自己的業務邏輯,Transform是Android Gradle 在1.5.0 版本後提供的, 它允許第三方的Plugin插件在打包成 dex 文件之前的編譯過程中操作 class 文件,對於一些重複性的操作,程序員可以統一修改class文件來實現自己的功能,從而避免在項目中出現大量的重複代碼。在Android項目中常用的場景有統計打點、打印方法執行的耗時時長等,本文中以統計打印Activity的打開和關閉事件爲例,來學習實踐下整個過程。

1、創建自定義的Plugin插件

在custom_plugin的src/main/groovy/com/znh/plugin下創建一個用於頁面統計的自定義插件PagePlugin,代碼如下:

package com.znh.plugin

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

/**
 * Created by znh on 2020-04-21
 * <p>
 * 用於頁面統計的Plugin
 */
class PagePlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {

        //註冊自定義的PageTransform
        def androidConfig = project.extensions.getByType(AppExtension)
        PageTransform transform = new PageTransform()
        androidConfig.registerTransform(transform)
    }
}

注:不要忘記在resources/META-INF/gradle-plugins下爲PagePlugin配置對應的properties文件

2、引入ASM依賴並創建Transform

這裏選用的操作字節碼的框架是ASM,所以需要引入ASM的依賴包,由於Transform是android gradle下面的api,所以也需要引入android gradle環境,在custom_plugin的build.gradle文件中添加以上配置,配置代碼如下:

//引入android環境
implementation 'com.android.tools.build:gradle:3.6.3'
    
//ASM依賴
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'

在custom_plugin的src/main/groovy/com/znh/plugin下創建一個用於頁面統計的Transform類PageTransform,代碼如下:

package com.znh.plugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.znh.gradle.custom.plugin.PageClassVisitor
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter

/**
 * Created by znh on 2020-04-21
 * <p>
 * 處理頁面統計的Transform
 */
class PageTransform extends Transform {

    @Override
    String getName() {
        return "PageTransform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)

        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider

        inputs.each {
            TransformInput transformInput ->

                //Gradle3.6.0之後需要單獨複製jar包
                transformInput.jarInputs.each { JarInput jarInput ->
                    File jarFile = jarInput.file
                    def destJar = outputProvider.getContentLocation(jarInput.name,
                            jarInput.contentTypes,
                            jarInput.scopes, Format.JAR)
                    FileUtils.copyFile(jarFile, destJar)
                }

                //操作class文件
                transformInput.directoryInputs.each {
                    DirectoryInput directoryInput ->
                        File dir = directoryInput.file
                        if (dir) {
                            dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->

                                //讀取和解析class文件
                                ClassReader classReader = new ClassReader(file.bytes)
                                //對class文件的寫入
                                ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                                //訪問class文件的內容
                                ClassVisitor classVisitor = new PageClassVisitor(classWriter)
                                //調用classVisitor的各個方法
                                classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

                                //將修改的字節碼以byte數組返回
                                byte[] bytes = classWriter.toByteArray()
                                //通過文件流寫入方式覆蓋原先的內容,完成class文件的修改
                                FileOutputStream outputStream = new FileOutputStream(file.path)
                                outputStream.write(bytes)
                                outputStream.close()
                            }
                        }

                        //處理完輸入文件後,把輸出傳遞給下一個transform
                        def destDir = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                        FileUtils.copyDirectory(directoryInput.file, destDir)
                }
        }
    }
}
  • getName方法:執行這個Transform的task時,會以這個名字爲基礎生成task名稱( 比如這裏的任務是Task :app:transformClassesWithPageTransformForDebug)
  • getInputTypes方法:表示要處理的數據類型是什麼,CLASSES 表示要處理編譯後的字節碼(可能是 jar 包也可能是目錄),RESOURCES 表示要處理的是標準的 java 資源
  • getScopes方法:表示Transform 的作用域,這裏設置的SCOPE_FULL_PROJECT代表作用域是全工程
  • isIncremental方法:表示是否支持增量編譯,false不支持
  • transform方法:在這個方法中獲取到文件輸入源,對裏面的class文件做一些自定義的修改後,將文件複製到指定目錄中做爲下一個Transform的輸入源,多個Transfrom之間是一條執行鏈,上一個Transfrom的輸出作爲下一個Transfrom的輸入,由於transform方法中的父類是空實現,所以當自定義一個Transfrom時,就算對class不進行任何操作,在這個方法中仍然要將文件複製到指定目錄作爲下一個Transfrom的輸入,如果不做複製操作直接調用super方法,那整個執行鏈將斷開而導致程序異常

上述代碼中的PageClassVisitor類是自定義的操作class文件的類,它實現了ASM框架中的ClassVisitor類

3、基於ASM創建class文件的操作類

需要根據ASM框架提供的api和規則來創建操作字節碼文件的操作類,這裏需要修改字節碼來實現自己的業務邏輯,所以爲了方便這裏使用java類來實現,在/custom_plugin的src/main/java/com/znh/gradle/custom/plugin下創建PageClassVisitor和PageMethodVisitor用來修改class字節碼,插入自己的統計指令,以完成自己的業務需求。PageClassVisitor類用於對class字節碼的觀察並監聽類的信息,PageClassVisitor代碼如下:

package com.znh.gradle.custom.plugin;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * Created by znh on 2020-04-21
 * <p>
 * ASM操作class的類
 */
public class PageClassVisitor extends ClassVisitor {

    //當前類的類名稱
    //本例:com/znh/gradle/plugin/demo/MainActivity
    private String className;

    //className類的父類名稱
    //本例:androidx/appcompat/app/AppCompatActivity
    private String superName;

    public PageClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor);
    }

    @Override
    public void visit(int version, int access, String className, String signature, String superName, String[] interfaces) {
        super.visit(version, access, className, signature, superName, interfaces);
        this.className = className;
        this.superName = superName;
    }

    @Override
    public MethodVisitor visitMethod(int access, String methodName, String descriptor, String signature, String[] exceptions) {

        MethodVisitor methodVisitor = cv.visitMethod(access, methodName, descriptor, signature, exceptions);

        //判斷是否自己要匹配的方法(這裏匹配onCreate和onDestroy方法)
        if ("android/support/v7/app/AppCompatActivity".equals(superName)//support包
                || "androidx/appcompat/app/AppCompatActivity".equals(superName)) {//androidx包
            if (methodName.startsWith("onCreate")) {
                return new PageMethodVisitor(methodVisitor, className, methodName);
            }
            if (methodName.startsWith("onDestroy")) {
                return new PageMethodVisitor(methodVisitor, className, methodName);
            }
        }
        return methodVisitor;
    }
}

PageMethodVisitor類用於對方法的觀察,可以在visitCode的方法中修改字節碼,插入自己的字節碼指令,從而植入自己的業務邏輯,PageMethodVisitor類代碼如下:

package com.znh.gradle.custom.plugin;

import org.objectweb.asm.MethodVisitor;

import groovyjarjarasm.asm.Opcodes;

/**
 * Created by znh on 2020-04-21
 * <p>
 * ASM操作Method方法的類
 */
public class PageMethodVisitor extends MethodVisitor {

    //當前的類名
    private String className;

    //當前的方法名
    private String methodName;

    PageMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
        super(Opcodes.ASM5, methodVisitor);
        this.className = className;
        this.methodName = methodName;
    }

    @Override
    public void visitCode() {
        super.visitCode();

        //在這裏插入自己的字節碼指令
        //可以插入頁面打開關閉的打點上報代碼,這裏用log打印代替了
        mv.visitLdcInsn("Page_TAG");//可以用來過濾log日誌的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);
    }
}
4、測試打印結果

上述代碼編寫完成後,需要將插件重新上傳到maven上,然後在app的build.gradle中引入新上傳的插件,運行項目並查看log日誌如下:
在這裏插入圖片描述

項目地址:https://github.com/huihuigithub/blog_demo_projects.git(gradle_plugin_demo項目)

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