Gradle插件与ASM入门

前言

需要了解一点gradle知识,一点groovy语言,简单的ASM知识,这个插件的功能只是用ASM在编译期间插入代码,做简单的方法执行时间统计。

主要内容

  1. 自定义插件
  2. 使用ASM插入代码
  3. 统计方法耗时

首先我们先看一张经典的打包流程图:

image

  我们这次要干的就是在.class文件转为Dex之前做代码插入,来达到编译时插入代码。

那么问题来了:

我怎么知道什么时候生成了.class文件,而且还要是没转成dex?

怎么在编译时候插入代码?

带着这两个问题,往下走:

在gradle插件1.5.0-beta1版本时候,提供了一个Transform API,这个API专门就是为了第三方插件对编译后class文件转为dex之前而提供的,直接撸一个代码,因为是插件所以直接新建一个module,命名为buildSrc,至于为啥要叫BuildSrc是因为这是Android保留给自定义plugin的名字,需要新建一个放插件的目录,都是用groovy语言写的所有目录层级如下图:
  

image

当然还需要新建一个build.gradle里面如下图:
  
image

注:这里面懒的去找asm的依赖,就直接用的android.tools.build里面的asm。

然后就可以开始写groovy脚本了,既然前面说了是用Transform API那么就来继承这个API,还需要实现Plugin这个接口,plugin这个接口非常重要是用来把我们这个自定义的插件注册到project的task中,回到transform中,这个类需要实现getName,getInputTypes,getScopes,isIncremental四个抽象方法,还有一个tranform方法:

getInputTypes():限定输入文件的类型(例如:class,jar,dex等)
getScopes():限定文件所在的区域(例如:所有project,只有主工程等)
isIncremental():是否增量更新
getName():在控制台打印的transform名字(只是把这个名字拼接上去而已,例如:transformClassesWith+name+ForDebug)
transform(TransformInvocation transformInvocation):这方法才是真正的插件实现

在这里面transform方法中就能得到所有的.class还有jar具体代码如下:

