Gradle 插件與ASM入門

前言

  須要瞭解一點gradle知識,一點groovy語言,簡單的ASM知識,這個插件的功能只是用ASM在編譯期間插入代碼,作簡單的方法執行時間統計。java

主要內容android

  1. 自定義插件
  2. 使用ASM插入代碼
  3. 統計方法耗時

首先咱們先看一張經典的打包流程圖:git

  咱們此次要乾的就是在.class文件轉爲Dex以前作代碼插入,來達到編譯時插入代碼。

那麼問題來了:github

  我怎麼知道何時生成了.class文件,並且還要是沒轉成dex?bash

  怎麼在編譯時候插入代碼?app

帶着這兩個問題,往下走:ide

  在gradle插件1.5.0-beta1版本時候,提供了一個Transform API,這個API專門就是爲了第三方插件對編譯後class文件轉爲dex以前而提供的,直接擼一個代碼,由於是插件因此直接新建一個module,命名爲buildSrc,至於爲啥要叫BuildSrc是由於這是Android保留給自定義plugin的名字,須要新建一個放插件的目錄,都是用groovy語言寫的全部目錄層級以下圖:    學習

固然還須要新建一個build.gradle裏面以下圖:   
注:這裏面懶的去找asm的依賴,就直接用的 android.tools.build裏面的asm。

  而後就能夠開始寫groovy腳本了,既然前面說了是用Transform API那麼就來繼承這個API,還須要實現Plugin這個接口,plugin這個接口很是重要是用來把咱們這個自定義的插件註冊到project的task中,回到transform中,這個類須要實現getName,getInputTypes,getScopes,isIncremental四個抽象方法,還有一個tranform方法:gradle

getInputTypes():限定輸入文件的類型(例如:class,jar,dex等)
getScopes():限定文件所在的區域(例如:全部project,只有主工程等)
isIncremental():是否增量更新
getName():在控制檯打印的transform名字(只是把這個名字拼接上去而已,例如:transformClassesWith+name+ForDebug)
transform(TransformInvocation transformInvocation):這方法纔是真正的插件實現
複製代碼

在這裏面transform方法中就能獲得全部的.class還有jar具體代碼以下:ui

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文件編譯生成的字節碼文件:

上圖只是隨便截了一段,臥槽,這讓我本身寫,或者本身去修改,告辭,打擾,等我再修煉兩年,那麼我又想去改,可是我又寫不來怎麼辦呢,這時候就須要瞭解一下as插件 ASM Bytecode Outline,簡單粗暴,一鍵生成修改字節碼的代碼:

看到這個插件生成了三個,一個是字節碼,一個是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來關聯本身的插件:

我這裏插件名字叫time-total,因此我在app的build.gradle裏面直接 apply plugin: 'time-total'這樣就能使用這個插件。

最後附上一張運行結果圖:

小小的總結:

1.先建立buildSrc文件夾,建立插件
2.使用asm生成代碼
3.cv代碼到自定義的ClassVisitor😜
4.app的build.gradle引用插件
複製代碼

遇到的坑:

  • 在groovy下面新建的groovy文件必定要.groovy結尾否則在編譯時候不會生成在build文件夾裏面,致使找不到類。
  • 以前在CompileSdkVerison >=28時候會報dexMeger失敗,因此只要改爲28一下就沒事了,我猜多是由於androidx的緣由吧。
  • 在咱們自定義ClassVisitor裏經過註解來判斷是否須要統計時,要注意,兩個註解描述要同樣,也就是包名+名字。

感謝

巴神的文章

總結

  雖然第一次玩這一類東西,可是感受也是收穫良多,對gradle又加深了些瞭解,學習的過程是痛苦的,可是最後作出來倒是欣慰和知足,僅此作個記錄。

  最後項目github地址:github.com/kgxl/TimeCo…

相關文章
相關標籤/搜索