Android AOP 三劍客:APT AspectJ Javassist
概述
AOP三劍客各自作用的位置
APT 註解處理器(Java5 中的Annotation Processing Tool),註解現在已經比較常見,使用廣泛,可以為我們提供準確的切入點。教程參見
代表框架:DataBinding、Dagger2、EventBus3、DBFlow、AndroidAnnotation等
AspectJ主要任務是在編譯期注入程式碼
代表框架:Hugo(Jake Wharton)
拓展介紹:通過AOP程式設計來實現對原始碼無侵入埋點的工具有
工具 | 方式 | 能力 | 缺點 |
---|---|---|---|
XPosed | 執行期hook | 能hook自己應用程序的方法能hook其他應用的方法能hook系統的方法 | 手機需要root依賴三方包的支援,碎片化嚴重相容性差 |
DexPosed | 執行期hook | 能hook自己應用程序的方法 | 目前不支援4.4以上的系統依賴三方包支援,碎片化嚴重相容性差 |
AspectJ | 編譯期位元組碼注入 | 可以在編譯成位元組碼的過程中插入程式碼 | 官方有Eclipse外掛;Android Studio沒有,需要替換編譯器,環境不好部署 |
ASM | 編譯期或執行期位元組碼注入 | 可以在位元組碼中檔案或者ClassLoader載入位元組碼的時候插入程式碼 | 需要熟悉位元組碼語法 |
Javassist可以在編譯期間修改class二進位制檔案(ASM也有同樣的功能),一般利用gradle建task在打包成dex之前進行class的修改
AspectJ
環境搭建
專案的build.gradle配置
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.3'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
module下的build.gradle
apply plugin: 'com.android.application'
apply plugin: 'android-aspectjx'
AspectJ是對java的擴充套件,而且是完全相容java的。但是編譯時得用Aspect專門的編譯器,這裡的配置就是使用Aspect的編譯器
到這裡環境搭建完成
幾個基礎概念
Advice(通知): 典型的 Advice 型別有 before、after 和 around,分別表示在目標方法執行之前、執行後和完全替代目標方法執行的程式碼。
Joint point(連線點): 程式中可能作為程式碼注入目標的特定的點和入口。
Pointcut(切入點): 告訴程式碼注入工具,在何處注入一段特定程式碼的表示式。
Aspect(切面): Pointcut 和 Advice 的組合看做切面。例如,在本例中通過定義一個 pointcut 和給定恰當的advice,新增一個了記憶體快取的切面。
Weaving(織入): 注入程式碼(advices)到目標位置(joint points)的過程。
AspectJ 使用
兩種常見的使用姿勢 第一種
@Aspect
public class TestAspect {
@Before("execution(* android.view.View.OnClickListener.onClick(android.view.View))")
public void onViewClickListener(JoinPoint joinPoint) throws Throwable {
Log.d("hsb","onViewClickListener");
}
}
第二種
@Aspect
public class TestAspect {
@Pointcut("execution(* android.view.View.OnClickListener.onClick(android.view.View))")
public void point(){
}
@Before("point()")
public void before(JoinPoint joinPoint){
Log.d("hsb","OkHttpAspect before");
}
@After("point()")
public void after(){
Log.d("hsb","OkHttpAspect after");
}
}
下面一一解析下,
首先是註解 - @Aspect:宣告切面,標記類 - @Pointcut(切點表示式):定義切點,標記方法 - @Before(切點表示式):前置通知,切點之前執行 - @Around(切點表示式):環繞通知,切點前後執行 - @After(切點表示式):後置通知,切點之後執行 - @AfterReturning(切點表示式):返回通知,切點方法返回結果之後執行 - @AfterThrowing(切點表示式):異常通知,切點丟擲異常時執行
再者是切點表示式
execution(<修飾符模式>? <返回型別模式> <方法名模式>(<引數模式>) <異常模式>?)
除了返回型別模式、方法名模式、引數模式外其他均為可選
其中execution其實還有其他的方法
方法 | 描述 |
---|---|
execution(MethodPattern) | 方法執行 |
call(MethodPattern) | 方法呼叫 |
execution(ConstructorPattern) | 建構函式執行 |
call(ConstructorPattern) | 建構函式被呼叫 |
staticinitialization(TypePattern) | static 塊初始化 |
get(FieldPattern) | 屬性讀操作 |
set(FieldPattern) | 屬性寫操作 |
handler(TypePattern) | 異常處理 |
adviceexecution() | 所有 Advice 執行 |
匹配規則的型別
名稱 | 描述 |
---|---|
MethodPattern | [!] [@Annotation] [public,protected,private] [static] [final] 返回值型別 [類名.]方法名(引數型別列表) [throws 異常型別] |
ConstructorPattern | [!] [@Annotation] [public,protected,private] [final] [類名.]new(引數型別列表) [throws 異常型別] |
FieldPattern | [!] [@Annotation] [public,protected,private] [static] [final] 屬性型別 [類名.]屬性名 |
TypePattern | 其他 Pattern 涉及到的型別規則也是一樣,可以使用 ‘!’、”、’..’、’+’,’!’ 表示取反,” 匹配除 . 外的所有字串,’*’ 單獨使用事表示匹配任意型別,’..’ 匹配任意字串,’..’ 單獨使用時表示匹配任意長度任意型別,’+’ 匹配其自身及子類,還有一個 ‘…’表示不定個數 |
匹配規則
符號 | 描述 |
---|---|
* | 表示任何數量的字元,除了(.) |
.. | 表示任何數量的字元包括任何數量的(.) |
+ | 描述指定型別的任何子類或者子介面 |
! | 一 元操作符 |
|| 和 && | 二 元操作符 |
一些例子
例子 | 描述 |
---|---|
*Account | 使用Account名稱結束的型別,如SavingsAccount和CheckingAccount |
java.*.Date | 型別Date在任何直接的java子包中,如java.util.Date和java.sql.Date |
java..* | 任何在java包或者所有子包中的型別,如java.awt和java.util或者java.awt.event 和java.util.logging |
javax..*Model+ | 所有javax包或者子包中以Model結尾的型別和其所有子類,如TableModel,TreeModel。 |
!vector | 所有除了Vector的型別 |
Vector | |
java.util.RandomAccess+ | RandomAccess的所有子類 |
void Account.set*(*) | Account中以set開頭,並且只有一個引數型別,無返回值的方法 |
void Account.*() | Account中所有的沒有引數的void 方法 |
public * Account.*() | Account中所有沒有引數的public 方法 |
public * Account.*(..) | Account中所有的public 方法 |
|
Account中的所有方法,包括private方法 |
!public * Account.*(..) | 所有的非public 方法 |
* Account+.*(..) | 所有的方法,包括子類的方法 |
* java.io.Reader.read(..) | 所有的read方法 |
* java.io.Reader.read(char[],..) | 所有以read(char[])開始的方法,包括read(char[])和read(char[],int,int) |
* javax..*.add*Listener(EventListener+) | 命名以add開始,以Listener結尾的方法,引數中為EventListener或子類 |
* *.*(..) throws RemoteException | 丟擲RemoteException的所有方法 |
搭配註解使用
這方面主要利用註解來精確定位程式碼注入點,這個時候我們的AspectJ的表示式就有了輕微的變動了,可以寫成這樣 execution(<@註解型別模式>? <修飾符模式>? <返回型別模式> <方法名模式>(<引數模式>) <異常模式>?)
先寫個註解類
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnoTrace {
String value();
int type();
}
在業務中使用註解
@TestAnnoTrace(value = "lqr_test", type = 1)
public void test() {
Log.d("hsb","Hello, I am a test");
}
最後在切面中進行處理
@Before("execution(@com.android.tdw.TestAnnoTrace * *(..))")
public void pointcut(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 通過Method物件得到切點上的註解
TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class);
String value = annotation.value();
int type = annotation.type();
}