transformInvocation.inputs.each {
            it.directoryInputs.each {
                if(it.file.isDirectory()){
                    it.file.eachFileRecurse {
                        def fileName=it.name
                        if(fileName.endsWith(".class")&&!fileName.startsWith("R\$")
                        && fileName != "BuildConfig.class"&&fileName!="R.class"){
                            //各种过滤类,关联classVisitor
                            handleFile(it)
                        }
                    }
                }
                def dest=transformInvocation.outputProvider.getContentLocation(it.name,it.contentTypes,it.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(it.file,dest)
            }
            it.jarInputs.each { jarInput->
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }

这里面我把处理.class文件提出了个handle方法:

private void handleFile(File file){
        def cr=new ClassReader(file.bytes)
        def cw=new ClassWriter(cr,ClassWriter.COMPUTE_MAXS)
        def classVisitor=new MethodTotal(Opcodes.ASM5,cw)
        cr.accept(classVisitor,ClassReader.EXPAND_FRAMES)
        def bytes=cw.toByteArray()
        //写回原来这个类所在的路径
        FileOutputStream fos=new FileOutputStream(file.getParentFile().getAbsolutePath()+File.separator+file.name)
        fos.write(bytes)
        fos.close()
    }

这里面最最最主要的就是这个自己定义的MethodTotal类,这个类里面才是真正修改.class的主要逻辑,这里我们来简单看下Java文件编译生成的字节码文件:

image

上图只是随便截了一段,卧槽,这让我自己写,或者自己去修改,告辞,打扰,等我再修炼两年,那么我又想去改,但是我又写不来怎么办呢,这时候就需要了解一下as插件ASM Bytecode Outline,简单粗暴,一键生成修改字节码的代码:

image

看到这个插件生成了三个,一个是字节码,一个是asm添加字节码的代码,还有个是groovy添加字节代码,所以我们只需要在Java文件中写好统计时间的代码然后使用这个插件生成代码就行了,那么继续走,现在也能生成编写.class文件的代码了,那么我们应该写到哪里去,是之前的transform方法还是自己定义的handle方法,当然都不是啦,是在上面的MethodTotal类,在其中做对class类的操作,里面还有一个自己定义的注解,其实就是用来过滤那些方法需要统计耗时用的,下一步就来到了,最喜欢的cv的步骤了(内容比较简单,也有注释就直接上代码了):

public class MethodTotal extends ClassVisitor {
    public MethodTotal(int i, ClassVisitor classVisitor) {
        super(i, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
        MethodVisitor methodVisitor = cv.visitMethod(i, s, s1, s2, strings);
        methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, i, s, s1) {
            boolean inject;

            @Override
            public AnnotationVisitor visitAnnotation(String s, boolean b) {
                //自定义的注解用来判断方法上的注解与TimeTotal是否为同一个注解,是否需要统计耗时
                if (Type.getDescriptor(TimeTotal.class).equals(s)) {
                    inject = true;
                }
                return super.visitAnnotation(s, b);
            }

            @Override
            protected void onMethodEnter() {
                //方法进入时期
                if (inject) {
                    //这里就是之前使用ASM插件生成的统计时间代码
                    mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                    mv.visitLdcInsn("this is asm input");
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

                    mv.visitTypeInsn(NEW, "java/lang/Throwable");
                    mv.visitInsn(DUP);
                    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Throwable", "<init>", "()V", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Throwable", "getStackTrace", "()[Ljava/lang/StackTraceElement;", false);
                    mv.visitInsn(ICONST_1);
                    mv.visitInsn(AALOAD);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement", "getMethodName", "()Ljava/lang/String;", false);
                    mv.visitVarInsn(ASTORE, 1);

                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
                    mv.visitMethodInsn(INVOKESTATIC, "com/example/asmdemo/TimeManager", "addStartTime", "(Ljava/lang/String;J)V", false);
                }
            }

            @Override
            protected void onMethodExit(int i) {
                //方法结束时期
                if (inject) {
                    //计算方法耗时
                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
                    mv.visitMethodInsn(INVOKESTATIC, "com/example/asmdemo/TimeManager", "addEndTime", "(Ljava/lang/String;J)V", false);

                    mv.visitVarInsn(ALOAD, 1);
                    mv.visitMethodInsn(INVOKESTATIC, "com/example/asmdemo/TimeManager", "calcuteTime", "(Ljava/lang/String;)V", false);
                }
            }
        };
        return methodVisitor;
    }
}

那么如此一来,整个就关联起来了,这个插件也基本成型,可以直接在app的build.gradle中使用apply plugin :完整的插件类名,当然也可以使用apply plugin:xxx引用,那我们就需要在这个buildSrc下面的main中新建resources->META-INF->gradle-plugins路径(别问为啥要这这样路径,这就规定),然后新建一个插件名.properties文件,里面使用implementation-class来关联自己的插件:

image

我这里插件名字叫time-total,所以我在app的build.gradle里面直接apply plugin: 'time-total'这样就能使用这个插件。

最后附上一张运行结果图:

image

小小的总结:

1.先创建buildSrc文件夹,创建插件
2.使用asm生成代码
3.cv代码到自定义的ClassVisitor😜
4.app的build.gradle引用插件

遇到的坑:

  • 在groovy下面新建的groovy文件一定要.groovy结尾不然在编译时候不会生成在build文件夹里面,导致找不到类。
  • 之前在CompileSdkVerison >=28时候会报dexMeger失败,所以只要改成28一下就没事了,我猜可能是因为androidx的原因吧。
  • 在我们自定义ClassVisitor里通过注解来判断是否需要统计时,要注意,两个注解描述要一样,也就是包名+名字。

感谢

巴神的文章

总结

虽然第一次玩这一类东西,但是感觉也是收获良多,对gradle又加深了些了解,学习的过程是痛苦的,但是最后做出来却是欣慰和满足,仅此做个记录。

最后项目github地址:https://github.com/kgxl/TimeCostPlugin

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

推荐阅读更多精彩内容