Android代码插桩与自定义插件使用

Android代码插桩与自定义插件使用

Android 代码插桩,可以在源文件的任意位置插入自定义代码。

由于插入代码需要使用 groovy 脚本编写一些插件,所以可以先了解插件相关知识:Gradle 脚本相关知识

asm插桩项目准备工作

在这里,首选新建一个空白的项目。一般来说 AndroidStudio 空白项目跑起来会是 Hello World ,使用此空白项目进行测试

asm 插桩需要入口,所以,这里使用 buildSrc 默认路径的方式,进行脚本编写。

首先在项目根目录,创建一个文件夹名称:buildSrc

这个文件夹必须在项目根目录,与 app 目录同级
文件夹名称必须使用 buildSrc 。这是默认的插件目录

然后在 buildSrc 目录下创建子目录:

buildSrc
|----src
|----|----main
|----|----|----java

然后在 java 目录下再次创建子目录(此时的子目录是java文件的包路径)

然后在 buildSrc 目录下新建文件 build.gradle ,文件内容如下:


apply plugin: 'java-library'

sourceSets {
    main{
//      groovy{
//            srcDir 'src/main/groovy'
//        }
        java{
            srcDir 'src/main/java'
        }
        resources {
            srcDir 'src/main/resources'
        }
    }
}

//获取远程库的仓库地址
repositories {
    jcenter()
    mavenCentral()
    maven { url "https://dl.google.com/dl/android/maven2/" }
}
//开发插件需要的库
dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.0.1'

    implementation 'org.ow2.asm:asm:7.0'
    implementation 'org.ow2.asm:asm-commons:7.0'
}

在一些文章中,可能使用的是 groovy 编写脚本。而此处我使用的是 java 编写脚本。其中:dependencies 中的依赖项都是需要的。否则在编写过程中可能会出现类找不到的问题

编写插件

编写一个 AsmTransform

文件内容如下:

package plugin;

import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Transform;

import java.util.Set;

public class AsmTransform extends Transform {
    @Override
    public String getName() {
        return null;
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return null;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return null;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }
}

编写一个 Task 入口文件


package plugin;


import com.android.build.gradle.AppExtension;

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class Main implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        System.out.println("test");
        AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);
//        println '----------- 开始注册 >>>>> -----------'
        AsmTransform transform = new AsmTransform();
        appExtension.registerTransform(transform);
    }
}

配置task

当上方两个的 Transform 和 Main 入口文件编写完成后,在 app 项目中引用:

打开项目根目录/app 目录的 build.gradle 文件

在文件最末尾添加:

apply plugin: Main

这里的 Main 就是编写的入口文件名称,如果取名是其它的,则自行修改。

在编写后,会提示无法找到 Main 的java 文件,使用智能提示即可正确导入,一般会在文件最前面导入路径:

import plugin.Main

如果包名路径不同,则 import 的路径也会不同

实现代码插入功能

当上方的配置都完成后,已经可以实现代码注入了。接下来只要实现 AsmTransform 中的方法即可

分别修改 AsmTransform.java 文件中的四个方法:

    @Override
    public String getName() {
        return "AsmTransform";
    }

    @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 false;
    }

然后再重新实现 transform 方法:

        System.out.println("start trans form");

        long startTime = System.currentTimeMillis();
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        //删除旧的输出
        if (outputProvider != null) {
            outputProvider.deleteAll();
        }
        inputs.forEach(transformInput -> {
            transformInput.getDirectoryInputs().forEach(directoryInput -> {
                handleDirectoryInput(directoryInput, outputProvider);
            });
            transformInput.getJarInputs().forEach(jarInput -> {
                try {
                    handleJarInput(jarInput, outputProvider);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
        });
        long endTime = System.currentTimeMillis();
        System.out.println("end time:" + (endTime - startTime));

一些新建的方法实现如下:


    static void transJavaFile(File file) {
        String name = file.getName();
        if (isClassFile(name)) {
            System.out.println("-------------------- handle class file:<$name> --------------------" + file.getName());
            try {
                ClassReader classReader = new ClassReader(new FileInputStream(file));
                ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
                ClassVisitor classVisitor = new ActivityClassVisitor(classWriter);
                classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES);
                byte[] bytes = classWriter.toByteArray();
                FileOutputStream fileOutputStream = new FileOutputStream(file.getParentFile().getAbsolutePath() + File.separator + name);
//                FileOutputStream fileOutputStream = new FileOutputStream(name);

                fileOutputStream.write(bytes);
                fileOutputStream.close();
                System.out.println("-------------------- handle class file: success --------------------");
            } catch (IOException e) {
                System.out.println("-------------------- handle class file: error --------------------");
                e.printStackTrace();
            }

        }
    }

    static void getAllFile(File inFile) {
        if (inFile.isDirectory()) {
            for (File fileChild : Objects.requireNonNull(inFile.listFiles())) {
                getAllFile(fileChild);
            }
        } else {
            transJavaFile(inFile);
        }
    }

    /**
     * 处理目录下的class文件
     *
     * @param directoryInput
     * @param outputProvider
     */
    static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        //是否为目录
        if (directoryInput.getFile().isDirectory()) {
            //列出目录所有文件(包含子文件夹,子文件夹内文件)
            getAllFile(directoryInput.getFile());
        }
        File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
        try {
            FileUtils.copyDirectory(directoryInput.getFile(), dest);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理Jar中的class文件
     *
     * @param jarInput
     * @param outputProvider
     */
    static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider) throws IOException {
        if (jarInput.getFile().getAbsolutePath().endsWith(".jar")) {
            //重名名输出文件,因为可能同名,会覆盖
            String jarName = jarInput.getName();
            String md5Name = DigestUtils.md5Hex(jarInput.getFile().getAbsolutePath());
            if (jarName.endsWith(".jar")) {
                jarName = jarName.substring(0, jarName.length() - 4);
            }
            JarFile jarFile = new JarFile(jarInput.getFile());
            Enumeration<JarEntry> enumeration = jarFile.entries();
            File tempFile = new File(jarInput.getFile().getParent() + File.separator + "temp.jar");
            //避免上次的缓存被重复插入
            if (tempFile.exists()) {
                tempFile.delete();
            }
            JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempFile));
            //保存
            while (enumeration.hasMoreElements()) {
                JarEntry jarEntry = enumeration.nextElement();
                String entryName = jarEntry.getName();
                ZipEntry zipEntry = new ZipEntry(entryName);
                InputStream inputStream = jarFile.getInputStream(zipEntry);
                if (isClassFile(entryName)) {
                    System.out.println("-------------------- handle jar file:<$entryName> --------------------");
                    jarOutputStream.putNextEntry(zipEntry);
                    ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream));
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
                    ClassVisitor classVisitor = new ActivityClassVisitor(classWriter);
                    classReader.accept(classVisitor, org.objectweb.asm.ClassReader.EXPAND_FRAMES);
                    byte[] bytes = classWriter.toByteArray();
                    jarOutputStream.write(bytes);
                } else {
                    jarOutputStream.putNextEntry(zipEntry);
                    jarOutputStream.write(IOUtils.toByteArray(inputStream));
                }
                jarOutputStream.closeEntry();
            }
            jarOutputStream.close();
            jarFile.close();
            File dest = outputProvider.getContentLocation(jarName + "_" + md5Name, jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
            FileUtils.copyFile(tempFile, dest);
            tempFile.delete();
        }
    }

    /**
     * 判断是否为需要处理class文件
     *
     * @param name
     * @return
     */
    static boolean isClassFile(String name) {
        return (name.endsWith(".class") && !name.startsWith("R$")
                && "R.class" != name && "BuildConfig.class" != name && name.contains("Activity"));
    }

