Gradle Transform 与 字节码插桩

        gradle用于构建项目,其plugin插件用于完成一特定功能,而有些时候我们希望在插件中完成对项目内容的一些更改,这就需要我们在gradle构建过程中,获取到源文件才能进行,所幸的是,gradle plugin从1.5.0版本开始,为我们提供了Transform功能,它可以以输入输出流的链式方式,供我们对源文件进行处理。

Transform功能的结构:

在这里插入图片描述

QualifiedContent
该接口定义了一个输入内容的基本实现,有name和file,此file可能有两种形式,文件夹和jar包,于是对应的分为两个实现类,DirectoryInput和JarInput

TransformInput
对于每一个输入流来说,内容既可能有一组文件夹,也可能有一组jar包,TransformInput类为一个输出流的标准实现

TransformOutputProvider
既然有了input,对应的就要有output,output的位置不能由我们私自决定,需要通过TransformOutputProvider的getContentLocation()获取

TransformInvocation
将以上输入输出流信息包装为TransformInvocation对象,这也是3.0之后的一个改变,之前只是将各个参数直接传递给Transform

ContentType
对于输出内容,我们可以指定想要获取的输入内,ContentType中设定了几种类型:

CLASSES:字节码文件

RESOURCES:资源文件

SCOPE
除了ContentType,还可以指定整个Transform的作用域,SCOPE中设定了几种类型:

PROJECT:主项目

SUB_PROJECT:子项目

TESTED_CODE:测试代码使用的依赖

PROVIDED_ONLY:本地或者远方服务器依赖

EXTERNAL_LIBRARIES:第三方依赖库

TransformManager
3.0开始,gradle为我们提供了TransformManager类,里面的一些常量帮我们指定了一些常用的ContentType和Scope,比如:

SCOPE_FULL_PROJECT:包括了PROJECT、SUB_PROJECT、EXTERNAL_LIBRARIES,也是最常用的一个

CONTENT_CLASS:包括了CLASSES,也是最常用的一个。

Transform
自定义Transform需要重写的几个方法:

getName():为Transform定义一个名字,不过该名字最后生成的文件名,也是拼接上了flavor、buildType等等

getInputTypes():该方法返回的就是一组ContentType,用于限定接受的输入内容类型

getScopes():该方法返回的就是一组Scope,用于限定transform的作用域

isIncremental():该方法是指定该Transform是否使用增量构建模式

transform(xxxx):该方法就是实际转换时候调用的方法,3.0之前将input、outputProvider等传入,3.0开始直接将一个TransformInvocation对象

自定义Transform

编写一个Transform的java类,起名为MyTransform(名字随便取),继承于Transform,需要重写上面所说的结果方法。

public class MyTransform extends Transform {
    @Override
    public String getName() {
        return null;
    }

    @Override
    public Set getInputTypes() {
        return null;
    }

    @Override
    public Set getScopes() {
        return null;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }
}

在gradle插件库中,为我们定义了许多的Transform,我们可以看Transform的实现类:

Gradle Transform 与 字节码插桩_第1张图片

2.注册我们的Transform,我们可以在插件当中注册,我们可以将当前的project作为参数传递给Transform当中

public class ChaZhuangPlugin implements Plugin {
    @Override
    public void apply(Project project) {
        System.out.println("your name is kobe");
        AppExtension baseExtension = project.getExtensions().getByType(AppExtension.class);
        baseExtension.registerTransform(new MyTransform(project));
    }
}

也可以直接在build.gradle注册,注册的方式都是一样的

project.getExtensions().getByType(AppExtension.class).registerTransform(new MyTransform(project))

现在整个Transform定义的流程就走完了,具体的就看我们需要重新的Transform里面相应的方法的逻辑了。

    @Override
    public String getName() {
        return "dexchazhuang";
    }
    
    @Override
    public Set getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

