自定義Gradle插件檢測函數耗時

前言

上一篇文章講解了Gralde的入門知識,其中講到了如何自定義Gralde插件,本文就經過AsmTransfrom來自定義一個簡單的Gradle插件,這個Gradle插件它能夠統計方法的耗時,並當方法的耗時超過閥值時,經過Log打印在控制檯上,而後咱們經過Log能夠定位到耗時方法的位置,幫助咱們找出耗時方法,一個很簡單的功能,原理也很簡單,這其中須要使用到Asm知識和Transfrom知識,因此本文首先會介紹Asm和Transfrom相關知識點,最後再介紹如何使用Asm和Transform來實現這個Gradle插件,若是你對Asm和Transfrom已經很熟悉了,能夠跳過這兩節。java

源碼位置在文末android

運行效果

因爲這個是本地插件,因此直接在app/build.gradle中apply就行,而後能夠經過time擴展配置它(可選):git

apply plugin: com.example.plugin.TimeCostPlugin
//函數耗時閥值爲200ms,只對應用內的函數作插樁(排除第三方庫)
time{
    threshold = 200
    appPackage = 'com.example.plugindemo'
}
複製代碼

而後特地定義幾個耗時函數:github

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        try {
            method1();
            method2();
            method3();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void method1() throws InterruptedException {
        Thread.sleep(500);
    }

    public void method2() throws InterruptedException {
        Thread.sleep(300);
    }

    void method3() throws InterruptedException {
        Thread.sleep(1000);
    }
}
複製代碼

最後編譯運行一下,就會在在控制檯打印耗時函數的信息出來:web

{% asset_img plugin1.png plugin %}
點擊方法行號就能夠直接定位到耗時函數處。api

Asm

官方地址:ASM數組

官方教程:ASM4-guide(英文版)ASM4-guide(中文版)markdown

Asm是一個通用的Java字節碼操做和分析框架, 它提供了一些簡單易用的字節碼操做方法,能夠直接以二進制的形式修改現有類或動態生成類,簡單地來講,Asm就是一個字節碼操做框架,經過Asm,咱們能夠憑空生成一個類,或者修改現有的類,Asm相比其餘的字節碼操做框架如Javasist、AspectJ等的優勢就是體積小、性能好、效率高,但它的缺點就是學習成本高,不過如今已經有IntelliJ插件ASM Bytecode Outline能夠替咱們自動的生成Asm代碼,因此對於想要入門Asm的人來講,它仍是很簡單的,咱們只須要簡單的學習一下Asm的相關api的含義,在此以前但願你已經對JVM的基礎知識:類型描述符、方法描述符、Class文件結構有必定的瞭解。app

Asm中有兩類api,一種是基於樹模型的tree api,一種是基於訪問者模式的visitor api,其中visitor api是Asm最核心和基本的api,因此對於入門者,咱們須要知道visitor api的使用,在visitor api中有三個主要的類用於讀取、訪問和生成class字節碼:框架

  • ClassVisitor: 它是用於訪問calss字節碼,它裏面有不少visitXX方法,每調用一個visitXX方法,就表示你在訪問class文件的某個結構,如Method、Field、Annotation等,咱們一般會擴展ClassVisitor,利用代理模式,把擴展的ClassVisitor的每個visitXX方法的調用委託給另一個ClassVisitor,在委託的先後咱們能夠添加本身的邏輯從而達到轉換修改這個類的class字節碼的目的;

  • ClassReader:它用於讀取以字節數組形式給出的class字節碼,它有一個accept方法,用於接收一個ClassVisitor實例,accept方法內部會調用ClassVisitor的visitXX方法來訪問已讀取的class文件;

  • ClassWriter:它繼承自ClassVisitor,能夠以二進制形式生成class字節碼,它有一個toByteArray方法,能夠把已生成的二進制形式的class字節碼轉換成字節數組形式返回.

    ClassVisitor、ClassReader、ClassWriter這三個之間通常都是須要組合使用的,下面經過一些實際的例子快速掌握,首先咱們須要在build.gradle中引入Asm,以下:

    dependencies {
        //核心api,提供visitor api
        implementation 'org.ow2.asm:asm:7.0'
        //可選,提供了一些基於核心api的預約義類轉換器
        implementation 'org.ow2.asm:asm-commons:7.0'
        //可選,提供了一些基於核心api的工具類
        implementation 'org.ow2.asm:asm-util:7.0'
    }
    複製代碼

一、讀取、訪問一個類

讀取類以前,首先介紹一下ClassVisitor中的visitXX方法,ClassVisitor的主要結構以下:

public abstract class ClassVisitor {

    //ASM的版本, 版本數值定義在Opcodes接口中,最低爲ASM4,目前最新爲ASM7
    protected final int api;
  
    //委託的ClassVisitor,可傳空
    protected ClassVisitor cv;

    public ClassVisitor(final int api) {
        this(api, null);
    }
  
    public ClassVisitor(final int api, final ClassVisitor cv) {
        //...
        this.api = api;
        this.cv = cv;
    }

    //表示開始訪問這個類
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        if (cv != null) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
    }

    //表示訪問這個類的源文件名(若是有的話)
    public void visitSource(String source, String debug) {
        if (cv != null) {
            cv.visitSource(source, debug);
        }
    }

    //表示訪問這個類的外部類(若是有的話)
    public void visitOuterClass(String owner, String name, String desc) {
        if (cv != null) {
            cv.visitOuterClass(owner, name, desc);
        }
    }

    //表示訪問這個類的註解(若是有的話)
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        if (cv != null) {
            return cv.visitAnnotation(desc, visible);
        }
        return null;
    }

    //表示訪問這個類的內部類(若是有的話)
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        if (cv != null) {
            cv.visitInnerClass(name, outerName, innerName, access);
        }
    }

    //表示訪問這個類的字段(若是有的話)
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        if (cv != null) {
            return cv.visitField(access, name, desc, signature, value);
        }
        return null;
    }

    //表示訪問這個類的方法(若是有的話)
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (cv != null) {
            return cv.visitMethod(access, name, desc, signature, exceptions);
        }
        return null;
    }

    //表示結束對這個類的訪問
    public void visitEnd() {
        if (cv != null) {
            cv.visitEnd();
        }
    }
  
  	//...省略了一些其餘visitXX方法
}
複製代碼

