android字節碼插樁研究

背景

以前在極客時間上面學習張紹文老師的《Android開發高手課》的時候,有一章節講了android中編譯插樁的三種方法:AspectJ、ASM、Redex。以爲這個東西好厲害,就想着要弄懂它,在後面章節的Sample練習中也詳細講解了ASM與TransForm結合在android插樁中的運用。可是這個知識點仍是有點難度的,想要弄懂這個知識點仍是須要不少儲備知識的。html

知識點

要想理解android中字節碼插樁的運用,須要掌握如下幾個知識點:java

  • 面向切面編程思想(AOP)
  • 自定義gradle插件
  • Transform相關知識
  • ASM相關知識
  • 將plugin、Transform、ASM結合起來使用

AOP簡介

AOP(Aspect Oriented Program)是一種面向切面編程的思想。這種思想是相對於OOP(Object Oriented Programming)來講的。這裏能夠參考鄧凡平老師的深刻理解Android之AOP。Java中的面向對象編程的特色是繼承、多態和封裝。這就使功能被劃分到一個一個模塊中,模塊之間經過設計好的接口交互。OOP的精髓就使把功能或者問題模塊化。
可是在現實中,咱們會有一些這樣的需求,好比:在項目中全部模塊都添加日誌統計模塊,統計每一個方法的運行時間等。這個若是用OOP的思想來實現的話,須要在每一個模塊的每一個方法中添加須要的代碼。而經過AOP就能很好的解決這個問題,AOP能夠理解爲在代碼運行期間,動態地將代碼切入到類中的指定方法、指定位置上的編程思想。注意這是一種編程思想,它的具體實現方式有不少,好比java中的動態代理,aspectj以及咱們今天要講的經過asm來實現。android

自定義Gradle插件

原本想先講Transform相關的知識的,可是,Transform通常在自定義插件中使用,因此若是不先介紹自定義插件的話,可能看不懂要講的Transform,這裏就簡單介紹一下自定義插件。
這裏推薦在AndroidStudio中自定義Gradle插件,這篇文章詳細講解了如何在android studio中建立Gradle插件,這裏就再也不細述。建立好了以後咱們會在groovy文件夾下面建立一個繼承Plugin類的子類,以下:數據庫

package com.soulmate.plugin.lifecycle

import org.gradle.api.Plugin
import org.gradle.api.Project

class CustomPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
       project.task("testTask"){
            doLast{
                println "hello from the CustomPlugin"
            }
        }
    }
}
複製代碼

當咱們在Terminal中輸入gradle testTask的時候,會看到輸出「hello from the CustomPlugin」,後面經過Transform來處理時,也是在apply方法中進行處理的。編程

Transform簡介

在官方文檔中是這麼形容Transform:設計模式

Starting with 1.5.0-beta1,the Gradle Plugin includes a Transform API allowing 
3rd party plugins to manipulate compiled class files before they are converted to dex files     

The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks,
and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex) 
have all moved to this new mechanism already in 1.5.0-beta1
複製代碼

簡單翻譯一下就是Gradle工具從1.5.0版本開始提供Transform API,在編譯後的class文件轉換成dex文件以前,經過Transform API來處理編譯後的class文件。api

Transform API的目標是不須要經過處理任務來簡化注入自定義類的操做,在處理上面提供了更大的靈活性。包括(proguard、multi-dex等)都在1.5.0中遷移到這個新機制中。數組

簡單總結就是Transform API是操做編譯後的.class文件,而咱們知道.class文件中是java編譯後的字節碼,因此Transform至關於提供了一個操做字節碼的入口。(具體java中的字節碼相關知識能夠網上搜索,這裏我強烈推薦一下《深刻理解Java虛擬機》這本書,這本書上面對字節碼有很詳細的講解)。而因爲字節碼的操做比較複雜,咱們通常須要藉助工具來處理java字節碼,ASM工具就是一個很是好的字節碼處理工具,後面咱們會介紹ASM在處理字節碼方面的運用。bash

Transform的代碼結構

咱們寫一個TestTransform繼承Transform而後看一些重寫的方法。數據結構

public class TestTransform extends Transform {
    private static Project project

    TestTransform(Project project) {
        this.project = project
    }
    
    @Override
    public String getName() {
        return 「TestTransform」;
    }

    /**
     * 須要處理的數據類型,有兩種枚舉類型
     * CLASSES 表明處理的編譯後的class文件,RESOURCES 表明要處理的java資源
     */
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * 值Transform 的做用範圍,有一下7種類型:
     * 1.EXTERNAL_LIBRARIES        只有外部庫
     * 2.PROJECT                   只有項目內容
     * 3.PROJECT_LOCAL_DEPS        只有項目的本地依賴(本地jar)
     * 4.PROVIDED_ONLY             只提供本地或遠程依賴項
     * 5.SUB_PROJECTS              只有子項目
     * 6.SUB_PROJECTS_LOCAL_DEPS   只有子項目的本地依賴項(本地jar)
     * 7.TESTED_CODE               由當前變量(包括依賴項)測試的代碼
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    //是否支持增量編譯
    @Override
    public boolean isIncremental() {
        return false;
    }
    
    //這個方法用來進行具體的輸入輸出處理,這裏能夠獲取輸入的目錄文件以及jar包文件
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
    }
}
複製代碼

這裏要補充一下,如今自定義的Transform有了,自定義的plugin也有了,如何將二者關聯起來了。這時咱們須要用到一個類AppExtension,這個類繼承自BaseExtension。咱們在TestPlugin類中改寫apply方法:

