ASM + Transform 在android中的使用
参考:https://juejin.im/post/5cc3db486fb9a03202222154
上一篇 ASM的使用
上一篇说到了am的使用,但是局限于对于特定class文件使用,但是在android中不能每个class都那样做。借助gradle插件和transfrom,我们可以干预android的打包过程,从中拿到所有class,从而进行插桩。
下面分三点进行介绍:
本文大纲:
- gradle插件简单介绍
- Transform的介绍
- 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插件=============") }}
现在的目录结构基本是这样:
至此,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 { @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
现在运行,点击按钮,发现生效:
更多相关文章
- Android(安卓)网络请求简单使用方式
- lua学习笔记 3 android调用Lua。Lua脚本使用LoadLib回调Java,并
- 【不负初心】Android初中高级开发工程师面试复习点
- Android(安卓)使用Parcelable序列化对象
- Android(安卓)Studio 使用笔记(1) -- 设置自动生成serialVersionUI
- Android中LocationManager的简单使用,获取当前位置
- 箭头函数的基础使用
- NPM 和webpack 的基础使用
- Python list sort方法的具体使用