能夠看到,ClassVisitor的全部visitXX方法都把邏輯委託給另一個ClassVisitor的visitorXX方法,咱們知道,當一個類被加載進JVM中時,它的class的大概結構以下:

{% asset_img plugin2.png plugin %}

因此把class文件結構和ClassVisitor中的方法作對比,能夠發現,ClassVisitor中除了visitEnd方法,其餘visitXX方法的訪問都對應class文件的某個結構,如字段、方法、屬性等,每一個visitXX方法的參數都表示字段、方法、屬性等的相關信息,例如:access表示修飾符、signature表示泛型、desc表示描述符、name表示名字或全權限定名,咱們還注意到有些visitXX方法會返回一個XXVisitor類實例,這些XXVisitor類裏面又會有相似的visitXX方法,這表示外部能夠繼續調用返回的XXVisitor實例的visitXX方法,從而繼續訪問相應結構中的子結構,這個後面再解釋。

知道了ClassVisitor中方法的做用後,咱們自定義一個類,使用ClassReaderClassVisitor把這個類的信息讀取、打印出來,首先自定義一個名爲OuterClass的類,以下:

@Deprecated
public class OuterClass{

    private int mData = 1;

    public OuterClass(int data){
        this.mData = data;
    }

    public int getData(){
        return mData;
    }

    class InnerClass{ }
}
複製代碼

OuterClass類有註解、字段、方法、內部類,而後再自定義一個名爲PrintClassVisitor的類擴展自ClassVisitor,以下:

public class PrintClassVisitor extends ClassVisitor implements Opcodes {

    public ClassPrinter() {
        super(ASM7);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + "{");
    }

    @Override
    public void visitSource(String source, String debug) {
        System.out.println(" source name = " + source);
    }

    @Override
    public void visitOuterClass(String owner, String name, String descriptor) {
        System.out.println(" outer class = " + name);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        System.out.println(" annotation = " + descriptor);
        return null;
    }

    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        System.out.println(" inner class = " + name);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        System.out.println(" field = "  + name);
        return null;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println(" method = " + name);
        return null;
    }

    @Override
    public void visitEnd() {
        System.out.println("}");
    }
}
複製代碼

其中Opcodes接口中定義了不少常量,ASM7就是來自Opcodes,在每一個visitXX方法把類的相關信息打印出來,最後使用ClassReader讀取OuterClass的class字節碼,在accept方法中傳入ClassVisitor實例,完成對OuterClass的訪問,以下:

public static void main(String[] args) throws IOException {
  //建立ClassVisitor實例
  ClassPrinter printClassVisitor = new ClassPrinter();
  //從構造傳入OuterClass的全權限定名,ClassReader會讀取OuterClass字節碼爲字節數組
  ClassReader classReader = new ClassReader(OuterClass.class.getName());
  //在ClassReader的accept傳入ClassVisitor實例,開啓訪問,第二個參數表示訪問模式,先不用管,傳入0
  classReader.accept(printClassVisitor, 0);
}

運行輸出:
com/example/plugindemo/OuterClass extends java/lang/Object{
 source name = OuterClass.java
 annotation = Ljava/lang/Deprecated;
 inner class = com/example/plugindemo/OuterClass$InnerClass
 field = mData
 method = <init>
 method = getData
}
複製代碼

ClassReader的構造除了能夠接受類的全限定名,還能夠接受class文件的輸入流,最終都是把class字節碼讀取到內存中,變成字節數組,ClassReader的accept方法會利用內存偏移量解析構造中讀取到的class字節碼的字節數組,把class字節碼的結構信息從字節數組中解析出來,而後調用傳入的ClassVisitor實例的visitorXX方法來訪問解析出來的結構信息,並且從運行輸出的結果能夠看出,accept方法中對於ClassVisitor的visitorXX方法的調用會有必定的順序,以visit方法開頭,以visitEnd方法結束,中間穿插調用其餘的visitXX方法,其大概順序以下:

visit 
[visitSource] 
[visitOuterClass] 
[visitAnnotation]
[visitInnerClass | visitField | visitMethod]
visitEnd

//其中[]表示可選,|表示平級
複製代碼

二、生成一個類

前面知道了ClassReader能夠用來讀取一個類,ClassVisitor能夠用來訪問一個類,而ClassWirter它能夠憑空生成一個類,接下來咱們來生成一個名爲Person的接口,該接口結構以下:

public interface Person {
    String NAME = "rain9155";
    int getAge();
}
複製代碼

使用ClassWriter生成Person接口的代碼以下:

import static org.objectweb.asm.Opcodes.*;

public class Main {

  public static void main(String[] args){
    //建立一個ClassWriter,構造傳入修改類的行爲模式,傳0就行
    ClassWriter classWriter = new ClassWriter(0);
    //生成類的頭部
    classWriter.visit(V1_7, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "com/example/plugindemo/Person", null, "java/lang/Object", null);
    //生成文件名
    classWriter.visitSource("Person.java", null);
    //生成名爲NAME,值爲rain9155的字段
    FieldVisitor fileVisitor = classWriter.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "NAME", "Ljava/lang/String;", null, "rain9155");
    fileVisitor.visitEnd();
		//生成名爲getAge,返回值爲int的方法
    MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "getAge", "()I", null, null);
    methodVisitor.visitEnd();
		//生成類完畢
    classWriter.visitEnd();
    //生成的類能夠經過toByteArray方法以字節數組形式返回
    byte[] bytes = classWriter.toByteArray();
  }
複製代碼

ClassWirter繼承自ClassVisitor,它擴展了ClassVisitor的visitorXX方法,使得它具備生成class字節碼的能力,最終toByteArray方法返回的字節數組能夠經過ClassLoader動態加載爲一個Class對象,因爲我這裏生成的是一個接口,因此getAge方法沒有方法體,因此visitMethod方法返回的MethodVisitor只是簡單的調用了visitEnd就完成了getAge方法頭的生成,若是須要生成getAge方法的內部邏輯,例如:

