編譯插樁利用ASM 插入字節碼

使用ASM,插入字節碼到Activity文件


簡單記錄一下ASM在此需求中的實現插入字節碼

一、 ASM

ASM 是一個Java字節碼操控框架, 可以用來改變或者增強現有類的功能,可以通過解析.class文件中的字節碼,經過一些處理,生成新的字節碼

1.1 在這裏僅完成需求,需要主要的幾個類:

  1. ClassReader 負責解析 .class 文件中的字節碼,並將所有字節碼傳遞給 ClassWriter
  2. ClassVisitor: 負責訪問.class文件的各個元素,可以解析或者修改.class文件的內容
  3. ClassWriter:繼承自 ClassVisitor,它是生成字節碼的工具類,負責將修改後的字節碼輸出爲 byte 數組

1.2 添加ASM到自定義gralde組件的中

  • 添加依賴
 // ASM 相關依賴
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'

  • 創建自定義的Visitor類

記住這個類文件不要使用kotlin來寫,用Java來寫,具體原因在後面專門記錄

public class CustomClassVisitor extends ClassVisitor {
    private String className;
    private String superName;

    public CustomClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @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) {
        System.out.println("ClassVisitor visitMethod name--->" + name + ", superName" + superName);
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        // 判斷是不是繼承於AppCompatActivity,這個地方是androidx的,看自己項目的需求,但是涉及到第三方的jar包,一般都是連個都判斷,找到Activity類幾方法
        if (superName.equals("androidx/appcompat/app/AppCompatActivity")) {
            if (name.startsWith("onCreate")) {
                return new CustomMethodVisitor(mv, className, name);
            }
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
    }
}

visitMethod方法中 判斷是不是繼承於AppCompatActivity,這個地方是androidx的,看自己項目的需求,但是涉及到第三方的jar包,一般都是連個都判斷,找到Activity類幾方法

  • 真正的執行插入字節碼的代碼是在自定義的MethodVisitor中
public class CustomMethodVisitor extends MethodVisitor {
    private String className;
    private String methodName;

    public CustomMethodVisitor(MethodVisitor mv, String className, String methodName) {
        super(Opcodes.ASM5, mv);
        this.className = className;
        this.methodName = methodName;
    }

    @Override
    public void visitCode() {
        super.visitCode();
        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);
    }
}

visitCode 方法中進行插入字節碼 ,ASM 都是直接以字節碼指令的方式進行操作的

二、將插入字節碼的類在transform中進行綁定

void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // 拿到所有的class文件
        Collection<TransformInput> transformInputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        transformInputs.each { TransformInput transformInput ->
        
            // // 遍歷directoryInputs(文件夾中的class文件) directoryInputs代表着以源碼方式參與項目編譯的所有目錄結構及其目錄下的源碼文件
            //            // 比如我們手寫的類以及R.class、BuildConfig.class以及MainActivity.class等
            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        System.out.println("find class: " + file.name)
                        // 對Class 文件進行讀取與解析
                        ClassReader classReader = new ClassReader(file.bytes)
                        // class 文件寫入
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        // 訪問class 文件相應的內容、解析某一個結構就會通知到ClassVisitor的相應方法
                        CustomClassVisitor classVisitor = new CustomClassVisitor(classWriter)
                        // 依次調用ClassVisitor 接口的各個方法
                        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                        // 將最終修改的字節碼以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)
                }
            }
        }

    }

這就完成了字節碼插樁, 進行編譯,並且以來在app 的gradle 中添加依賴,如果是gradle3.6以下版本,通過上述方法是可以的,

2.1 gradle3.6 及以上 包適配,

// 運行保存
Landroidx/appcompat/R$drawable;

gradle3.6+將R文件單獨編譯成了一個jar包,

所有找不到,需要將R.jar單獨進行復制

transformInput.jarInputs.each { JarInput jarInput ->
                File file = jarInput.file
                System.out.println("find jar input: " + file.name)
                def dest = outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes,
                        jarInput.scopes, Format.JAR)
                FileUtils.copyFile(file, dest)
            }

然後在編譯,運行

成功後日志打印:

2020-04-28 14:12:59.272 10953-10953/com.kpa.compiletheplugpile I/TAG: com/kpa/compiletheplugpile/MainActivity---->onCreate

項目代碼地址

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