Android开发Android开发Android开发经验谈

ASM+Transform 字节码插桩完成函数耗时统计插件

2023-11-25  本文已影响0人  奔跑吧李博
Transform

Transform的作用:是用来替换(或转换)Class
利用Transform将旧的class文件取出来,再用AMS修改class的字节码,最后替换成我们新的class文件。

android gradle 插件自从1.5.0-beta1版本开始就包含了一个Transform API,允许第三方插件在编译后的类文件转换为dex文件之前做处理操作。

注册Transform

在我们的gradle插件中,通过android.registerTransform(theTransform)

class StatisticsPlugin implements Plugin<Project> {
    void apply(Project project) {

        AppExtension appExtension = project.extensions.findByType(AppExtension.class)
        appExtension.registerTransform(new MyTransform())
    }
}
Transform的使用

自定义的Transform继承于com.android.build.api.transform.Transform

class MyTransform(val project: Project) : Transform() {

    private var SCOPES: MutableSet<QualifiedContent.Scope> = mutableSetOf()

    init {
        SCOPES.add(QualifiedContent.Scope.PROJECT)
        SCOPES.add(QualifiedContent.Scope.SUB_PROJECTS)
        SCOPES.add(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
    }

    /**
     * transform 名字
     */
    override fun getName(): String {
        return "xxx"
    }

    /**
     * 输入文件的类型
     */
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指定作用范围
     */
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return SCOPES
    }

     /**
     * 是否支持增量
     */
    override fun isIncremental(): Boolean {
        return false
    }

    /**
     * transform的执行
     */
    override fun transform(transformInvocation: TransformInvocation?) {
         transformInvocation?.inputs?.forEach {
          // 项目中编写的代码
          it.directoryInputs.forEach {directoryInput->
              with(directoryInput){
                 //字节码操作
                 ......
              }
          }

          // 项目中引入第三方Jar包的代码
          it.jarInputs.forEach { jarInput->
              with(jarInput){
                 //字节码操作
                 ......
              }
          }
        }
    }
}
作用域

通过Transform#getScopes指定的作用域:


作用对象

通过Transform#getInputTypes指定的作用对象


转换transform

Transform插桩主要是在override fun transform(transformInvocation: TransformInvocation?) 执行完成,对于有代码的地方都需要扫描到。

TransformInvocation

我们通过实现Transform#transform方法来处理我们的中间转换过程, 而中间相关信息都是通过TransformInvocation对象来传递

 // transform的上下文
    @NonNull
    Context getContext();

    // 返回transform的输入源
    @NonNull
    Collection<TransformInput> getInputs();

    // 返回引用型输入源
    @NonNull Collection<TransformInput> getReferencedInputs();
    
    //额外输入源
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    //输出源
    @Nullable
    TransformOutputProvider getOutputProvider();

    //是否增量
    boolean isIncremental();

示例:

对全文件字节码扫描进行函数耗时统计。

插件中注册transform,然后apply form引用插件。

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class StatisticsPlugin implements Plugin<Project> {
    void apply(Project project) {

        AppExtension appExtension = project.extensions.findByType(AppExtension.class)
        appExtension.registerTransform(new MyTransform())
    }
}

MyTransform类,进行全局class文件夹扫描。

public class MyTransform extends Transform {