int getAge(){
  return 1;
}
複製代碼

那麼在調用MethodVisitor的visitEnd方法以前,還須要調用MethodVisitor的其餘visitXX方法來生成方法的內部邏輯,MethodVisitor的visitXX方法就是在模擬的JVM的字節碼指令,例如入棧、出棧等,對於visitField方法返回的FieldVisitor和visitAnnotation方法返回的AnnotationVisitor的含義和MethodVisitor相似。

能夠看到使用ClassWirter生成一個簡單的接口的代碼量就如此繁瑣,若是這是一個類,而且類中的方法有方法體,代碼會更加的複雜,所幸的是咱們能夠經過ASM Bytecode Outline插件來完成這繁瑣的過程,首先你要在你的AS或IntelliJ IDE中安裝這個插件,而後在你想要查看的Asm代碼的類右鍵 -> Show Bytecode outline,就會在側邊窗口中顯示這個類的字節碼(Bytecode)和Asm代碼(ASMified),點擊ASMified欄目就會顯示這個類的Asm碼,例以下圖就是Person接口的經過插件生成的Asm代碼:

{% asset_img plugin3.png plugin %}
能夠看到,使用ClassWriter來生成Person接口。

三、轉換一個類

ClassReader能夠用來讀取一個類,ClassVisitor能夠用來訪問一個類,ClassWirter能夠生成一個類,因此當把它們三個組合在一塊兒時,咱們能夠把class字節碼經過ClassReader讀取,把讀取到的class字節碼經過擴展的ClassVisitor轉換,轉換後,再經過ClassWirter從新生成這個類,就能夠達到轉換一個類的目的,下面咱們把前面的OuterClass類的註解經過轉換移除掉,首先自定義一個ClassVisitor,以下:

public class RemoveAnnotationClassVisitor extends ClassVisitor implements Opcodes {

    public RemoveAnnotationClassVisitor(ClassVisitor classVisitor) {
        super(ASM7, classVisitor);
    }

    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
      //返回null
      return null;
    }
}
複製代碼

這裏我只重寫了ClassVisitor的visitAnnotation方法,在visitAnnotation方法中返回null,這樣調用者就沒法使用返回的AnnotationVisitor生成類的註解,而後使用這個RemoveAnnotationClassVisitor,以下:

public static void main(String[] args) throws IOException {
  	//讀取OuterClass類的字節碼到ClassReader
		ClassReader classReader = new ClassReader(OuterClass.class.getName());
  	//定義用於生成類的ClassWriter
  	ClassWriter classWriter = new ClassWriter(0);
    //把ClassWriter傳進RemoveAnnotationClassVisitor的構造中
  	RemoveAnnotationClassVisitor removeAnnotationClassVisitor = new RemoveAnnotationClassVisitor(classWriter);
    //在ClassReader的accept方法中傳入RemoveAnnotationClassVisitor實例,開啓訪問
 	  classReader.accept(removeAnnotationClassVisitor, 0);
    //最終使用ClassWriter的toByteArray方法返回轉換後的OuterClass類的字節數組
  	byte[] bytes = classWriter.toByteArray();
}
複製代碼

上面這段代碼只是把前面所講的讀取、訪問、生成一個類的知識結合在一塊兒,ClassVisitor的構造能夠傳進一個ClassVisitor,從而代理傳進的ClassVisitor,而ClassWriter是繼承自ClassVisitor的,因此RemoveAnnotationClassVisitor代理了ClassWriter,RemoveAnnotationClassVisitor把OuterClass轉換完後就交給了ClassWriter,最終咱們能夠經過ClassWriter的toByteArray方法返回轉換後的OuterClass類的字節數組。

上面是隻有簡單的一個ClassVisitor進行轉換的代碼,若是咱們把它擴展,咱們還能夠定義RemoveMethodClassVisitor、AddFieldClassVisitor等多個具備不一樣功能的ClassVisitor,而後把全部的ClassVisitor串成一條轉換鏈,把ClassReader想象成頭,ClassWriter想象成尾,中間是一系列的ClassVisitor,ClassReader把讀取到的class字節碼通過一系列的ClassVisitor轉換後到達ClassWriter,最終被ClassWriter生成新的class,這個過程如圖:

{% asset_img plugin4.png plugin %}
Asm的入門知識就講解到這裏,若是想要了解更多關於Asm的知識請查閱開頭給出的官方教程,下面咱們來學習Transform相關知識。

Transform

官網:Transform

Transform是android gradle api中的一部分,它能夠在android項目的.class文件編譯爲.dex文件以前,獲得全部的.class文件,而後咱們能夠在Transform中對全部的.class文件進行處理,因此Transform提供了一種可讓咱們獲得android項目的字節碼的能力,如圖紅色標誌的位置爲Transform的做用點:

{% asset_img plugin5.png plugin %}
上圖就是android打包流程的一部分,而android的打包流程是交給android gradle plugin完成的,因此若是咱們想要自定義Transform,必需要注入到android gradle plugin中才能產生效果,而plugin的執行單元是Task,但Transform並非Task,那麼Transform是怎麼被執行的呢?android gradle plugin會爲每個Transform建立對應的TransformTask,由相應的TransformTask執行相應的Transform。

接下來咱們來介紹Transform,首先咱們須要在build.gradle中引入Transform,以下:

dependencies {
   	//引用android gradle api, 裏面包含transform api
    implementation 'com.android.tools.build:gradle:4.0.0'
}
複製代碼

由於transform api是android gradle api的一部分,因此咱們引入android gradle api就行,自定義一個名爲MyTransform的Transform,以下:

public class MyTransform extends Transform {

    @Override
    public String getName() {
        //用來生成TransformTask的名稱
        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 false;
    }
  
    @Override
    public void transform(TransformInvocation transformInvocation){
        //在這裏處理class文件
    }
}
複製代碼

Transform是一個抽象類,因此它會強制要求咱們實現幾個方法,還要重寫transform方法,下面分別講解這幾個方法的含義:

一、getName方法

