参考:https://juejin.im/post/5cc3db486fb9a03202222154

上一篇 ASM的使用

上一篇说到了am的使用,但是局限于对于特定class文件使用,但是在android中不能每个class都那样做。借助gradle插件和transfrom,我们可以干预android的打包过程,从中拿到所有class,从而进行插桩。

下面分三点进行介绍:

本文大纲:

  1. gradle插件简单介绍
  2. Transform的介绍
  3. asm结合Transform进行android中的编译插桩

 

1.gradle插件简单介绍

本文主要介绍Transform和Asm结合使用,但是要用到gradle插件,所以先简单介绍下。

 

1.1 自定义gradle插件的三种方式

官网介绍了自定义gradle插件的三种方式:

https://docs.gradle.org/current/userguide/custom_plugins.html#example_a_build_for_a_custom_plugin

 

  • Build Script方式。

即直接在项目的build.gradle中添加groovy脚本代码并引用。这样插件在构建脚本之外不可见,只能在此模块中使用脚本插件。

  • buildSrc项目。

将插件的源代码放在rootProjectDir/buildSrc/src/main/groovy目录中,Gradle将负责编译和测试插件,并使其在构建脚本的类路径中可用。该插件对构建使用的每个构建脚本都是可见的。但是,它在构建外部不可见,因此您不能在定义该构建的外部重用该插件。即在当前工程的各个模块都可见,但是项目之外不可见。

  • 独立项目

您可以为插件创建一个单独的项目。这个项目产生并发布了一个JAR,您可以在多个版本中使用它并与他人共享。通常,此JAR可能包含一些插件,或将几个相关的任务类捆绑到一个库中。或两者的某种组合。

 

本次我们使用的第二种,buildSrc方式。

 

1.2 buildSrc方式介绍:

 

buildSrc是android中一个保留名,是一个专门用来做gradle插件的module,所以这个module的名字必须是buildSrc,此模块下面有一个固定的目录结构src/main/groovy,这个是用来存放真正的脚本文件的。其他的自定义类可以放在这个目录下,也可以放在自建的其他目录下。

 

创建buildSrc插件模块:

创建buildSrc模块时,可以直接创建一个 library module,也可以自己创建目录。我使用自己创建目录的形式。步骤如下:

  • 1.在工程根目录下创建目录buildSrc
  • 2.在buildSrc下创建目录结构 src/main/groovy
  • 3.在buildSrc根目录下创建 build.gradle,并添加如下:
apply plugin: 'groovy'

dependencies {
    // gradle插件必须的引用
    implementation gradleApi()
    implementation localGroovy()

    // transform依赖
    // gradle 1.5
//    implementation 'com.android.tools.build:transfrom-api:1.5.0'
    // gradle 2.0开始
    implementation 'com.android.tools.build:gradle:3.5.2'
    implementation 'com.android.tools.build:gradle-api:3.5.2'

    // asm依赖
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-util:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
}

repositories {
    mavenCentral()
    jcenter()
    google()
}

// 指定编译的编码 不然有中文的话会出现  ’编码GBK的不可映射字符‘
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
    println('使用utf8编译')
}
  • 4.在src/main/groovy下创建插件入口, ASMPlugin.java:
package com.dgplugin;

import com.android.build.gradle.AppExtension;

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.ExtensionsSchema;

/**
 * author: DragonForest
 * time: 2019/12/24
 */
public class AsmPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
       // 这里是插件的入口,在此做插件的处理
       System.out.println("==============我是AsmPlugin插件=============")
    }
}

 

现在的目录结构基本是这样:

 

 

至此,buildSrc插件就完成了。

 

使用buildSrc插件:

使用十分简单

1.首先在settting.gradle中添加buildSrc模块

 

2.在app模块下的build.gradle中添加使用:

 

注意这里apply plugin: 后面的名字是 pluginId, 这里id就是AsmPlugin的全类名,而且不能加引号。

 

此时我们build一下app模块,可以看到 ==============我是AsmPlugin插件============= 已经打印,此时我们的插件已经生效。

 

2.Transform的介绍

2.1 Transform是什么