最主要的逻辑就是在我们需要重写的transform方法里面,在构建Transform的时候,每个Transform的输出作为下一个Transform的输入,前一个Transform的output,会根据下一个Transform的contentTypes和scopes,将相应的内容传入到下一个Transform的inputs中,这点很重要,我们要添加自己的行为,又不能破坏系统的行为,所以在修改输入之后,要要输出到指定位置,供下一个transform使用。

每一个自定义的Transform,都会被gradle插件包装成一个Task执行,并且,每一个自定义的Transform一定是在所有的Transform之前执行的,并且是在得到所有java文件编译成class文件之后才执行的,这样我们才可以对class进行处理,进行插桩。

ASM 工具

对字节码进行处理的开源工具有很多,例如Javassist ,ASM,我们这里选择ASM,其实在gradle的插件中就集成了ASM的相关工具的使用,我们也不需要单独引进ASM的相关库了.如果需要的话,也可以去jcenter中搜索 https://bintray.com/  ,可以在自己module中引用

implementation 'org.ow2.asm:asm:8.0.1'
implementation 'org.ow2.asm:asm-commons:8.0.1'

也可以 引入下面的

implementation gradleApi()
implementation 'com.android.tools.build:gradle:3.4.1'

上面两种方式选择一种就行了(二选一)

因为ASM修改的是字节码,所以我们需要借助javac 来查看一个类的字节码。但是这样查看一个类的字节码,太麻烦了,在AndroidStudio中,我们可以借助字节码插件ASM,来查看一个类的字节码:

Gradle Transform 与 字节码插桩_第2张图片

安装插件之后,重启AndroidStudio就可以使用插件了,要想查看一个类的字节码,我们必须在编写一个类之后,重新编译Gradle一次,让其生成对应java文件的class文件,然后ASM插件才能查看字节码:选中一个类之后,点击鼠标右键,选择ASM ByteCode Viewer,等待一会就会在右侧出现对应类的字节码文件:

Gradle Transform 与 字节码插桩_第3张图片

接下来我们就来利用ASM进行字节码插桩

Gradle Transform 与 字节码插桩_第4张图片    Gradle Transform 与 字节码插桩_第5张图片

首先左侧的ASMTest使我们的原始类,我们需要使用ASM 工具,进行字节码插桩,在ASM中的所有函数(虽然现在只写了一个函数,但是有一个默认的构造函数)插入一段计算方法执行时间的代码,插桩完成之后ASMTest.class文件就变成BTest这样的了

用一个纯java的module来做写着例子在main方法中去调用ASM修改字节码