前面講過android gradle plugin會爲每個Transform建立一個對應的TransformTask,而建立的TransformTask的名稱通常的格式爲transformXX1WithXX2ForXX3,其中XX1是inputType類型,XX2的值就是getName方法的返回值,而XX3的值就是當前構建環境的Build Variants,例如Debug、Release等,因此若是你自定義的的Transform名爲MyTransform,Build Variants爲Debug,inputType爲Class文件,那麼該Transform對應的Task名爲transformClassesWithMyTransformForDebug。

二、getInputTypes和getScopes方法

getInputTypes方法和getScopes方法都返回一個Set集合,其中集合的元素類型分別爲ContentType接口和Scope枚舉,在Transform中,ContentType表示Transform輸入的類型Scope表示Transform輸入的做用域,Transform從ContentType和Scope這兩個維度來過濾Transform的輸入,某個輸入只有同時知足了getInputTypes方法返回的ContentType集合和getScopes方法返回的Scope集合,纔會被Transform消費。

在Transform中,主要有兩種類型的輸入,它們分別爲CLASSES和RESOURCES,以實現了ContentType接口的枚舉DefaultContentType表示,各枚舉含義以下:

DefaultContentType 含義
CLASSES 表示在jar或文件夾中的.class文件
RESOURCES 表示標準的java源文件

同理,在Transform中,輸入的做用域也以枚舉Scope表示,主要有PROJECT、SUB_PROJECTS、EXTERNAL_LIBRARIES、TESTED_CODE、PROVIDED_ONLY這五種做用域,各枚舉含義以下:

Scope 含義
PROJECT 只處理當前項目
SUB_PROJECTS 只處理當前項目的子項目
EXTERNAL_LIBRARIES 只處理當前項目的外部依賴庫
TESTED_CODE 只處理當前項目構建環境的測試代碼
PROVIDED_ONLY 只處理當前項目使用provided-only依賴的庫

ContentType和Scope均可以分別進行組合,已Set集合的形式返回,在TransformManager類中定義了一些咱們經常使用的組合,咱們能夠直接使用,如MyTransform的ContentType爲CONTENT_CLASS, Scope爲SCOPE_FULL_PROJECT,定義以下:

public class TransformManager extends FilterableStreamCollection {
  
      public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
  		
      public static final Set<ScopeType> SCOPE_FULL_PROJECT = ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);
  
  	 //...還有其餘不少組合
}
複製代碼

能夠看到CONTENT_CLASS由CLASSES組成,SCOPE_FULL_PROJECT由PROJECT、SUB_PROJECTS、EXTERNAL_LIBRARIES組成,因此MyTransform只會處理來自當前項目(包括子項目)和外部依賴庫的.class文件輸入。

三、isIncremental方法

isIncremental方法的返回值表示當前Transform是否支持增量編譯,返回true表示支持,其實在Gradle中只有Task纔有增量編譯這一說,Transform最終會被TransformTask執行,因此Transform是依賴Task來實現增量編譯的,Gradle Task經過檢測它的輸入與輸出來實現增量編譯:當檢測到輸入有文件changed時,Gradle斷定本次編譯爲增量編譯,Task內部根據changed文件作增量輸出,即只對changed文件產生輸出;當檢測到輸入與上一次輸入沒有發生任何changed時,Gradle斷定本次編譯UP-TO-DATA,能夠跳過執行;當檢測到輸出被deleted時,Gradle斷定本次編譯爲全量編譯,會觸發該Task的全量輸出,即對全部輸入文件產生輸出。

當Transform被斷定爲增量編譯後,在transform方法中就能夠根據輸入文件的Status來處理每一個輸入的文件產生增量輸出,Status也是一個枚舉,各枚舉含義以下:

Status 含義
NOTCHANGED 該文件自上次構建以來沒有發生變化
ADDED 該文件爲新增文件
CHANGED 該文件自上次構建以來發生變化(被修改)
REMOVED 該文件已被刪除

開啓增量編譯能夠大大的提升Gradle的構建速度。

注意:若是你的isIncremental方法返回true,那麼自定義的Transform的transform方法中必須提供對增量編譯的支持,即根據Status來對輸入的文件做出處理,不然增量編譯是不生效的,這在後面的插件實現中能夠看到如何提供對增量編譯的支持。

四、transform方法

transform方法就是Transform中處理輸入的地方,TransformTask執行時就是執行Transform的transform方法,transform方法的參數是TransfromInvocation,它包含的當前Transform的輸入和輸出信息,可使用TransfromInvocation的getInputs方法來獲取Transform的輸入,使用TransformInvocation的getOutputProvider方法來生成Transform的輸出,還能夠經過TransfromInvocation的isIncremental方法的返回值判斷本次transform是不是增量編譯。

TransfromInvocation的getInputs方法返回一個元素類型爲TransformInput的集合,其中TransformInput能夠獲取兩種類型的輸入,以下:

public interface TransformInput {
  	//getJarInputs方法返回JarInput集合
    Collection<JarInput> getJarInputs();
  
  	//getDirectoryInputs方法返回DirectoryInput集合
    Collection<DirectoryInput> getDirectoryInputs();
}
複製代碼

兩種類型的輸入又抽象爲JarInputDirectoryInput,JarInput表明輸入爲.Jar文件,DirectoryInput表明輸入爲文件夾類型,JarInput有一個getStatus方法來獲取該jar文件的Status,而DirectoryInputgetChangedFiles方法來獲取一個Map<File, Status>集合,因此能夠遍歷這個Map集合,而後根據File對應的Status來對File進行增量處理。

TransfromInvocation的getOutputProvider方法返回一個TransformOutputProvider,它能夠用來建立Transform的輸出位置,以下:

public interface TransformOutputProvider {
    //刪除全部輸出
    void deleteAll() throws IOException;

    //根據參數給的name、ContentType、Scope、Format來建立輸出位置
    File getContentLocation( @NonNull String name, @NonNull Set<QualifiedContent.ContentType> types, @NonNull Set<? super QualifiedContent.Scope> scopes, @NonNull Format format);
}

複製代碼

