滴滴開源的Android 字節(jié)碼編輯插件!

DroidAssist
DroidAssist?是一個(gè)輕量級的 Android 字節(jié)碼編輯插件,基于?Javassist?對字節(jié)碼操作,根據(jù) xml 配置處理 class 文件,以達(dá)到對 class 文件進(jìn)行動(dòng)態(tài)修改的效果。和其他 AOP 方案不同,DroidAssist 提供了一種更加輕量,簡單易用,無侵入,可配置化的字節(jié)碼操作方式,你不需要 Java 字節(jié)碼的相關(guān)知識,只需要在 Xml 插件配置中添加簡單的 Java 代碼即可實(shí)現(xiàn)類似 AOP 的功能,同時(shí)不需要引入其他額外的依賴。
功能
替換:把指定位置代碼替換為指定代碼
插入:在指定位置的前后插入指定代碼
環(huán)繞:在指定位置環(huán)繞插入指定代碼
增強(qiáng):
TryCatch?對指定代碼添加 try catch 代碼
Timing?對指定代碼添加耗時(shí)統(tǒng)計(jì)代碼
特點(diǎn)
靈活的配置化方式,使得一個(gè)配置就可以處理項(xiàng)目中所有的 class 文件。
豐富的字節(jié)碼處理功能,針對 Android 移動(dòng)端的特點(diǎn)提供了例如代碼替換,添加 try catch,方法耗時(shí)等功能。
簡單易用,只需要依賴一個(gè)插件,處理過程以及處理后的代碼中也不需要添加額外的依賴。
處理速度較快,只占用較少的編譯時(shí)間。
開發(fā)文檔
DroidAssist 配置文件
DroidAssist 將掃描工程中的每一個(gè)單獨(dú)的 class 以及 jar 中的 class, 并對 class 與配置文件中的規(guī)則進(jìn)行匹配,如果有規(guī)則能夠匹配到 class,則根據(jù) DroidAssist 配置對此 class 進(jìn)行字節(jié)碼修改。DroidAssist 配置是一個(gè) xml 文件,根節(jié)點(diǎn)是?DroidAssist?, 根節(jié)點(diǎn)下包含?Global?,?Insert?,?Around?,?Replace?,?Enhance?代碼操作配置,完整的 DroidAssist 配置文件格式如下:
????
????
????????...
????
????
????
????????...
????
????
????
????????...
????
????
????
????????...
????
????
????
????????...
????
為了方便編寫配置文件,在 IDE 中能自動(dòng)提示,請將根目錄下 DTD文件 拷貝到配置文件第二行。
配置分類:
Insert:代碼插入類
Replace:代碼替換類
Around:代碼環(huán)繞類
Enhance:代碼增強(qiáng)類
Source 和 Target
Insert、Replace、Around、Enhance 類型代碼操作配置中均需要包含?Source?和?Target?元素:例:
????
????????
????????????int?android.util.Log.d(java.lang.String,java.lang.String)
????????
????????
????????????$_=?com.didichuxing.tools.test.LogUtils.log($$);
????????
????
Source 的值?int android.util.Log.d(java.lang.String,java.lang.String)?表示需要匹配方法調(diào)用?android.util.Log.d( )?Target 的值?$_= com.didichuxing.tools.test.LogUtils.log($$);表示將調(diào)用?android.util.Log.d( )?方法調(diào)用的代碼替換為?com.didichuxing.tools.test.LogUtils.log( )
Source
表示需要進(jìn)行修改的代碼位置,用以精確匹配代碼位置,Source 按照代碼位置類型可以分為方法、構(gòu)造方法、字段、靜態(tài)初始化塊:
1. 方法
Source 表示方法時(shí),格式為?returnType className.methodName(argType1,argType2)?:
??int?android.util.Log.d(java.lang.String,java.lang.String)?
2. 構(gòu)造方法
Source 表示方法時(shí),格式為?new className(argType1,argType2)?或者?className.new(argType1,argType2):
??new?com.didichuxing.tools.test.ExampleSpec(int)?
或
??com.didichuxing.tools.test.ExampleSpec.new(int)?
3. 字段
Source 表示字段時(shí),格式為?fieldType className.fieldName?:
??int?com.didichuxing.tools.test.ExampleSpec.id?
4. 靜態(tài)初始化塊
Source 表示靜態(tài)初始化塊時(shí),格式為?className?:
??com.didichuxing.tools.test.ExampleSpec?
注意:
Source 的范圍為本類和在子類有效(構(gòu)造方法和靜態(tài)初始化塊除外),方法和字段如果在子類中可見,則也會(huì)被匹配,如果不需要匹配子類只匹配當(dāng)前配置類,可在
標(biāo)簽中添加?extend?屬性:extend = "false"Source 中所有的 class 均需要配置全限定名。
Source 中 class 如果是內(nèi)部類,需要使用分隔符?
$?和外部類隔開,如?com.didichuxing.tools.test.ExampleSpec$Inner。
Target
需要修改成的目標(biāo)代碼,該值接受一個(gè) Java 表達(dá)式或者以大括號{}包圍的代碼塊。如果表達(dá)式是一個(gè)單獨(dú)的表達(dá)式,需要以分號;結(jié)尾。
例:
java.lang.System.out.println("BeforeMethodCall");
或:
{System.out.println("Hello");?System.out.println("World");}
注意:
如果 Source 的表達(dá)式中?
returnType?為非?void?類型時(shí), Target 中表達(dá)式必須 要包含?$_=?以保存返回值,否則可能會(huì)出現(xiàn)錯(cuò)誤。
擴(kuò)展變量
基于 Javassist 的支持,可以在 Target 中使用語言擴(kuò)展,$?開頭的標(biāo)識符有特殊的含義:
| 符號 | 含義 | scope |
|---|---|---|
$0,$1,$2?.. | this?和方法的參數(shù) | runtime |
$args | 方法參數(shù)數(shù)組,類型為?Object[] | runtime |
$$ | 所有實(shí)參, 例如 m($$) 等價(jià)于 m($1,$2,…) | runtime |
$proceed | 表示原始的方法、構(gòu)造方法、字段調(diào)用 | runtime |
$cflow(...) | cflow 變量 | runtime |
$r | 返回結(jié)果的類型,用于強(qiáng)制類型轉(zhuǎn)換 | runtime |
$w | 包裝器類型,用于強(qiáng)制類型轉(zhuǎn)換 | runtime |
$_ | 返回值 | runtime |
$sig | 參數(shù)類型數(shù)組,類型為?java.lang.Class[] | runtime |
$type | 返回值類型,類型為?java.lang.Class | runtime |
$class | 表示當(dāng)前正在修改的類,類型為?java.lang.Class | compile |
$line | 表示當(dāng)前正在修改的行號,類型為?int | compile |
$name | 表示當(dāng)前正在方法或字段名,類型為?java.lang.String | compile |
$file | 表示當(dāng)前正在修改的文件名,類型為?java.lang.String | compile |
Target 中對于?
java.lang?包中的類可以直接使用,不用添加包名。Around 類型配置中?
Target?分解為?TargetBefore?和?TargetAfterScope 為?
compile?類型的擴(kuò)展在編譯后將直接替換成結(jié)果值,runtime?類型的擴(kuò)展只在運(yùn)行期有效。
類過濾器 Filter
默認(rèn)情況下 DroidAssist 將掃描工程中的每一個(gè) class 并進(jìn)行匹配和處理,為了加快處理速度以及排除某些不需要處理的類,可以使用類過濾器 Filter 配置將不需要處理的類排除。Filter 配置中包含:
Include:需要處理的類,支持通配符匹配和精確匹配
Exclude:不需要處理的類,支持通配符匹配和精確匹配
每個(gè) Filter 中可以包含多個(gè) Include、Exclude 配置,支持通配符匹配,class 被匹配的條件是類名可以被 Include 規(guī)則匹配但是不能被 Exclude 匹配,即?(Include & !Exclude)?。
例:
????
????????
????????????int?android.util.Log.d(java.lang.String,java.lang.String)
????????
????????
????????????com.didichuxing.tools.test.LogUtils.log($$);
????????
????
????
????????*
????????com.didichuxing.tools.test.Utils
????????android.*
????????com.android.*
????
該配置中的 Filter 中 有1個(gè) Include 配置,值?*?表示將處理所有的 class,有 3 個(gè) Exclude 配置表示將不處理com.didichuxing.tools.test.Utils?類,以及類名匹配?android.*?和?com.android.*?的類。
每一個(gè)代碼操作配置規(guī)則下都可以添加 Filter 配置(可選)
Global 配置中可以包含 Filter,當(dāng) Filter 出現(xiàn)在 Global 配置中時(shí),對所有的代碼操作配置都生效。
Global 配置
Global 配置可以包含類過濾器 Filter:
????
????????*
????????android.*
????????com.android.*
????
Replace 配置
Replace 類型代碼操作配置的作用是將指定代碼替換成目標(biāo)代碼,包含以下配置:
MethodCall?方法調(diào)用
MethodExecution?方法體執(zhí)行
ConstructorCall?構(gòu)造方法調(diào)用
ConstructorExecution?構(gòu)造方法體執(zhí)行
InitializerExecution?靜態(tài)代碼初始化塊執(zhí)行
FieldRead?字段讀取
FieldWrite?字段賦值
Call 表示方法或者構(gòu)造方法被其他代碼調(diào)用,Execution 代表方法、構(gòu)造方法或者靜態(tài)初始化代碼塊的方法體本身。
例:
????
????????
????????????int?android.util.Log.d(java.lang.String,java.lang.String)
????????
????????
????????????$_=?com.didichuxing.tools.test.LogUtils.log($$);
????????
????
????
????????new?com.didichuxing.tools.test.ExampleSpec(int)
????????{$_=?com.didichuxing.tools.test.ExampleSpec.getInstance();}
????
Insert 配置
Insert 類型代碼操作配置的作用是將指定代碼之前或之后插入目標(biāo)代碼,包含以下配置:
BeforeMethodCall?方法調(diào)用之前
AfterMethodCall?方法調(diào)用之后
BeforeMethodExecution?方法體執(zhí)行之前
AfterMethodExecution?方法體執(zhí)行之后
BeforeConstructorCall?構(gòu)造方法體調(diào)用之前
AfterConstructorCall?構(gòu)造方法體調(diào)用之后
BeforeConstructorExecution?構(gòu)造方法體執(zhí)行之前
AfterConstructorExecution?構(gòu)造方法體執(zhí)行之前
BeforeInitializerExecution?靜態(tài)代碼初始化塊執(zhí)行之前
AfterInitializerExecution?靜態(tài)代碼初始化塊執(zhí)行之前
BeforeFieldRead?字段讀取之前
AfterFieldRead?字段讀取之后
BeforeFieldWrite?字段賦值之前
AfterFieldWrite?字段賦值之后
例:
????
????????void?com.didichuxing.tools.test.ExampleSpec.run()
????????{java.lang.System.out.println("BeforeMethodCall");}
????
????
????????new?com.didichuxing.tools.test.ExampleSpec()
????????java.lang.System.out.println("AfterConstructorExecution");
????
Around 配置
Around 類型代碼操作配置的作用是將指定代碼前后環(huán)繞插入目標(biāo)代碼,包含以下配置:
MethodCall?方法調(diào)用環(huán)繞插入代碼
MethodExecution?方法體執(zhí)行環(huán)繞插入代碼
ConstructorCall?構(gòu)造方法調(diào)用環(huán)繞插入代碼
ConstructorExecution?構(gòu)造方法體執(zhí)行環(huán)繞插入代碼
InitializerExecution?靜態(tài)代碼初始化塊執(zhí)行環(huán)繞插入代碼
FieldRead?字段讀取環(huán)繞插入代碼
FieldWrite?字段賦值環(huán)繞插入代碼
在 Around 類型配置中 Target 配置分解為?TargetBefore?和?TargetAfter,分別表示 Source 代碼之前和之后插入的代碼,在?TargetBefore?中聲明的變量,在?TargetAfter?可以直接使用。
例:
????
????????
????????????void?com.didichuxing.tools.test.ExampleSpec.call()
????????
????????
????????????java.lang.System.out.println("around?before?MethodCall");
????????
????????
????????????java.lang.System.out.println("around?after?MethodCall");
????????
????
Enhance 配置
Enhance 類型代碼操作配置的作用是加入增強(qiáng)性代碼,可以對 Source 代碼添加?TryCatch?方法和?Timing?耗時(shí)統(tǒng)計(jì)方法 :
TryCatch
TryCatch 類型配置可以對 Source 代碼添加?try{...} catch(...){...}代碼,包含以下配置:
TryCatchMethodCall?方法調(diào)用添加 Try Catch 代碼
TryCatchMethodExecution?方法體執(zhí)行添加 Try Catch 代碼
TryCatchConstructorCall?構(gòu)造方法調(diào)用添加 Try Catch 代碼
TryCatchConstructorExecution?構(gòu)造方法體執(zhí)行添加 Try Catch 代碼
TryCatchInitializerExecution?靜態(tài)代碼初始化塊執(zhí)行添加 Try Catch 代碼
Exception
TryCatch 配置默認(rèn)將捕獲?java.lang.Exception?類型異常,如果需要捕獲其他異常,需要添加?Exception?配置,聲明需要捕獲的異常,在 Target 表達(dá)式中可以使用?$e?擴(kuò)展變量接收捕獲的異常對象。
例:
????
????????void?android.content.Context.startActivity(android.content.Intent)
????
????
????????android.content.ActivityNotFoundException
????
????
????????android.util.Log.d("test",?"startActivity?error",?$e);
????
Timing
Timing 類型配置可以對 Source 代碼添加耗時(shí)統(tǒng)計(jì)代碼,包含以下配置:
TimingMethodCall?方法調(diào)用添加耗時(shí)統(tǒng)計(jì)代碼
TimingMethodExecution?方法體執(zhí)行耗時(shí)統(tǒng)計(jì)代碼
TimingConstructorCall?構(gòu)造方法調(diào)用耗時(shí)統(tǒng)計(jì)代碼
TimingConstructorExecution?構(gòu)造方法體執(zhí)行耗時(shí)統(tǒng)計(jì)代碼
TimingInitializerExecution?靜態(tài)代碼初始化塊執(zhí)行耗時(shí)統(tǒng)計(jì)代碼
Timing 類型配置會(huì)自動(dòng)在 Source 代碼前后添加耗時(shí)計(jì)算代碼,并將耗時(shí)毫秒值保存到?$time?擴(kuò)展變量中,可以在 Target 配置中直接使用該擴(kuò)展變量。例:
????void?com.didichuxing.tools.test.ExampleSpec.timing()
????
????????android.util.Log.d("test",?"time?cost=?"+?$time);
????
$time?擴(kuò)展變量為?long?型,單位為毫秒,如果需要獲取耗時(shí)的微秒值,可以使用?$nanotime?擴(kuò)展變量。
Q & A
1. DroidAssist 可以實(shí)現(xiàn)什么功能?
DroidAssist 提供了一套輕量級的字節(jié)碼操作方案,可以輕易實(shí)現(xiàn)諸如代碼替換,代碼插入等功能,滴滴出行APP 目前利用DroidAssist 實(shí)現(xiàn)了日志輸出替換,系統(tǒng) SharedPreferences 替換,SharedPreferences commit 替換 apply,Dialog show 保護(hù),獲取 deviceId 接口替換,getPackageInfo 接口替換,getSystemService 接口替換,startActivity 保護(hù),匿名線程重命名,線程池創(chuàng)建監(jiān)控,主線程卡頓監(jiān)控,文件夾創(chuàng)建監(jiān)控,Activity 生命周期耗時(shí)統(tǒng)計(jì),APP啟動(dòng)耗時(shí)統(tǒng)計(jì)等功能。
2. DroidAssist 和 AspectJ 有什么區(qū)別?
DroidAssist 采用配置化方案,編寫相關(guān)配置就可以實(shí)現(xiàn) AOP 的功能,可以完全不用修改 Java 代碼,DroidAssist 使用比較簡單,不需要復(fù)雜的注解配置,DroidAssist 可以比較方便的實(shí)現(xiàn) AspectJ 不容易實(shí)現(xiàn)的代碼替換功能。
項(xiàng)目地址
github地址:https://github.com/didi/DroidAssist
? 開發(fā)者全社區(qū)?