android 构建流程是一套流水线的工作机制,每一个的构建单元接收上一个构建单元的输出,作为输入,再将产品进行输出,com.android.build库提供了Transform的机制,而这个机制是android 构建系统为了给外部提供一个可以加入自定义构建单元,如拦截某个构建单元的输出,或者加入一些输出等。而这些Transform是在java源码编译完成之后,最终package之前进行的。而external Transform是在mergeJava、mergeResouces之后,在proguard之前执行的。

我们可以看普通的编译过程中,transform处于哪个位置:

 

2.2 Transform执行机制:

在android gradle构建系统中,可以通过project.getExtensions().getByType(AppExtension.class).registerTransform(Transform transform)将transform注册到构建系统中。其内部调用了 BaseExtension#registerTransform(Transform transform) --->TranformManager#addTransform(Transform transform)

 

其实android gradle plugin对每一个Transform对添加一个TransformTask对象,由这个对象执行Transform,因此可以为Transform添加依赖。

至于transform如何添加进构建系统,暂时不是很明白,可以参考https://mp.weixin.qq.com/s/YFi6-DrV22X_VVfFbKHNEg

下面是我自己理解的transform执行流程:

 

其实有很多操作就是使用transform来做的,比如混淆,当我们开启的混淆之后,执行build,会发现混淆的task:

2.3 Transform抽象类

Transform是一个抽象类,位于com.android.build.api.transform包中,因此要自定义Transform,要继承Transform,下面我们看看Transform的抽象方法。

1、getName

返回这个Transform的名字,一般而言这个Transform的名字代表这个Transform的工作内容。

2、getInputTypes

返回这个Transforem输入数据类型,这个数据类型必须是  QualifiedContent.ContentType,有两种类型CLASSES(是java 编译之后的class 文件,可以是文件夹,或者jar文件、RESOURCES(是资源文件)

3、getScopes

返回Transform处理数据的来源,在Scope定义了它的枚举

 

4、isIncremental

是否增量Transform,如果是,则TransformInput返回changed、removed、added的文件集合。

5、transform

是一个内部方法。当这个构建系统执行到该构建单元的时候,会调用这个Transform的方法。这是进行处理class文件的核心方法。

 

 

3.asm结合Transform进行android中的编译插桩

铺垫了这么多,下面来实战一下。假如我们在app模块中有现在下面的代码:

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.sayHello).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                InjectTest injectTest=new InjectTest();
                injectTest.sayHello();
            }
        });
    }
}

 InjectTest.java

public class InjectTest {
    public void sayHello(){
        Log.e("InjectTest","你好啊 啊啊啊啊");
        System.out.println("你好");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

需求: 在点击按钮之后,输出sayHello() 方法的执行时间。

 

分析: 我们的目的是在InjectTest#sayHello() 方法中添加记录时间的代码,结合之前的铺垫我们的方案步骤如下:

  • 1.定义插件,在插件入口注册我们自己的transform
  • 2.在transform中获取所有的class,当拿到InjectTest.class的时候对其进行插桩。
  • 3.插桩之后将其输出到下一步任务中。

 

定义插件,上面已经介绍过了,我们在入口处注册transform:

/**
 * author: DragonForest
 * time: 2019/12/24
 */
public class AsmPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        System.out.println("===================");
        System.out.println("I am com.dgplugin.AsmPlugin");
        System.out.println("===================");

        // 注册transform
        registerTransform(project);
    }

    private void registerTransform(Project project) {
        AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);
        appExtension.registerTransform(new AsmTransform(project));
    }
}

 下面写我们AsmTransform.java:

package com.dgplugin;

import com.android.build.api.transform.DirectoryInput;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager;

import org.apache.commons.io.FileUtils;
import org.gradle.api.Project;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;

/**
 * author: DragonForest
 * time: 2019/12/24
 */
public class AsmTransform extends Transform {
    Project project;

