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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容