参考: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<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
现在运行,点击按钮,发现生效: