TransformAPI + ASM实现自动插桩

Transform是Android官方插件提供给开发者在项目构建阶段件的一套api。目前典型的由class到dex转换之前修改Class应用就是字节码插桩技术

Transform的整体运行流程

image.png

这里的resource不是指的是安卓中的resource,而是指Java的资源:

image.png

不过一般的,我们都是用来处理class文件。

我们所写的配置文件一般都会被打包成一个jar包,jar包里边会包含resource资源。

image.png

另外,对于每一个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,就相当于每一次都是初次构建。

image.png

上图就是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插桩

image.png

首先我们需要准备一份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));
    }
}

为了实现修改,我们需要分别将上边的两段代码编译成字节码文件,查看其中的差异,然后再通过上边图中提到的方式来对字节码进行修改。 这样就实现了字节码插桩。

方法签名

这些就是字节码插桩的时候需要注意的:

image.png

例如这个指令:

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();
        }
    }
}

源码:GitHub - xingchaozhang/Transform-ASM

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值