package com.soulmate.plugin.lifecycle

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class CustomPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        AppExtension appExtension = project.extensions.findByType(AppExtension.class)
        appExtension.registerTransform(new TestTransform(project))
    }
}
複製代碼

這樣咱們就將自定義的插件和Transform關聯起來了。接下來咱們介紹一下ASM相關的知識,而後最後在講解在transform()方法中使用ASM來處理相應的需求

ASM簡介

ASM是一個java字節碼操控框架。它能被用來 動態生成類或者加強既有類的功能。ASM採用的是Visitor設計模式對字節碼進行訪問和修改,核心類主要有如下幾個:

  • ClassReader: 它將字節數組或者class文件讀入內存當中,並以樹的數據結構表示,樹中的一個節點表明class文件中的某個區域。能夠將ClassReader看作是接受訪問者(accept)的實現類,其每個元素均可以被Visitor訪問
  • ClassVisitor(抽象類):ClassReader對象建立以後,調用ClassReader#accept()方法,傳入一個ClassVisitor對象。在ClassReader中遍歷樹結構的不一樣節點時會調用ClassVisitor對象不一樣的visit()方法,從而實現對字節碼的修改。在ClassVisitor中的一些訪問會產生子過程,好比visitMethod會產生MethodVisitor,visitField會產生FieldVisitor。咱們也能夠對這些Visitor進行實現,從而達到對這些子節點的字節碼的訪問和修改。如自帶的AdviceAdapter就是繼承自MethodVisitor.
  • ClassWriter:繼承自ClassVisitor,它是生成字節碼的工具類,它通常是責任鏈的最後一個節點,其以前的每個ClassVisitor都是致力於對原始字節碼作修改,而ClassWriter的操做則是把每一個節點修改後的字節碼輸出爲字節數組。

ASM工做流程

  1. ClassReader讀取字節碼到內存中,生成用於表示該字節碼的內部表示的樹,ClassReader對應於訪問者模式被訪問的元素
  2. 組裝ClassVisitor責任鏈,這一系列ClassVisitor完成對字節碼一系列不一樣的字節碼修改工做
  3. 而後調用ClassReader#accept()方法,傳入ClassVisitor對象,此ClassVisitor是責任鏈的頭結點,通過責任鏈中每個ClassVisitor對已加載進內存的字節碼的樹結構上的每一個節點的訪問和修改
  4. 最後,在責任鏈的末端,調用ClassWriter中的visitor進行修改後的字節碼的輸出工做。

ASM使用demo

這裏主要給的是巴巴巴巴巴巴掌的文章手摸手增長字節碼往方法體內插代碼,這個例子對於理解asm中具體的插入代碼方式有很是直觀的理解。這裏我就不貼出具體代碼了,我只是將main()方法中的

FileOutputStream fos = new FileOutputStream("out/Bazhang223.class");
fos.write(code);
fos.close();
複製代碼

替換成了

FileOutputStream fos = new FileOutputStream("Bazhang223.class");
fos.write(code);
fos.close();
複製代碼

運行main方法後,會在as的根目錄下面生成Bazhang223.class文件。打開這個class文件,你會發現你想要添加的兩個輸出已經添加成功了。

Plugin、Transform和ASM的結合使用

前面咱們已經對每個都進行了介紹,如今咱們對這三者的概念應該有了清晰的認識,接下來就要看看如何將三者結合起來使用了。
自定義plugin這個不用說,確定是首先須要作的事。

而後咱們須要作的是重寫自定義自定義的Transform子類中的transform()方法,這個方法很是重要,這個方法是全部業務邏輯的入口,在這個方法裏面你能夠遍歷全部目錄和jar包,獲取全部的class文件,而後作須要的處理。具體遍歷的代碼以下。

//Transform 的 inputs 有兩種類型,一種是目錄,一種是 jar 包,要分開遍歷
inputs.each { TransformInput input ->
    //遍歷directoryInputs
    input.directoryInputs.each { DirectoryInput directoryInput ->
        //do Something
    }

    //遍歷jarInputs
    input.jarInputs.each { JarInput jarInput ->
        //do Something
    }
}
複製代碼

既然咱們能夠獲取全部的class文件了,那麼如今咱們就能夠對每一個class文件進行修改了,修改class文件就用到了ASM。這裏就以《android高手開發課》上面的例子講一下,將每一個class文件轉換成字節數組,而後傳給下面的方法:

public static run(InputStream is) throws IOException {
    ClassReader classReader = new ClassReader(is);
    //COMPUTE_MAXS 說明使用 ASM 自動計算本地變量表最大值和操做數棧的最大值
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
    //EXPAND_FRAMES 說明在讀取 class 的時候同時展開棧映射幀 (StackMap Frame),在使用 AdviceAdapter 裏這項是必須打開的
    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
}
複製代碼

具體在ASM中如何修改這裏就不詳細說了,能夠參考《android高手開發課》中的代碼。

好了,到這裏咱們終於將這三者的關係講完了,這樣你應該對字節碼插樁的實現有了清晰的認識了。後面你就能夠結合網上的一些案例來本身實現字節碼插樁了。

總結

編譯插樁技術仍是很是重要的,咱們平時用到的不少框架包括butterknifeDagger以及數據庫ORM框架都會在編譯過程當中生成代碼。因此對於一名開發人員來講仍是要很好的掌握這門技術的。

參考文獻

相關文章
相關標籤/搜索