    @Override
    public String getName() {
        //名字,输出的文件会默认生成在这个名字的目录下,比如:MyPlugin\app\build\intermediates\transforms\MyTransform..
        return "MyTransform";
    }

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

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);

        //可以从中获取jar包和class文件夹路径。需要输出给下一个任务
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        //OutputProvider管理输出路径
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        //遍历所有的输入,有两种类型,分别是文件夹类型(也就是我们自己写的代码)和jar类型(引入的jar包),这里我们只处理自己写的代码。
        for (TransformInput input: inputs) {
            //遍历所有文件夹
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                //获取transform的输出目录,等我们插桩后就将修改过的class文件替换掉transform输出目录中的文件,就达到修改的效果了。
                File dest = outputProvider.getContentLocation(directoryInput.getName(),
                        directoryInput.getContentTypes(), directoryInput.getScopes(),
                        Format.DIRECTORY);
                transformDir(directoryInput.getFile(), dest);
            }
        }
    }

    /**
     * 遍历文件夹,对文件进行插桩
     * @param input 源文件
     * @param dest  源文件修改后的输出地址
     * @throws IOException
     */
    private static void transformDir(File input, File dest) throws IOException {
        if (dest.exists()) {
            FileUtils.delete(dest);
        }
        dest.mkdirs();
        String srcDirPath = input.getAbsolutePath();
        String destDirPath = dest.getAbsolutePath();
        File[] fileList = input.listFiles();
        if (fileList == null) {
            return;
        }

        for (File file : fileList) {
            String destFilePath = file.getAbsolutePath().replace(srcDirPath, destDirPath);
            File destFile = new File(destFilePath);
            if (file.isDirectory()) {
                //如果是文件夹,继续遍历
                transformDir(file, destFile);
            } else if (file.isFile()) {
                //创造了大小为0的新文件,或者,如果该文件已存在,则将打开并删除该文件关闭而不修改,但更新文件日期和时间
//                FileUtils.touch(destFile);
                MyASMCost.asmHandleFile(file.getAbsolutePath(), destFile.getAbsolutePath());
            }
        }
    }
}

使用ClassReader,ClassWriter,ClassVisitor进行文件读取,在方法前后做字节码插桩操作。

public class MyASMCost {

    /**
     * 通过ASM进行插桩
     * @param inputPath 源文件路径
     * @param destPath  输出路径
     */
    public static void asmHandleFile(String inputPath, String destPath) {
        try {
            File file = new File(inputPath);
            FileInputStream fis = new FileInputStream(file);
            //将class文件转成流
            ClassReader cr = new ClassReader(fis);
            //ClassWriter.COMPUTE_FRAMES 参数意义: 自动计算栈帧 和 局部变量表的大小
            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

            //执行分析
            cr.accept(new MyClassVisitor(Opcodes.ASM5, cw), ClassWriter.COMPUTE_FRAMES);

            //执行了插桩之后的字节码数据输出
            byte[] bytes = cw.toByteArray();
            FileOutputStream fos = new FileOutputStream(destPath);
            fos.write(bytes);
            fos.close();

        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static class MyClassVisitor extends ClassVisitor {

        public MyClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            //类似于动态代理的机制,会将执行的方法进行回调,然后在方法执行之前和之后做操作
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
        }

    }

    static class MyMethodVisitor extends AdviceAdapter {
        private int startTimeId = -1;
        /**
         * 用变量区分方法是否需要执行插桩
         */
        boolean inject = false;
        private String methodName = null;

        protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
            methodName = name;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            //descriptor为方法的注解类型 行如: Lcom/example/bytecodeProject/ASMTest
            //如果方法的注解为ASMTest,则执行插桩代码
            if (descriptor.equals("Lcom/example/asmbytecode/simpledemo/ASMTest")) {
                inject = true;
            }
            return super.visitAnnotation(descriptor, visible);
        }

        @Override
        protected void onMethodEnter() {  //代码插入到方法头部
            super.onMethodEnter();

            if (!inject) {
                return;
            }

            //在Java kotlin中写代码直接写,但是ASM写代码有最大区别,就是需要用方法签名的格式来写。

            //long l = System.currentTimeMillis();
            //要写如上一行代码的字节码,需要执行一个静态方法,,类是System,方法名是currentTimeMillis,所以有如下代码:
            startTimeId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitIntInsn(LSTORE, startTimeId);
        }

        @Override
        protected void onMethodExit(int opcode) { //代码插入到方法结尾
            super.onMethodExit(opcode);

            if (!inject) {
                return;
            }

            int durationId = newLocal(Type.LONG_TYPE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LLOAD, startTimeId);
            mv.visitInsn(LSUB);
            mv.visitVarInsn(LSTORE, durationId);
            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("The cost time of " + methodName + "() is ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, durationId);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitLdcInsn(" ms");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

        }
    }
}

参考:
https://blog.csdn.net/qq_30379689/article/details/127986526
https://juejin.cn/post/7129381154121056292

上一篇 下一篇

猜你喜欢

热点阅读