    public AsmTransform(Project project) {
        this.project = project;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        // 消费型输入,可以从中获取jar包和class包的文件夹路径,需要输出给下一个任务
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        // 引用型输入,无需输出
        Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs();
        // 管理输出路径,如果消费型输入为空,你会发现OutputProvider==null
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        // 当前是否是增量编译
        boolean incremental = transformInvocation.isIncremental();

        /*
            进行读取class和jar, 并做处理
         */
        for (TransformInput input : inputs) {
            // 处理class
            Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
            for (DirectoryInput directoryInput : directoryInputs) {
                // 目标file
                File dstFile = outputProvider.getContentLocation(
                        directoryInput.getName(),
                        directoryInput.getContentTypes(),
                        directoryInput.getScopes(),
                        Format.DIRECTORY);
                // 执行转化整个目录
                transformDir(directoryInput.getFile(), dstFile);
                System.out.println("transform---class目录:--->>:" + directoryInput.getFile().getAbsolutePath());
                System.out.println("transform---dst目录:--->>:" + dstFile.getAbsolutePath());
            }
            // 处理jar
            Collection<JarInput> jarInputs = input.getJarInputs();
            for (JarInput jarInput : jarInputs) {
                String jarPath = jarInput.getFile().getAbsolutePath();
                File dstFile = outputProvider.getContentLocation(
                        jarInput.getFile().getAbsolutePath(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
                transformJar(jarInput.getFile(),dstFile);
                System.out.println("transform---jar目录:--->>:" + jarPath);
            }
        }
    }

    @Override
    public String getName() {
        return AsmTransform.class.getSimpleName();
    }

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

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

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

    private void transformDir(File inputDir, File dstDir) {
        try {
            if (dstDir.exists()) {
                FileUtils.forceDelete(dstDir);
            }
            FileUtils.forceMkdir(dstDir);
        } catch (IOException e) {
            e.printStackTrace();
        }

        String inputDirPath = inputDir.getAbsolutePath();
        String dstDirPath = dstDir.getAbsolutePath();
        File[] files = inputDir.listFiles();
        for (File file : files) {
            System.out.println("transformDir-->" + file.getAbsolutePath());
            String dstFilePath = file.getAbsolutePath();
            dstFilePath = dstFilePath.replace(inputDirPath, dstDirPath);
            File dstFile = new File(dstFilePath);
            if (file.isDirectory()) {
                System.out.println("isDirectory-->" + file.getAbsolutePath());
                // 递归
                transformDir(file, dstFile);
            } else if (file.isFile()) {
                System.out.println("isFile-->" + file.getAbsolutePath());
                // 转化单个class文件
                transformSingleFile(file, dstFile);
            }
        }
    }

    /**
     * 转化jar
     * 对jar暂不做处理,所以直接拷贝
     * @param inputJarFile
     * @param dstFile
     */
    private void transformJar(File inputJarFile, File dstFile) {
        try {
            FileUtils.copyFile(inputJarFile,dstFile);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 转化class文件
     * 注意:
     *      这里只对InjectTest.class进行插桩,但是对于其他class要原封不动的拷贝过去,不然结果中就会缺少class
     * @param inputFile
     * @param dstFile
     */
    private void transformSingleFile(File inputFile, File dstFile) {
        System.out.println("transformSingleFile-->" + inputFile.getAbsolutePath());
        if (!inputFile.getAbsolutePath().contains("InjectTest")) {
            try {
                FileUtils.copyFile(inputFile,dstFile,true);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }
        AsmUtil.inject(inputFile, dstFile);
    }
}

在最后我们使用到了AsmUtil.inject(inputFile, dstFile);就是对其进行了插桩

AsmUtil.java

package com.dgplugin;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * author: DragonForest
 * time: 2019/12/23
 */
public class AsmUtil {

    public static void main(String arg[]) {
//        AsmUtil asmUtil = new AsmUtil();
//        asmUtil.inject();
    }

    /**
     * 使用ASM 向class中的方法插入记录代码
     */
    public static void inject(File srcFile,File dstFile) {

        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            /*
                1. 准备待插桩的class
             */
            fis = new FileInputStream(srcFile);
            /*
                2. 执行分析与插桩
             */
            // 字节码的读取与分析引擎
            ClassReader cr = new ClassReader(fis);
            // 字节码写出器,COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            // 分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展形式进行访问
            cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);

            /*
                3.获得新的class字节码并写出
             */
            byte[] newClassBytes = cw.toByteArray();
            fos = new FileOutputStream(dstFile);
            fos.write(newClassBytes);
            fos.flush();
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("执行字节码插桩失败!" + e.getMessage());
        } finally {
            try {
                if (fis != null)
                    fis.close();
                if (fos != null)
                    fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

 

ClassAdapterVisitor.java

package com.dgplugin;

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

/**
 * author: DragonForest
 * time: 2019/12/24
 */
public class ClassAdapterVisitor extends ClassVisitor {
    public ClassAdapterVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM7, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("方法名:" + name + ",签名:" + signature);
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new MethodAdapterVisitor(api, methodVisitor, access, name, descriptor);
    }
}

MethodAdapterVisitor.java

package com.dgplugin;

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

/**
 * author: DragonForest
 * time: 2019/12/24
 * AdviceAdapter 是 asm-commons 里的类
 * 对MethodVisitor进行了扩展,能让我们更轻松的进行分析
 */
public class MethodAdapterVisitor extends AdviceAdapter {
    private int start;
    private int end;
    private boolean inject = true;

    /**
     * Constructs a new {@link AdviceAdapter}.
     *
     * @param api           the ASM API version implemented by this visitor. Must be one of {@link
     *                      Opcodes#ASM4}, {@link Opcodes#ASM5}, {@link Opcodes#ASM6} or {@link Opcodes#ASM7}.
     * @param methodVisitor the method visitor to which this adapter delegates calls.
     * @param access        the method's access flags (see {@link Opcodes}).
     * @param name          the method's name.
     * @param descriptor    the method's descriptor (see {@link Type Type}).
     */
    protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        System.out.println("visitAnnotation, descriptor" + descriptor);
        if (Type.getDescriptor(ASMTest.class).equals(descriptor)) {
            inject = true;
        }
        return super.visitAnnotation(descriptor, visible);
    }

    /**
     * 整个方法最开始的时候的回调
     * 我们要在这里插入的逻辑就是 start=System.currentTimeMillis()
     * <p>
     * 使用ASMByteCodeViwer查看 上述代码的字节码:
     * LINENUMBER 19 L0
     * INVOKESTATIC java/lang/System.currentTimeMillis ()J
     * LSTORE 1
     */
    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        System.out.println("onMethodEnter");
        if (inject) {
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));
            // 创建本地local变量
            start = newLocal(Type.LONG_TYPE);
            // 方法执行的结果保存给创建的本地变量
            storeLocal(start);
        }
    }

    /**
     * 方法结束时的回调
     * 我们要在这里插入
     * long end = System.currentTimeMillis();
     * System.out.println("方法耗时:"+(end-start));
     * <p>
     * 使用ASMByteCodeViwer查看上述字节码:
     * L2
     * LINENUMBER 21 L2
     * INVOKESTATIC java/lang/System.currentTimeMillis ()J
     * LSTORE 3
     * L3
     * LINENUMBER 22 L3
     * GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
     * NEW java/lang/StringBuilder
     * DUP
     * INVOKESPECIAL java/lang/StringBuilder.<init> ()V
     * LDC "\u65b9\u6cd5\u8017\u65f6\uff1a"
     * INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
     * LLOAD 3
     * LLOAD 1
     * LSUB
     * INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
     * INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
     * INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
     *
     * @param opcode
     */
    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        System.out.println("onMethodOuter");
        if (inject) {
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));
            // 创建本地local变量
            end = newLocal(Type.LONG_TYPE);
            // 方法执行的结果保存给创建的本地变量
            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("<init>", "()V"));
            visitLdcInsn("方法耗时:");
            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;"));
            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"));
        }
    }
}

如果对于ASM的使用还有疑惑,可以去看一下上一篇的ASM的使用

 

在app#build.gradle中引用插件

apply plugin: com.dgplugin.AsmPlugin

现在运行,点击按钮,发现生效:

 

 

 

 

 

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