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 super QualifiedContent.Scope> getScopes() {
return null;
}
@Override
public boolean isIncremental() {
return false;
}
}
在gradle插件库中,为我们定义了许多的Transform,我们可以看Transform的实现类:
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 super QualifiedContent.Scope> 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,来查看一个类的字节码:
安装插件之后,重启AndroidStudio就可以使用插件了,要想查看一个类的字节码,我们必须在编写一个类之后,重新编译Gradle一次,让其生成对应java文件的class文件,然后ASM插件才能查看字节码:选中一个类之后,点击鼠标右键,选择ASM ByteCode Viewer,等待一会就会在右侧出现对应类的字节码文件:
接下来我们就来利用ASM进行字节码插桩
首先左侧的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插件,对着插件生成的字节码一行一行的写,在插件中:
Bytecode 栏显示类的字节码,ASMified显示使用ASM 实现的字节码,我们可以对着ASMified这一栏一行一行的写代码
其实就是按照这个字节码一个个翻译一下,其中"java/lang/System"要修改为方法签名所以在修改字节码时候要修改为”Ljava/lang/System;“
其中L代表引用类型。而且最后面的分号还不能少(System后面的分号’;‘),签名中的特殊字符的代表的意思。
也可以使用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
然后编译整个工程,就会对我们自己编写的代码进行字节码插桩了
我们可以看到的确在构造函数中插入了一段代码。
我们在开发Android项目的时候,当编译项目指挥,都会在底部控制台的build栏里面看到很多任务的执行,例如:
这个时候我们就应该明白这些任务是干什么的。我们就可以通过:
project.getTasks().findByName(****) ; // *****代表任务的名称
得到当前的任务,得到当前的任务之后,我们就可以通过任务得到这个任务的输入输出,之后就可以操作这个任务的输入输出了。
例如,我们得到混淆的任务
final Task proguardTask = project.getTasks().findByName("transformClassesAndResourcesWithProguardForRelease" ); TaskOutputs outputs = proguardTask.getOutputs(); Set
files = outputs.getFiles().getFiles();
我们就可以得到混淆任务的输出文件集合,最后通过遍历files的文件集合,就可以得到mapping文件。
Demo传送门