Android 字節碼插樁

1、爲何要插樁

        咱們都知道JAVA是面向對象(繼承、封裝、多態),而插樁的意義在於面向切面(AOP),可想而知單方面的面向對象開發有許多的侷限性,而結合面向切面編程能夠說補足了咱們的這種侷限性。舉個例子:在onClick中通常都要作防抖動操做,這樣是爲了不屢次打開頁面的問題。通常實現的話是在每一個onClick實現第二次點擊的時候加個時間判斷。而插樁的話業務端能夠不寫任何代碼經過插樁的方法把這個時間判斷插入的字節碼裏面。java

從標題名字看android

  • Java字節碼:是Java虛擬機執行的一種虛擬指令格式。經過JVM轉換生成機器指令
  • 插樁:是在保證被測程序原有邏輯完整性的基礎上在程序中插入一些探針(又稱爲「探測儀」。

2、插樁能帶來什麼


3、AOP思想

            

       銀行系統會有一個取款流程,咱們能夠把方框裏的流程合爲一個,另外系統還會有一個查詢餘額流程,咱們先把這兩個流程放到一塊兒,有沒有發現,這個二者有一個相同的驗證流程,咱們先把它們圈起來再說下一步,有沒有想過能夠把這個驗證用戶的代碼是提取出來,不放到主流程裏去呢,這就是AOP的做用了,有了AOP,你寫代碼時不要把這個驗證用戶步驟寫進去,即徹底不考慮驗證用戶。編程

  • 什麼是AOP:把這些橫跨並嵌入衆多模塊裏的功能(如監控每一個方法的性能) 集中起來,放到一個統一的地方來控制和管理
  • 能給我帶來什麼:不修改源代碼的狀況下給程序動態統一添加功能的一種技術,把散落在程序中的公共部分提取出來,作成切面類,這樣的好處在於,代碼的可重用,一旦涉及到該功能的需求發生變化,只要修改該代碼就行,不然,你要處處修改,若是隻要修改一、2處那還能夠接受,萬一有1000處呢。

4、Android打包流程插樁入口

                     

這是app打包流程的整個過程而我把這個打包流程主要分爲一下步驟:api

  • aapt來打包資源文件,生成R.java文件
  • 處理AIDL,生成對應的.java接口文件
  • 編譯Java文件,生成對應的.class文件
  • 把.class文件轉化成Davik VM支持的.dex文件
  • 打包生成未簽名的.apk文件。

字節碼插樁入口:咱們知道Android程序從Java源代碼到可執行的Apk包主要分析兩個環節:數組

  • javac:將源文件編譯成class格式的文件
  • dex:將class格式的文件彙總到dex格式的文件中

咱們要想對字節碼進行修改,只須要在javac以後,dex以前對class文件進行字節碼掃描,並按照必定規則進行過濾及修改就能夠了,這樣修改事後的字節碼就會在後續的dex打包環節被打到apk中,這就是咱們的插樁入口。
bash

插樁方式1、:transform api

每一個Transform其實都是一個gradle task,Android編譯器中的TaskManager將每一個Transform串連起來,第一個Transform接收來自javac編譯的結果,以及已經拉取到在本地的第三方依賴(jar. aar),還有resource資源,注意,這裏的resource並不是android項目中的res資源,而是asset目錄下的資源。這些編譯的中間產物,在Transform組成的鏈條上流動,每一個Transform節點能夠對class進行處理再傳遞給下一個Transform。咱們常見的混淆,Desugar等邏輯,它們的實現現在都是封裝在一個個Transform中,而咱們自定義的Transform,會插入到這個Transform鏈條的最前面。
app

對於Android Gradle Plugin 版本在1.5.0及以上的狀況,Google官方提供了transformapi用做字節碼插樁的入口。框架

implementation 'com.android.tools.build:gradle:1.5.0'複製代碼

通常使用方法爲:extends Transform重寫transform()maven

插樁方式二:hook dx.jar

須要引入Instrumentation性能


經過Java Instrumentation機制,爲得到插樁入口,對於apk build過程進行了兩處插樁(即hook),圖中標紅部分:

Instrumentation:指的是能夠用獨立於應用程序以外的代理(agent)程序來監測和協助運行在JVM上的應用程序。這種監測和協助包括但不限於獲取JVM運行時狀態,替換和修改類定義等。

  • 在build進程,對ProcessBuilder.start()方法進行插樁
    ProcessBuilder類是J2SE 1.5在java.lang中新添加的一個新類,此類用於建立操做系統進程,它提供一種啓動和管理進程的方法,start方法就是開始建立一個進程,對它進行插樁,使得經過下面方式啓動dx.jar進程執行dex任務時:

    java  dex.jar  com.android.dx.command.Main  --dex …........複製代碼

    增長參數-javaagent agent.jar,使得dex進程也可使用Java Instrumentation機制進行字節碼插樁

  • 在dex進程
    對咱們的目標方法com.android.dx.command.Main.processClasses進行字節碼插入,從而實現打入apk的每個項目中的類都按照咱們制定的規則進行過濾及字節碼修改。

build進程使用Instrumentation的方式時以前敘述過的VirtualMachine.loadAgent方式(方式二),dex進程中的方式則是-javaagent agent.jar方式(方式一)。

  由此,咱們得到了進行字節碼插樁的入口,下面咱們就使用ASM庫的API,對項目中的每個類進行掃描,過濾,及字節碼修改。



5、自定義Gradle插件


一、建立一個Android library Module工程

创建module

二、build.gradle改爲groovy方式

apply plugin: 'groovy'

    dependencies {
        compile gradleApi()
        compile localGroovy()
    }複製代碼

三、新建.groovy類繼承 Plugin並實現apply方法,注意:類的後綴再也不是.java而是.groovy


四、在main下建立resources目錄


五、增長對應的maven deployer發佈到本地或遠程倉庫


六、使用已發佈的倉庫


6、ASM


ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者加強既有類的功能。ASM 能夠直接產生二進制 class 文件,也能夠在類被加載入 Java 虛擬機以前動態改變類行爲。Java class 被存儲在嚴格格式定義的 .class 文件裏,這些類文件擁有足夠的元數據來解析類中的全部元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息後,可以改變類行爲,分析類信息,甚至可以根據用戶要求生成新類。


爲何選擇ASM來進行字節碼編織?

Framework First time Later times
Javassist 257 5.2
BCEL 473 5.5
ASM 62.4 1.1

可使用一個插件[ASM Bytecode Outline]更有效的用ASM編寫字節碼

ASM(core api) 按照visitor模式按照class文件結構依次訪問class文件的每一部分,有以下幾個重要的visitor。

操做流程

  1. 須要建立一個 ClassReader 對象,將 .class 文件的內容讀入到一個字節數組中
  2. 而後須要一個 ClassWriter 的對象將操做以後的字節碼的字節數組回寫
  3. 須要事件過濾器 ClassVisitor。在調用 ClassVisitor 的某些方法時會產生一個新的 XXXVisitor 對象,當咱們須要修改對應的內容時只要實現本身的 XXXVisitor 並返回就能夠了

 ClassReader 類

這個類會將 .class 文件讀入到 ClassReader 中的字節數組中,它的 accept 方法接受一個 ClassVisitor 實現類,並按照順序調用 ClassVisitor 中的方法

 ClassWriter 類

ClassWriter 是一個 ClassVisitor 的子類,是和 ClassReader 對應的類,ClassReader 是將 .class 文件讀入到一個字節數組中,ClassWriter 是將修改後的類的字節碼內容以字節數組的形式輸出。

ClassVisitor 抽象類

  • void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
    該方法是當掃描類時第一個調用的方法,主要用於類聲明使用。下面是對方法中各個參數的示意:visit( 類版本 , 修飾符 , 類名 , 泛型信息 , 繼承的父類 , 實現的接口)
  • AnnotationVisitor visitAnnotation(String desc, boolean visible)
    該方法是當掃描器掃描到類註解聲明時進行調用。下面是對方法中各個參數的示意:visitAnnotation(註解類型 , 註解是否能夠在 JVM 中可見)。
  • FieldVisitor visitField(int access, String name, String desc, String signature, Object value)
    該方法是當掃描器掃描到類中字段時進行調用。下面是對方法中各個參數的示意:visitField(修飾符 , 字段名 , 字段類型 , 泛型描述 , 默認值)
  • MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
    該方法是當掃描器掃描到類的方法時進行調用。下面是對方法中各個參數的示意:visitMethod(修飾符 , 方法名 , 方法簽名 , 泛型信息 , 拋出的異常)
  • void visitEnd()
    該方法是當掃描器完成類掃描時纔會調用,若是想在類中追加某些方法

  • MethodVisitor & AdviceAdapter

    MethodVisitor 是一個抽象類,當 ASM 的 ClassReader 讀取到 Method 時就轉入 MethodVisitor 接口處理。
    AdviceAdapter 是 MethodVisitor 的子類,使用 AdviceAdapter 能夠更方便的修改方法的字節碼。

    AdviceAdapter

    其中比較重要的幾個方法以下:

    1. void visitCode():表示 ASM 開始掃描這個方法
    2. void onMethodEnter():進入這個方法
    3. void onMethodExit():即將從這個方法出去
    4. void onVisitEnd():表示方法掃碼完畢



    字節碼基礎

    • 全限定名即爲全類名中的「.」,換爲「/」,舉例:
      類android.widget.AdapterView.OnItemClickListener的全限定名爲:
      android/widget/AdapterView$OnItemClickListener複製代碼
    • 描述符(descriptors):
      1.類型描述符,以下圖所示:


    在class文件中類型 boolean用「Z」描述,數組用「[」描述(多維數組可疊加),那麼咱們最多見的自定義引用類型呢?「L全限定名;」.例如:
    Android中的android.view.View類,描述符爲「Landroid/view/View;」

    2.方法描述符的組織結構爲:

    (參數類型描述符)返回值描述符複製代碼複製代碼

    其中無返回值void用「V」代替,舉例:

    方法boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id)  的描述符以下:
    (Landroid/widget/ExpandableListView;Landroid/view/View;IJ)Z複製代碼




    對上圖中三個步驟的詳細說明:

    步驟一:

    ASM的ClassVisitor對全部類的class文件進行掃描,在visitMethod()方法中判斷是否是BaseActivity,若是是進行步驟二,不然終止掃描;

    步驟二:

    ClassVisitor每掃描到一個方法時,在visitMethod中進行以下斷定:

    1. 是否是要過濾的<init>方法

    若是斷定經過,則證實本次掃描到的方法是須要注入字節碼的方法,而後將
    將掃描邏輯交給MethodVisitor,進行字節碼的修改(步驟三)。

    步驟三:修改掃碼到的方法字節碼

    假設待修改的方法以下:

    public int test() {
      try {  
          Thread.sleep(1000);
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
    }複製代碼

    修改以後須要變成:

    public int test() {
       long startTime = System.currentTimeMillis();
       try {  
           Thread.sleep(1000);}
       catch (InterruptedException e){ 
           e.printStackTrace();  
       }
       long timing = System.currentTimeMillis() - startTime;
       BlockManager.timingPage(getLocalClassName(), timing);
    }複製代碼
    相關文章
    相關標籤/搜索