調用getContentLocation方法就能夠建立一個輸出位置並返回該位置表明的File實例,若是存在就直接返回,經過getContentLocation方法建立的輸出位置通常位於 /app/build/intermediates/transforms/build variants/transform名稱/ 目錄下,其中build variants就是當前的構建環境如debug、release等,transform名稱就是getName方法的返回值,例如在debug構建下MyTransform的輸出位置就是/app/build/intermediates/transforms/debug/MyTransform/目錄下,該目錄下都是Transform輸出的jar文件或文件夾,名稱是以0、一、二、...遞增的命名形式命名,調用deleteAll方法就能夠把getContentLocation方法建立的輸出位置下的全部文件刪除掉。

因此若是不支持增量編譯的話,transform方法裏面通常會這樣寫:

public void transform(TransformInvocation transformInvocation) throws IOException {
        //經過TransformInvocation的getInputs方法獲取全部輸入,是一個集合,TransformInput表明一個輸入
        Collection<TransformInput> transformInputs = transformInvocation.getInputs();

        //經過TransformInvocation的getOutputProvider方法獲取輸出的提供者,經過TransformOutputProvider能夠建立Transform的輸出
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        //遍歷全部的輸入,每個輸入裏面包含jar和directory兩種輸入類型的文件集合
        for(TransformInput transformInput : transformInputs){
            Collection<JarInput> jarInputs = transformInput.getJarInputs();
            //遍歷,處理jar文件
            for(JarInput jarInput : jarInputs){
                File dest = outputProvider.getContentLocation(
                        jarInput.getName(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR
                );
                //這裏只是簡單的把jar文件複製到輸出位置
                FileUtils.copyFile(jarInput.getFile(), dest);
            }

            Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
            //遍歷,處理文件夾
            for(DirectoryInput directoryInput : directoryInputs){
                File dest = outputProvider.getContentLocation(
                        directoryInput.getName(),
                        directoryInput.getContentTypes(),
                        directoryInput.getScopes(),
                        Format.DIRECTORY
                );
                //這裏只是簡單的把文件夾中的全部文件遞歸地複製到輸出位置
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            }
        }
    }
複製代碼

就是獲取到輸入,遍歷輸入中的全部JarInput和DirectoryInput,而後把相應的輸入簡單地重定向到輸出位置中,在這過程當中,咱們還能夠獲取jar文件和文件夾中的class文件,對class文件進行修改後再進行重定向到輸出,這就達到了在編譯期間修改字節碼的目的,這也是後面插件實現的核心。

每個Transform的輸出會做爲下一個Transform的輸入,這些Transform會被串行執行,以下:

{% asset_img plugin6.png plugin %}
如今對於Asm和Transform都有了一個大概的瞭解,就能夠動手實現函數耗時檢測插件。

插件實現

檢測函數耗時很簡單,只須要在每一個方法的開頭和結尾增長耗時檢測的代碼邏輯便可,例如:

protected void onCreate(Bundle savedInstanceState) {
  long startTime = System.currentTimeMillis();//start
  
  super.onCreate(savedInstanceState);
  
  long endTime = System.currentTimeMillis();//end
  long costTime = endTime - startTime;
  if(costTime > 100){
    StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[0];//得到當前方法的StackTraceElement
    Log.e("TimeCost", String.format(
      "===> %s.%s(%s:%s)方法耗時 %d ms",
      thisMethodStack.getClassName(), //類的全限定名稱
      thisMethodStack.getMethodName(),//方法名
      thisMethodStack.getFileName(),  //類文件名稱
      thisMethodStack.getLineNumber(),//行號
      costTime                        //方法耗時
    	)
    );
  }
}
複製代碼

咱們不可能手動的替應用內的每一個方法的開頭結尾加上上述代碼,應用內的方法太多了,因此咱們須要Gradle插件替咱們完成這重複的過程,在項目編譯的過程當中,經過Transform拿到項目中每一個類的字節碼,而後使用Asm對每一個類的的每一個方法的開頭結尾增長上述函數耗時檢測的字節碼,若是你不知道自定義一個Gradle插件的步驟,請移步上一篇文章,我把Gradle插件的實現代碼放在buildSrc目錄下,整個項目的目錄結構以下:

{% asset_img plugin7.png plugin %}
有關Plugin和Transform實現的代碼放在com.example.plugin下,有關Asm實現的代碼放在com.example.asm下。

一、自定義Plugin

自定義Plugin對應代碼以下:

public class TimeCostPlugin implements Plugin<Project> {

    //當函數運行時間大於threshold閥值時斷定爲耗時函數,單位ms
    public static long sThreshold = 100L;
    //當package有值時,只打印package包內的耗時函數
    public static String sPackage = "";

    @Override
    public void apply(Project project) {
        try {
            //經過project實例註冊一個名爲time的擴展
            Time time = project.getExtensions().create("time", Time.class);
            //在project構建完成後獲取time擴展中的賦值狀況
            project.afterEvaluate(project1 -> {
                if(time.getThreshold() >= 0){
                    sThreshold = time.getThreshold();
                }
                if(time.getAppPackage().length() > 0){
                    sPackage = time.getAppPackage();
                }
            });
            //經過project實例獲取android gradle plugin中的名爲android的擴展實例
            AppExtension appExtension = (AppExtension) project.getExtensions().getByName("android");
            //調用android的擴展實例即appExtension的registerTransform方法往android gradle plugin中註冊咱們自定義的Transform
            appExtension.registerTransform(new TimeCostTransform());
        }catch (UnknownDomainObjectException e){
            e.printStackTrace();
        }
    }

    /** * 擴展對應的bean類 */
    static class Time{

        private long mThreshold = -1;
        private String mPackage = "";

        public Time(){}

        public long getThreshold() {
            return mThreshold;
        }

        public void setThreshold(long threshold) {
            this.mThreshold = threshold;
        }

        public String getAppPackage() {
            return mPackage;
        }

        public void setAppPackage(String p) {
            this.mPackage = p;
        }
    }
}
複製代碼

TimeCostPlugin作了兩件事:

一、定義了一個名爲time的擴展,擴展對應的bean類爲Time類,經過這個擴展咱們能夠在build.gradle中配置咱們的插件,在這裏我定義了函數耗時閥值threshold和經過package過濾打印的函數,而後咱們在app/build.gradle中就能夠這樣使用:

apply plugin: com.example.plugin.TimeCostPlugin
//函數耗時閥值爲200ms,只對應用內的函數作插樁(排除第三方庫)
time{
    threshold = 200
    filter = 'com.example.plugindemo'
}
複製代碼

擴展屬性的賦值狀況要在project構建完畢後才能獲取,因此註冊了project的afterEvaluate回調,在裏面獲取time擴展屬性的賦值狀況。

二、把咱們自定義的Transform注入到android gradle plugin中去,android gradle plugin的名爲android的擴展對應的bean類爲AppExtension類,AppExtension中有一個元素類型爲Transform的List集合,咱們調用registerTransform方法就是把TimeCostTransform放入到這個集合中,這個Transform集合會在android gradle plugin中被使用,android gradle plugin也註冊了project的afterEvaluate回調,在回調中它會爲每一個Transform生成TransformTask.

二、自定義Transform

自定義Transform對應部分代碼以下:

public class TimeCostTransform extends Transform {

    private static final String TAG = TimeCostTransform.class.getSimpleName();//類名

    @Override
    public String getName() {
        return TAG;
    }

    @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 IOException {
        System.out.println("transform(), ---------------------------start------------------------------");

        Collection<TransformInput> transformInputs = transformInvocation.getInputs();
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
      
        //經過TransformInvocation的isIncremental方法判斷本次Transform任務是不是增量,若是Transform的isIncremental方法返回false,TransformInvocation的isIncremental方法永遠返回false
        boolean isIncremental = transformInvocation.isIncremental();

        System.out.println("transform(), isIncremental = " + isIncremental);

        //若是不是增量,就刪除以前全部產生的輸出,重頭來過
        if(!isIncremental){
            outputProvider.deleteAll();
        }

        //遍歷全部的輸入,每個輸入裏面包含jar和directory兩種輸入類型的文件集合
        for(TransformInput transformInput : transformInputs){
            Collection<JarInput> jarInputs = transformInput.getJarInputs();
            //遍歷全部的jar文件輸入
            for(JarInput jarInput : jarInputs){
                //判斷本次Transform任務是否增量
                if(isIncremental){
                    //增量處理Jar文件
                    handleJarIncremental(jarInput, outputProvider);
                }else {
                    //非增量處理Jar文件
                    handleJar(jarInput, outputProvider);
                }
            }

            Collection<DirectoryInput> directoryInputs = transformInput.getDirectoryInputs();
            //遍歷全部的directory文件輸入
            for(DirectoryInput directoryInput : directoryInputs){
                //判斷本次Transform任務是否增量
                if(isIncremental){
                    //增量處理目錄文件
                    handleDirectoryIncremental(directoryInput, outputProvider);
                }else {
                    //非增量處理目錄文件
                    handleDirectory(directoryInput, outputProvider);
                }
            }
        }

        System.out.println("transform(), ---------------------------end------------------------------");
    }

  //... 
}
複製代碼

根據前面Transform的講解,TimeCostTransform中每一個方法的含義應該是比較好理解的了,其中最重要的就是transform方法,因爲我在isIncremental方法返回了true表示TimeCostTransform支持增量編譯,因此就須要在transform方法中須要根據是不是增量編譯分別作出全量處理和增量處理,因爲jar文件的處理和directory文件的處理雷同,下面就以jar文件的處理爲例講解,對於directory文件的處理能夠查看文末源碼連接:

一、handleJar方法,全量處理jar文件輸入,產生新的輸出:

private void handleJar(JarInput jarInput, TransformOutputProvider outputProvider) throws IOException {
  //獲取輸入的jar文件
  File srcJar = jarInput.getFile();
  //使用TransformOutputProvider的getContentLocation方法根據輸入構造輸出位置
  File destJar = outputProvider.getContentLocation(
    jarInput.getName(),
    jarInput.getContentTypes(),
    jarInput.getScopes(),
    Format.JAR
  );
  //遍歷srcJar的全部內容, 在遍歷的過程當中把srcJar中的內容一條一條地複製到destJar
  //若是發現這個內容條目是class文件,就把它經過asm修改後再複製到destJar中
  foreachJarWithTransform(srcJar, destJar);
}
複製代碼

handleJar方法中肯定輸入輸出而後調用foreachJarWithTransform方法,以下:

private void foreachJarWithTransform(File srcJar, File destJar) throws IOException {
  try(
    JarFile srcJarFile = new JarFile(srcJar);
    JarOutputStream destJarFileOs = new JarOutputStream(new FileOutputStream(destJar))
  ){
    Enumeration<JarEntry> enumeration = srcJarFile.entries();
    //遍歷srcJar中的每一條條目
    while (enumeration.hasMoreElements()){
      JarEntry entry = enumeration.nextElement();
      try(
        //獲取每一條條目的輸入流
        InputStream entryIs = srcJarFile.getInputStream(entry)
      ){
        destJarFileOs.putNextEntry(new JarEntry(entry.getName()));
        if(entry.getName().endsWith(".class")){//若是是class文件
          //經過asm修改源class文件
          ClassReader classReader = new ClassReader(entryIs);
          ClassWriter classWriter = new ClassWriter(0);
          TimeCostClassVisitor timeCostClassVisitor = new TimeCostClassVisitor(classWriter);
          classReader.accept(timeCostClassVisitor, ClassReader.EXPAND_FRAMES);
          //而後把修改後的class文件複製到destJar中
          destJarFileOs.write(classWriter.toByteArray());
        }else {//若是不是class文件
          //原封不動地複製到destJar中
          destJarFileOs.write(IOUtils.toByteArray(entryIs));
        }
        destJarFileOs.closeEntry();
      }
    }
  }
}
複製代碼

因爲該輸入是jar文件,而jar文件本質是一個zip文件,因此foreachJarWithTransform中就像在解壓這個jar文件,而後遍歷解壓後的jar文件中的全部文件,經過後綴名判斷該文件是不是.class文件,若是是.class文件就經過asm處理後輸出,若是不是就是原封不動地複製到輸出中去,邏輯仍是很簡單的,關於asm的處理在後面再講。

二、handleJarIncremental方法, 增量處理jar文件輸入, 可能產生新的輸出:

private void handleJarIncremental(JarInput jarInput, TransformOutputProvider outputProvider) throws IOException {
  //獲取輸入文件的狀態
  Status status = jarInput.getStatus();
  //根據文件的Status作出不一樣的操做
  switch (status){
    case ADDED:
    case CHANGED:
      handleJar(jarInput, outputProvider);
      break;
    case REMOVED:
      //刪除全部輸出
      outputProvider.deleteAll();
      break;
    case NOTCHANGED:
      //do nothing
      break;
    default:
  }
}
複製代碼

理解了前面handleJar方法的全量處理,那麼handleJarIncremental方法中的增量處理就很好理解了,其實就是根據輸入的jar文件的Status來作出不一樣處理,對於ADDED和CHANGED都斷定爲changed文件,只對changed文件作處理,因此直接調用handleJar方法處理就行,對於REMOVED表示輸入被刪除了,那麼就刪除對應的輸出,對於NOTCHANGED表示輸入沒有變化,不作處理,跳過。

三、asm處理class文件

前面transform方法中當斷定爲某個文件爲class文件後就使用asm處理class文件,以下:

if(entry.getName().endsWith(".class")){//若是是class文件
  //經過asm修改源class文件
  ClassReader classReader = new ClassReader(entryIs);
  ClassWriter classWriter = new ClassWriter(0);
  TimeCostClassVisitor timeCostClassVisitor = new TimeCostClassVisitor(classWriter);
  classReader.accept(timeCostClassVisitor, ClassReader.EXPAND_FRAMES);
  //而後把修改後的class文件複製到destJar中
  destJarFileOs.write(classWriter.toByteArray());
}
複製代碼

根據前面asm的講解,這是使用asm轉換一個類的步驟,首先使用ClassReader讀取這個class文件,而後調用ClassReader的accept方法使用TimeCostClassVisitor開啓對class文件的訪問,最終經過ClassWriter的toByteArray方法獲取轉換後的class字節流,因此對class文件修改的邏輯都在TimeCostClassVisitor中,以下:

public class TimeCostClassVisitor extends ClassVisitor implements Opcodes {

    private String mPackage;//包名
    private String mCurClassName;//當前訪問的類的全限定名
    private boolean isExcludeOtherPackage;//是否排除不屬於package的類

    public TimeCostClassVisitor(ClassVisitor classVisitor) {
        super(ASM7, classVisitor);
        mPackage = TimeCostPlugin.sPackage;
        if(mPackage.length() > 0){
            mPackage = mPackage.replace(".", "/");
        }
        isExcludeOtherPackage = mPackage.length() > 0;
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        mCurClassName = name;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        if(isExcludeOtherPackage){
           //若是該方法對應的類在package中就處理
            if(mCurClassName.startsWith(mPackage) && !"<init>".equals(name)){
                return new TimeCostMethodVisitor(methodVisitor, access, descriptor);
            }
        }else {
            if(!"<init>".equals(name)){
                return new TimeCostMethodVisitor(methodVisitor, access, descriptor);
            }
        }
        return methodVisitor;
    }
}
複製代碼

TimeCostClassVisitor繼承自ClassVisitor,由於咱們只須要修改class文件中的方法,因此只重寫了ClassVisitor的visit方法和visitMethod方法,其中visit方法中獲取了當前訪問的類的全限定名,它在visitMethod方法中與TimeCostPlugin擴展獲取的package包名結合斷定這個類的方法是否須要被過濾掉,若是這個類不屬於package中的類,那麼就不對這個類的class文件的方法作修改,跳過,若是這個類屬於package中的類,就返回TimeCostMethodVisitor,在TimeCostMethodVisitor中修改class文件的方法,因此對於class文件中方法的修改的邏輯都在TimeCostMethodVisitor中,以下:

class TimeCostMethodVisitor extends LocalVariablesSorter implements Opcodes {

  //局部變量
  int startTime, endTime, costTime, thisMethodStack;

  public TimeCostMethodVisitor(MethodVisitor methodVisitor, int access, String desc) {
    super(ASM7, access, desc, methodVisitor);
  }

  @Override
  public void visitCode() {
    super.visitCode();
    //...方法開頭
    //long startTime = System.currentTimeMillis();
  }

  @Override
  public void visitInsn(int opcode) {
    if(opcode == RETURN){
      //...方法結尾
      //long endTime = System.currentTimeMillis();
      //long costTime = endTime - startTime;
      //if(costTime > 100){
      // StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[0];//得到當前方法的StackTraceElement
     // Log.e("TimeCost", String.format(
     // "===> %s.%s(%s:%s)方法耗時 %d ms",
     // thisMethodStack.getClassName(), //類的全限定名稱
     // thisMethodStack.getMethodName(),//方法名
     // thisMethodStack.getFileName(), //類文件名稱
     // thisMethodStack.getLineNumber(),//行號
     // costTime //方法耗時
     // )
     // );
     //}
    }
    super.visitInsn(opcode);
  }
}
複製代碼

咱們須要作的就是在方法先後插入函數耗時檢測邏輯的代碼,而visitCode方法是開始生成方法字節碼的時候調用,即方法開始時調用,而visitInsn方法在訪問RETURN指令時就是表示訪問到方法的return語句,即方法正常結束時調用,因此咱們只須要在上述地方加入函數耗時檢測邏輯的asm代碼便可,asm會自動的替咱們把asm代碼轉換爲字節碼,這樣最終生成的方法字節碼就會包含咱們的函數耗時檢測邏輯的字節碼,TimeCostMethodVisitor繼承自LocalVariablesSorter,而LocalVariablesSorter繼承自MethodVisitor,LocalVariablesSorter擴展了MethodVisitor,使得咱們很方便的在MethodVisitor的visitXX方法中經過asm代碼使用局部變量,如:startTime、 endTime、 costTime、thisMethodStack。

那麼咱們能夠經過前面介紹的ASM插件生成函數耗時檢測的asm代碼,以下:

{% asset_img plugin8.png plugin %}
因爲生成的asm代碼篇幅太長截圖不徹底,去除onCreate方法頭、結尾和super.onCreate(savedInstanceState)這句代碼的asm代碼,剩下的就屬於函數耗時檢測邏輯的asm代碼,我作了一些精簡,把一些無用的visitLabel、visitLineNumber去掉,而後把它複製到TimeCostMethodVisitor中,以下:

class TimeCostMethodVisitor extends LocalVariablesSorter implements Opcodes {

    //局部變量
    int startTime, endTime, costTime, thisMethodStack;

    public TimeCostMethodVisitor(MethodVisitor methodVisitor, int access, String desc) {
        super(ASM7, access, desc, methodVisitor);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        //long startTime = System.currentTimeMillis();
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        startTime = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE, startTime);
    }

    @Override
    public void visitInsn(int opcode) {
        if(opcode == RETURN){
            //long endTime = System.currentTimeMillis();
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            endTime = newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, endTime);

            //long costTime = endTime - startTime;
            mv.visitVarInsn(LLOAD, endTime);
            mv.visitVarInsn(LLOAD, startTime);
            mv.visitInsn(LSUB);
            costTime = newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, costTime);

            //判斷costTime是否大於sThreshold
            mv.visitVarInsn(LLOAD, costTime);
            mv.visitLdcInsn(new Long(TimeCostPlugin.sThreshold));//閥值由TimeCostPlugin的擴展屬性threshold控制
            mv.visitInsn(LCMP);

            //if costTime <= sThreshold,就跳到end標記處,不然繼續往下執行
            Label end = new Label();
            mv.visitJumpInsn(IFLE, end);

            //StackTraceElement thisMethodStack = (new Exception()).getStackTrace()[0]
            mv.visitTypeInsn(NEW, "java/lang/Exception");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Exception", "<init>", "()V", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Exception", "getStackTrace", "()[Ljava/lang/StackTraceElement;", false);
            mv.visitInsn(ICONST_0);
            mv.visitInsn(AALOAD);
            thisMethodStack = newLocal(Type.getType(StackTraceElement.class));
            mv.visitVarInsn(ASTORE, thisMethodStack);

            //Log.e("rain", String.format("===> %s.%s(%s:%s)方法耗時 %d ms", thisMethodStack.getClassName(), thisMethodStack.getMethodName(),thisMethodStack.getFileName(),thisMethodStack.getLineNumber(),costTime));
            mv.visitLdcInsn("TimeCost");
            mv.visitLdcInsn("===> %s.%s(%s:%s)\u65b9\u6cd5\u8017\u65f6 %d ms");
            mv.visitInsn(ICONST_5);
            mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
            mv.visitInsn(DUP);
            mv.visitInsn(ICONST_0);
            mv.visitVarInsn(ALOAD, thisMethodStack);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement", "getClassName", "()Ljava/lang/String;", false);
            mv.visitInsn(AASTORE);
            mv.visitInsn(DUP);
            mv.visitInsn(ICONST_1);
            mv.visitVarInsn(ALOAD, thisMethodStack);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement", "getMethodName", "()Ljava/lang/String;", false);
            mv.visitInsn(AASTORE);
            mv.visitInsn(DUP);
            mv.visitInsn(ICONST_2);
            mv.visitVarInsn(ALOAD, thisMethodStack);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement", "getFileName", "()Ljava/lang/String;", false);
            mv.visitInsn(AASTORE);
            mv.visitInsn(DUP);
            mv.visitInsn(ICONST_3);
            mv.visitVarInsn(ALOAD, thisMethodStack);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StackTraceElement", "getLineNumber", "()I", false);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
            mv.visitInsn(AASTORE);
            mv.visitInsn(DUP);
            mv.visitInsn(ICONST_4);
            mv.visitVarInsn(LLOAD, costTime);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false);
            mv.visitInsn(AASTORE);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/String", "format", "(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(POP);

            //end標記處,即方法的末尾
            mv.visitLabel(end);
        }
        super.visitInsn(opcode);
    }
}
複製代碼