public static void main(String args[]) {
        try {
            /*** 1、准备待分析的class*/
            String classInPath = "/Users/we/Documents/androidproject/AndroidDemo/asm_test/build/classes/java/main/com/android/asm_test/ASMTest.class";
            FileInputStream fis = new FileInputStream(classInPath);
            ClassReader cr = new ClassReader(fis);
            /*** 2、执行分析与插桩*/
            //class字节码的读取与分析引擎
            // 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

            //分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问
            cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);
            /** 3、获得结果并输出*/
            byte[] newClassBytes = cw.toByteArray();
            String classOutPath = "/Users/we/Documents/androidproject/AndroidDemo/asm_test/build/classes/java/main/com/android/ASMTest2.class";
            FileOutputStream fos = new FileOutputStream(classOutPath);
            fos.write(newClassBytes);
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

其中ClassAdapterVistor的写法

public class ClassAdapterVisitor extends ClassVisitor {
    public ClassAdapterVisitor(ClassVisitor api) {
        super(Opcodes.ASM5,api);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.err.println("方法名称:"+name+"  signature:"+signature+"   descriptor="+descriptor);
        //得到MethodVistor 然后传递给使用自定义的包装类来返回
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new CustomMethodVisitor(api, mv, access, name, descriptor);
    }
}

使用自定义的包装类CustomMethodVisitor来返回,将原来的MethodVisitor作为对象传递进去,这样我们就可以来监听相关的方法,然后对方法的字节码进行操作,对方法的字节码进行操作的主要逻辑就在CustomMethodVistor中。

对应的ASMTest.java的字节码,修改成为和BTest.class一样的字节码的实现:

package com.android.asm_test;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

public class CustomMethodVisitor extends AdviceAdapter {

    protected CustomMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
    }

    private int start;

    /**
     * 方法进入的时候执行
     */
    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        //invokeStatic指令,调用静态方法
        invokeStatic(Type.getType("Ljava/lang/System;"),
                new Method("currentTimeMillis", "()J"));
        //创建本地 LONG类型变量
        start = newLocal(Type.LONG_TYPE);
        //store指令 将方法执行结果从操作数栈存储到局部变量
        storeLocal(start);
    }

    /**
     * 方法返回的时候执行
     * @param opcode
     */
    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
        int end = newLocal(Type.LONG_TYPE);
        //store指令 将方法执行结果从操作数栈存储到局部变量
        storeLocal(end);

        getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType( "Ljava/io/PrintStream;"));
        newInstance(Type.getType("Ljava/lang/StringBuilder;"));
        dup();
        invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), new Method("", "()V"));
        visitLdcInsn("execute :");
        invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
        loadLocal(end);
        loadLocal(start);
        math(SUB,Type.LONG_TYPE);
        invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append", "(J)Ljava/lang/StringBuilder;"));
        visitLdcInsn("ms.");
        invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
        invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString", "()Ljava/lang/String;"));
        invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println", "(Ljava/lang/String;)V"));
    }
}

这里需要在 进入方法onMethodEnter和方法快执行完 返回的时候,也就是onMethodExit的时候,去加入我们的字节码,我们可以利用ASM插件,对着插件生成的字节码一行一行的写,在插件中:

Gradle Transform 与 字节码插桩_第6张图片

Bytecode 栏显示类的字节码,ASMified显示使用ASM 实现的字节码,我们可以对着ASMified这一栏一行一行的写代码

Gradle Transform 与 字节码插桩_第7张图片

其实就是按照这个字节码一个个翻译一下,其中"java/lang/System"要修改为方法签名所以在修改字节码时候要修改为”Ljava/lang/System;“

其中L代表引用类型。而且最后面的分号还不能少(System后面的分号’;‘),签名中的特殊字符的代表的意思。

Gradle Transform 与 字节码插桩_第8张图片

也可以使用javap 命令查看签名。

回到上面的自定义自定义Transform来修改我们Android工程中class文件的字节码,自定义Transform处理字节码的方法主要是重写transform方法:

public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException

