Transform是Android官方插件提供给开发者在项目构建阶段件的一套api。目前典型的由class到dex转换之前修改Class应用就是字节码插桩技术
Transform的整体运行流程
这里的resource不是指的是安卓中的resource,而是指Java的资源:
不过一般的,我们都是用来处理class文件。
我们所写的配置文件一般都会被打包成一个jar包,jar包里边会包含resource资源。
另外,对于每一个transform,在build阶段,都会出现如下的log:
:app:transformClassesWithTestForDebug
其中,transform是固定的;Test表示的是当前transform的名称;ForDebug表示的是当前是在debug环境下构建的。 当前除了这种自定义的,还有很多系统的流程,这都是由安卓系统实现的:
:app:transformClassesWithDexBuilderForDebug
这个就是class转化成dex文件。
我们自定的transform一般是在最开始执行的。这也是需要的,因为我们改完之后的代码需要被加载进入dex文件才能正常运行,实现编译时修改代码的能力。
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//清理文件
outputProvider.deleteAll()
def inputs = transformInvocation.inputs
inputs.each {
// 所有的jar文件的输入,包含了所有的非自己的模块,有aar和jar。三方的jar也在里边。
def jarInputs = it.jarInputs
jarInputs.each {
// 如果开启了增量编译(isIncremental() = true),这里会看到jar包的状态, 删除,新增等。
println (it.status)
}
// 所有的目录的输入。就是自己写的代码的,如下图所示
def dIs = it.directoryInputs
dIs.each {
// 文件有
def changeFiles = it.changedFiles
changeFiles.entrySet().each {
// 如果开启了增量编译,这里会看到类的状态, 删除,新增等。
println(it.key.name + it.value.name())
}
}
}
}
isIncremental()如果这个值为false,就相当于每一次都是初次构建。
上图就是directoryInputs的位置。
自定义的Transform的全部代码以及对应的解释:
class ASMTransform extends Transform {
@Override
public String getName() {
return "asm";
}
/**
* 处理所有class
*
* @return
*/
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
/**
* 用来处理范围,可以决定是否包含三方库等。
* 范围是整个项目所有的类,包含依赖的库
*
* @return
*/
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
/**
* 不使用增量
* @return true表示
*/
@Override
public boolean isIncremental() {
return false;
}
/**
* android插件将所有的class通过这个方法告诉给我们
* 我们这个transform的输出就是下一个transform的输入。
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//清理文件
outputProvider.deleteAll()
def inputs = transformInvocation.inputs
inputs.each {
// 所有的jar文件的输入,包含了所有的非自己的模块,有aar和jar。三方的jar也在里边。
def jarInputs = it.jarInputs
jarInputs.each {
// 如果开启了增量编译(isIncremental() = true),这里会看到jar包的状态, 删除,新增等。
println (it.status)
}
// 所有的目录的输入。就是自己写的代码的,如下图所示
def dIs = it.directoryInputs
dIs.each {
// 文件有
def changeFiles = it.changedFiles
changeFiles.entrySet().each {
// 如果开启了增量编译,这里会看到类的状态, 删除,新增等。
println(it.key.name + it.value.name())
}
}
}
}
}
ASM插桩
首先我们需要准备一份class文件,用来做插桩用,接着需要利用ClassReader这样一个类。它里边有一个accept这样一个api,通过里边的ClassVisitor,可以用来访问类,访问方法,访问注解,或者操作他们。然后通过MethodVisitor来实现插入。
例如我们想在所有的增加了@ASMTest注解的代码中增加统计执行时长的代码:
public class InjectTest {
public InjectTest() {
}
@ASMTest
public static void main(String[] var0) throws InterruptedException {
Thread.sleep(1000L);
}
}
// 我们希望修改成这样。
public class InjectTest {
public InjectTest() {
}
@ASMTest
public static void main(String[] var0) throws InterruptedException {
long var1 = System.currentTimeMillis();
Thread.sleep(1000L);
long var3 = System.currentTimeMillis();
System.out.println("execute:" + (var3 - var1));
}
}
为了实现修改,我们需要分别将上边的两段代码编译成字节码文件,查看其中的差异,然后再通过上边图中提到的方式来对字节码进行修改。 这样就实现了字节码插桩。
方法签名
这些就是字节码插桩的时候需要注意的:
例如这个指令:
INVOKESTATIC java/lang/System.currentTimeMillis ()J
我们在进行插桩的时候,需要对照表,System是一个对象,因此需要在最前边加上L, ()J表示一个方法的签名标识。 修改完成的代码就是这样的:
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
Transform与ASM联动
联动的方式就是通过实现一个Plugin来做,这相当于增加了一个gradle任务:
public class ASMPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
BaseExtension android = project.getExtensions().getByType(BaseExtension.class);
// android 插件 能够获得所有的class
// 同时他提供一个接口,能够让我们也获得所有class
android.registerTransform(new ASMTransform());
}
}
ASMTransform是我们自定义的Transform,就是通过他来实现字节码插桩:
public class ASMTransform extends Transform {
@Override
public String getName() {
return "asm";
}
/**
* 处理所有class
*
* @return
*/
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
/**
* 用来处理范围,可以决定是否包含三方库等。
* PROJECT_ONLY:范围仅仅包含我们自己写的代码中的java或者kotlin文件
*
* @return
*/
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.PROJECT_ONLY;
}
/**
* 不使用增量
* @return true表示
*/
@Override
public boolean isIncremental() {
return false;
}
/**
* android插件将所有的class通过这个方法告诉给我们
* 我们这个transform的输出就是下一个transform的输入。
* @param transformInvocation
* @throws TransformException
* @throws InterruptedException
* @throws IOException
*/
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
// 因为不是增量构建,所以可以对之前的文件进行清理
outputProvider.deleteAll();
// 得到所有的输入
Collection<TransformInput> inputs = transformInvocation.getInputs();
for (TransformInput input : inputs) {
// 处理class目录
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
// 直接复制输出到对应的目录
String dirName = directoryInput.getName();
File src = directoryInput.getFile();
System.out.println("目录:" + src);
String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());
File dest = outputProvider.getContentLocation(dirName + md5Name/*用来作为输出的唯一标记,为什么要做么做?这是因为Transform是一个一个执行的,
上一个作为下一个输入,所以我们将通过Transform得到的结果写入 outputprovider 获得的一个file中去,然后outputprovider获取的文件的第一个参数就需要给一个唯一的标记
*/,
directoryInput.getContentTypes()/*类型*/, directoryInput.getScopes()/*作用域*/,
Format.DIRECTORY);
// todo 插桩
processInject(src, dest);
}
// 处理jar(依赖)的class todo 先不处理了
for (JarInput jarInput : input.getJarInputs()) {
String jarName = jarInput.getName();
File src = jarInput.getFile();
System.out.println("jar包:" + src);
String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4);
}
File dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
FileUtils.copyFile(src, dest);
}
}
}
private void processInject(File src, File dest) throws IOException {
String dir = src.getAbsolutePath();
FluentIterable<File> allFiles = FileUtils.getAllFiles(src);
for (File file : allFiles) {
FileInputStream fis = new FileInputStream(file);
// 插桩
ClassReader cr = new ClassReader(fis);
// 写出器
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 分析,处理结果写入cw
cr.accept(new ClassInjectTimeVisitor(cw,file.getName()), ClassReader.EXPAND_FRAMES);
byte[] newClassBytes = cw.toByteArray();
// class 文件
String absolutePath = file.getAbsolutePath();
// class文件的绝对地址去掉目录,得到的全类名.
String fullClassPath = absolutePath.replace(dir, "");
// 完成文件覆盖
File outFile = new File(dest, fullClassPath);
FileUtils.mkdirs(outFile.getParentFile());
FileOutputStream fos = new FileOutputStream(outFile);
fos.write(newClassBytes);
fos.close();
}
}
}
ClassInjectTimeVisitor.java
package com.enjoy.asm.plugin;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class ClassInjectTimeVisitor extends ClassVisitor {
private String className;
public ClassInjectTimeVisitor(ClassVisitor cv, String fileName) {
super(Opcodes.ASM5, cv);
className = fileName.substring(0,fileName.lastIndexOf("."));
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
return new MethodAdapterVisitor(mv, access, name, desc,className);
}
}
MethodAdapterVisitor.java
package com.enjoy.asm.plugin;
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;
public class MethodAdapterVisitor extends AdviceAdapter {
private String className;
private String methodName;
private boolean inject;
private int index;
private int start, end;
protected MethodAdapterVisitor(MethodVisitor mv, int access, String name, String desc,
String className) {
super(Opcodes.ASM5, mv, access, name, desc);
methodName = name;
this.className = className;
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
// 记录方法是不是被 注解
if ("Lcom/example/transformapi/InjectTime;".equals(desc)) {
inject = true;
}
return super.visitAnnotation(desc, visible);
}
@Override
protected void onMethodEnter() {
if (inject) {
// 0: invokestatic #2 // Method java/lang/System.currentTimeMillis:()J
// 3: lstore_1
//储备本地变量备用
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
index = newLocal(Type.LONG_TYPE);
start = index;
mv.visitVarInsn(LSTORE, start);
}
}
@Override
protected void onMethodExit(int opcode) {
if (inject) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
index = newLocal(Type.LONG_TYPE);
end = index;
mv.visitVarInsn(LSTORE, end);
// getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
//获得静态成员 out
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// new #4 // class java/lang/StringBuilder
// 引入类型 分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
//invokevirtual #7 // Method java/lang/StringBuilder.append:
// (Ljava/lang/String;)Ljava/lang/StringBuilder;
// 执行构造方法
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>",
"()V", false);
// ldc #6 // String execute:
// 把常量压入栈顶 后面使用
mv.visitLdcInsn("==========>"+className + " execute " + methodName + ": ");
//invokevirtual #7 // Method java/lang/StringBuilder.append: (Ljava/lang/String;)
// Ljava/lang/StringBuilder;
// 执行append方法,使用栈顶的值作为参数
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// lload_3 获得存储的本地变量
// lload_1
// lsub 减法指令
mv.visitVarInsn(LLOAD, end);
mv.visitVarInsn(LLOAD, start);
mv.visitInsn(LSUB);
// invokevirtual #8 // Method java/lang/StringBuilder.append:(J)
// Ljava/lang/StringBuilder;
// 把减法结果append
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(J)Ljava/lang/StringBuilder;", false);
//append "ms."
mv.visitLdcInsn("ms.");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
//tostring
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString",
"()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
"(Ljava/lang/String;)V", false);
}
}
}
整体结构就是,通过注解标记哪些方法需要插桩,完成插桩代码编写之后,通过Transform遍历class插桩代码插入到class文件中。最后在编译的时候自动运行这个任务完成自动插桩。
build.gradle
plugins {
id 'java-library'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation libs.gradle
implementation gradleApi()
}
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
apply plugin: 'maven-publish'
publishing {
publications {
ASMPlugin(MavenPublication) {
from components.java
groupId 'com.enjoy.plugin'
artifactId 'asm'
version '1.0'
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
app测在使用的时候先创建一个注解:
InjectTime.java
package com.example.transformapi;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface InjectTime {
}
MainActivity.java
package com.example.transformapi;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@InjectTime
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
a();
}
@InjectTime
void a() {
try {
Thread.sleep(2_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}