以前在極客時間上面學習張紹文老師的《Android開發高手課》的時候,有一章節講了android中編譯插樁的三種方法:AspectJ、ASM、Redex。以爲這個東西好厲害,就想着要弄懂它,在後面章節的Sample練習中也詳細講解了ASM與TransForm結合在android插樁中的運用。可是這個知識點仍是有點難度的,想要弄懂這個知識點仍是須要不少儲備知識的。html
要想理解android中字節碼插樁的運用,須要掌握如下幾個知識點:java
AOP(Aspect Oriented Program)是一種面向切面編程的思想。這種思想是相對於OOP(Object Oriented Programming)來講的。這裏能夠參考鄧凡平老師的深刻理解Android之AOP。Java中的面向對象編程的特色是繼承、多態和封裝。這就使功能被劃分到一個一個模塊中,模塊之間經過設計好的接口交互。OOP的精髓就使把功能或者問題模塊化。
可是在現實中,咱們會有一些這樣的需求,好比:在項目中全部模塊都添加日誌統計模塊,統計每一個方法的運行時間等。這個若是用OOP的思想來實現的話,須要在每一個模塊的每一個方法中添加須要的代碼。而經過AOP就能很好的解決這個問題,AOP能夠理解爲在代碼運行期間,動態地將代碼切入到類中的指定方法、指定位置上的編程思想。注意這是一種編程思想,它的具體實現方式有不少,好比java中的動態代理,aspectj以及咱們今天要講的經過asm來實現。android
原本想先講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:設計模式
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
咱們寫一個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是一個java字節碼操控框架。它能被用來 動態生成類或者加強既有類的功能。ASM採用的是Visitor設計模式對字節碼進行訪問和修改,核心類主要有如下幾個:
這裏主要給的是巴巴巴巴巴巴掌
的文章手摸手增長字節碼往方法體內插代碼,這個例子對於理解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子類中的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高手開發課》中的代碼。
好了,到這裏咱們終於將這三者的關係講完了,這樣你應該對字節碼插樁的實現有了清晰的認識了。後面你就能夠結合網上的一些案例來本身實現字節碼插樁了。
編譯插樁技術仍是很是重要的,咱們平時用到的不少框架包括butterknife
、Dagger
以及數據庫ORM
框架都會在編譯過程當中生成代碼。因此對於一名開發人員來講仍是要很好的掌握這門技術的。