具体的实现如下,注释标记的很清楚了

 @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        //得到Transform的输入  可以是jar 和 目录
        Collection inputs = transformInvocation.getInputs();
        //遍历 我们的输入
        for (TransformInput input : inputs) {
            // 得到 目录输入
            Collection directoryInputs = input.getDirectoryInputs();

            //得到 jar的 input 要修改jar里面的字节码 需要先解压,然后 修改子字节码最后压缩放回原来的位置
            //这里就不便利 jar了
           // Collection jarInputs = input.getJarInputs();
            // 遍历目录输入
            for (DirectoryInput directoryInput : directoryInputs) {
                // 遍历 输入文件
                File src = directoryInput.getFile();
                //得到 输出,必须使用transformInvocation.getOutputProvider() 来获取文件的输出
                //供下一个transform 使用,不能破坏transform的 输入
                File dst = transformInvocation.getOutputProvider().getContentLocation(
                        directoryInput.getName(), directoryInput.getContentTypes(),
                        directoryInput.getScopes(), Format.DIRECTORY);
                //过滤 当前目录 中 DOT_CLASS = ".class" 以.class 结尾的文件,递归调用 文件夹
                Collection files = FileUtils.listFiles(src,
                        new SuffixFileFilter(SdkConstants.DOT_CLASS, IOCase.INSENSITIVE), TrueFileFilter.INSTANCE);
                for (File f : files) {
                    // src 的 path 直接定位到 编译之后形成的class文件所在的根目录
                    //  /Users/we/Documents/androidproject/AndroidDemo/app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes
                    // 出去src 根目录之后 后面就是具体的类的目录  但是是以'/' 结尾的,我们需要将'/'换成'.'就是全类名了
                    //  /Users/we/Documents/androidproject/AndroidDemo/app/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes/androidx/activity/R.class
                    String className = f.getAbsolutePath()
                            .substring(src.getAbsolutePath().length() + 1,
                                    f.getAbsolutePath().length() - SdkConstants.DOT_CLASS.length())
                            .replace(File.separatorChar, '.');
                    // 符合 com.android.androiddemo 开头的类 都是我们自己的类,就是我们需要插桩的class文件了
                    if(className.startsWith(GENERATED_PACKAGE)){
                        try {
                            FileInputStream fis = new FileInputStream(f.getAbsoluteFile());
                            //具体的插桩逻辑
                            byte[] byteCode = referHackWhenInit(fis);
                            fis.close();

                            FileOutputStream fos = new FileOutputStream(f.getAbsoluteFile());
                            fos.write(byteCode);
                            fos.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
                //插桩完成之后 需要将你写的文件重新 拷贝到dst中 供下一个transform使用
                //这个不能忘记
                FileUtils.copyDirectory(src, dst);
            }
        }

    }

    private byte[] referHackWhenInit(InputStream fis) throws IOException {
        ClassReader cr = new ClassReader(fis);// 通过IO流,将一个class解析出来,解析失败会抛异常
        ClassWriter cw = new ClassWriter(cr, 0);//再构建一个writer
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, cw) {
            public MethodVisitor visitMethod(int access, final String name, String desc,
                                             String signature, String[] exceptions) {
                MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                mv = new MethodVisitor(Opcodes.ASM5, mv) {
                    @Override
                    public void visitInsn(int opcode) {
                        //就是在构造函数中 插入一段Class clazz = Antilazyload.class的代码
                        if ("".equals(name) && opcode == Opcodes.RETURN) {
                            super.visitLdcInsn(Type.getType("Lcom/android/androiddemo/Antilazyload;"));//在class的构造函数中插入一行代码
                        }
                        super.visitInsn(opcode);
                    }
                };
                return mv;
            }
        };
        cr.accept(cv, 0);
        return cw.toByteArray();
    }

此时所有的插件 transform已经编写完毕,我们可以引入我们的插件:

apply plugin: com.android.buildsrc.ChaZhuangPlugin

然后编译整个工程,就会对我们自己编写的代码进行字节码插桩了

Gradle Transform 与 字节码插桩_第9张图片

 

我们可以看到的确在构造函数中插入了一段代码。

总结

    我们在开发Android项目的时候,当编译项目指挥,都会在底部控制台的build栏里面看到很多任务的执行,例如:

Gradle Transform 与 字节码插桩_第10张图片      Gradle Transform 与 字节码插桩_第11张图片

这个时候我们就应该明白这些任务是干什么的。我们就可以通过:

project.getTasks().findByName(****) ;     //  *****代表任务的名称

得到当前的任务,得到当前的任务之后,我们就可以通过任务得到这个任务的输入输出,之后就可以操作这个任务的输入输出了。

例如,我们得到混淆的任务

final Task proguardTask =
        project.getTasks().findByName("transformClassesAndResourcesWithProguardForRelease" );
TaskOutputs outputs = proguardTask.getOutputs();
Set files = outputs.getFiles().getFiles();

我们就可以得到混淆任务的输出文件集合,最后通过遍历files的文件集合,就可以得到mapping文件。

Demo传送门

你可能感兴趣的:(gradle,插桩,ASM,自定义,Transform)