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

上一篇 ASM的使用

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

ASM + Transform 在android中的使用_第1张图片

下面分三点进行介绍:

本文大纲:

  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 {    @Override    public void apply(Project project) {       // 这里是插件的入口,在此做插件的处理       System.out.println("==============我是AsmPlugin插件=============")    }}

 

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

ASM + Transform 在android中的使用_第2张图片

 

 

至此,buildSrc插件就完成了。

 

使用buildSrc插件:

使用十分简单

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

 

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

 

ASM + Transform 在android中的使用_第3张图片

注意这里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处于哪个位置:

ASM + Transform 在android中的使用_第4张图片

 

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执行流程:

 

ASM + Transform 在android中的使用_第5张图片

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

ASM + Transform 在android中的使用_第6张图片

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定义了它的枚举

ASM + Transform 在android中的使用_第7张图片

 

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 {    @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 inputs = transformInvocation.getInputs();        // 引用型输入,无需输出        Collection referencedInputs = transformInvocation.getReferencedInputs();        // 管理输出路径,如果消费型输入为空,你会发现OutputProvider==null        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();        // 当前是否是增量编译        boolean incremental = transformInvocation.isIncremental();        /*            进行读取class和jar, 并做处理         */        for (TransformInput input : inputs) {            // 处理class            Collection 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 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 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()     * 

* 使用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)); *

* 使用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. ()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("", "()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

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

 

 

 

 

 

更多相关文章

  1. android调用Webservice方法
  2. Android 文件读写操作方法总结
  3. 低版本android project在高版本ADK中运行方法
  4. Android中应用界面主题Theme使用方法和页面定时跳转应用
  5. G1安装android软件方法 (实机运行)
  6. android设置主题和自定义主题的方法
  7. android intent使用方法
  8. Android webkit webkit中skia的使用方法简析
  9. 安装Android Studio遇到中文乱码的解决方法

随机推荐

  1. ADT-bundle(Android(安卓)Development To
  2. android 引入jni 的so库的方法
  3. [Android]安装环境
  4. Android中自定义SeekBar来控制音量,并与系
  5. android studio template模块开发
  6. Android(安卓)短信的一些关键字
  7. 介绍两个Android开源项目:Android显示GIF
  8. android之selector使用
  9. Android如何调用第三方SO库
  10. Android(安卓)修改TextView字体样式