上方代码的基本逻辑是,遍历每一个 class 以及 jar 文件。然后通过判断,看是否是当前需要修改的 class 文件。

然后将 class 文件的相关信息传入 Visitor 中进行判断修改:

ActivityClassVisitor 实现如下:

package plugin;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class ActivityClassVisitor extends ClassVisitor implements Opcodes {

    private String mClassName;

    private static final String CLASS_NAME_ACTIVITY = "com/example/plugintest/MainActivity";

    private static final String METHOD_NAME_ONCREATE = "onCreate";

    private static final String METHOD_NAME_ONDESTROY = "onDestroy";

    public ActivityClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        mClassName = name;
        System.out.println("-------------------- ActivityClassVisitor,visit mClassName:" + mClassName +
                " --------------------");
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
        if (CLASS_NAME_ACTIVITY.equals(mClassName)) {
            if (METHOD_NAME_ONCREATE.equals(name)) {
                System.out.println("-------------------- ActivityClassVisitor,visit method:" + name +
                        " --------------------");
                return new ActivityOnCreateMethodVisitor(Opcodes.ASM5, methodVisitor);
            } else if (METHOD_NAME_ONDESTROY.equals(name)) {
                System.out.println("-------------------- ActivityClassVisitor,visit method:" + name +
                        " --------------------");
                return new ActivityOnDestroyMethodVisitor(Opcodes.ASM5, methodVisitor);
            }
        }
        return methodVisitor;
    }
}

在这个 ActivityClassVisitor 中,首先判断是否是当前我要修改的文件。如果是,则往两个方法中插入代码,这明哥方法名是:onCreate, onDestory

他们的修改器实现如下:

ActivityOnCreateMethodVisitor 修改器

package plugin;


import org.objectweb.asm.MethodVisitor;

import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.POP;

public class ActivityOnCreateMethodVisitor extends MethodVisitor {
    public ActivityOnCreateMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv);
    }

    @Override
    public void visitCode() {
        System.out.println("-------------------- ActivityOnCreateMethodVisitor,visitCode --------------------");
        mv.visitLdcInsn("ASMPlugin");
        mv.visitLdcInsn("-------------------- MainActivity onCreate --------------------");
        mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" +
                "Ljava/lang/String;)I", false);
        mv.visitInsn(POP);
        System.out.println("-------------------- ActivityOnCreateMethodVisitor,visitCode over --------------------");
        super.visitCode();
    }

    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}

ActivityOnDestroyMethodVisitor 修改器代码:

package plugin;

import org.objectweb.asm.MethodVisitor;

import static groovyjarjarasm.asm.Opcodes.INVOKESTATIC;
import static groovyjarjarasm.asm.Opcodes.POP;

public class ActivityOnDestroyMethodVisitor extends MethodVisitor {
    public ActivityOnDestroyMethodVisitor(int api, MethodVisitor mv) {
        super(api, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();

        mv.visitLdcInsn("ASMPlugin");
        mv.visitLdcInsn("-------------------- MainActivity onDestroy --------------------");
        mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;" +
                "Ljava/lang/String;)I", false);
        mv.visitInsn(POP);
    }

    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}

点击运行,在执行 Task :app:transformClassesWithAsmTransformForDebug 时,会往 class 插入自定义的代码。

然后在控制台即可看到日志输出:

2021-01-05 12:09:16.207 13686-13686/com.example.plugintest I/ASMPlugin: -------------------- MainActivity onCreate --------------------

至此,已经实现了往 Activity 中插入一行日志的功能。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://zwc365.com/2021/01/05/android代码插桩使用

Buy me a cup of coffee ☕.