上面每一句註釋都表示了註釋下面asm代碼的含義,對於局部變量使用了LocalVariablesSorter的newLocal方法生成,其實若是你仔細觀察生成的asm代碼,它們仍是頗有規律的,使用MethodVisitor的visitXX方法生成方法字節碼時它們的調用順序以下(忽略註解註釋):

[visitCode]
[visitLabel | visitLineNumber | visitFrame | visitXXInsn | visitLocalVariable | visitTryCatchBlock]
[visitMax]
visitEnd

//其中[]表示可選,|表示平級
複製代碼

與ClassVisitor相似,但以visitCode開頭,表示開始生成方法體字節碼,中間調用visitLabel、visitXXInsn等生成方法體字節碼,而後以一個visitMax結尾,最終必定要調用一個visitEnd結束,若是這個方法沒有方法體,那麼調用一個visitEnd就行。

到這裏這個函數耗時檢測插件就完成了,使用方法就和平時使用gradle插件同樣。

結語

這個gradle插件仍是很簡陋,還能夠繼續擴展它,例如耗時閥值支持ns、發現耗時函數時把函數的調用棧打印出來等,不過本文的目的仍是主要學習自定義個gradle插件的過程,還有asm和transform知識, 其實android gradle api從3.6開始不少apk打包時用到的內置transform基本都變成了直接使用Task來實現,如DesugarTransform -> DesugarTask, MergeClassesTransform -> MergeClassesTask等,多是爲了提升構建效率,這也說明了transform本質是依賴task來完成的,它並非一個新東西,它只是android gradle api提供給外部,方便外部操做字節碼的工具,同時android gradle api中也有不少apk構建時用的的插件,如AppPluginLibrayPlugin等,咱們編寫gradle插件時也能夠選擇一個做爲參考。

以上就是本文的所有內容!

本文源碼地址

參考資料:

Android Gradle Plugin打包Apk過程當中的Transform API

一塊兒玩轉Android項目中的字節碼

一文讀懂 AOP

Android Gradle Plugin 主要流程分析

相關文章
相關標籤/搜索