Android AspectJ详解
- @Aspect 用它聲明一個類,表示一個需要執行的切面。
- @Pointcut 聲明一個切點。
- @Before/@After/@Around/…(統稱為Advice類型) 聲明在切點前、后、中執行切面代碼。
這么說你可能有點蒙,我們換個角度解釋。
假設你是一個AOP框架的設計者,最先需要理清的其基本組成要素。既然需要做代碼織入那是不是一定得配置代碼的織入點呢?這個織入點就是Pointcut,有了織入點我們還需要指定具體織入的代碼,這個代碼寫在哪里呢?就是寫在以@Before/@After/@Around注解的方法體內。有了織入點和織入代碼,還需要告訴框架自己是一個面向切面的配置文件,這就需要使用@Aspect聲明在類上。
我們舉個簡單的栗子,全部示例參考github [sample_aspectj](()。
@Aspect //①
public class MethodAspect {
@Pointcut(“call(* com.wandering.sample.aspectj.Animal.fly(…))”)//②
public void callMethod() {
}
@Before(“callMethod()”)//③
public void beforeMethodCall(JoinPoint joinPoint) {
Log.e(TAG, “before->” + joinPoint.getTarget().toString()); //④
}
}
我們事先準備好的Animal類中有一個fly方法。
public class Animal {
public void fly() {
Log.e(TAG, “animal fly method:” + this.toString() + “#fly”);
}
}
①處聲明了本類是一個AspectJ配置文件。
②處指定了一個代碼織入點,注解內的call(* com.wandering.sample.aspectj.Animal.fly(…)) 是一個切點表達式,第一個*號表示返回值可為任意類型,后跟包名+類名+方法名,括號內表示參數列表, … 表示匹配任意個參數,參數類型為任何類型,這個表達式指定了一個時機:在Animal類的fly方法被調用時。
③處聲明Advice類型為Before并指定切點為上面callMethod方法所表示的那個切點。
④處為實際織入的代碼。
翻譯成白話就是說在Animal類的fly方法被調用前插入④處的代碼。
編寫測試代碼并調用fly方法,運行觀察日志輸出你會發現before->的日志先于animal fly日志被打印,具體可查看sample工程MethodAspect示例。
我們再將APK反編譯看一下織入結果。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qju0X2dp-1651545443167)(https://user-gold-cdn.xitu.io/2019/12/27/16f45564fa1108a4?imageView2/0/w/1280/h/960/ignore-error/1)]
紅色框選部分就是AspectJ為我們織入的代碼。
通過上面的例子我們了解了AspectJ的基本用法,但實際上AspectJ的語法可以十分復雜,下面我們來看看具體的語法。
Join Point
上面的例子中少講了一個連接點的概念,連接點表示可織入代碼的點,它屬于Pointcut的一部分。由于語法內容較多,實際使用過程中我們可以參考[語法手冊]((),我們列出其中一部分Join Point:
| Method call | 方法被調用 |
| Method execution | 方法執行 |
| Constructor call | 構造函數被調用 |
| Constructor execution | 構造函數執行 |
| Static initialization | static 塊初始化 |
| Field get | 讀取屬性 |
| Field set | 寫入屬性 |
| Handler | 異常處理 |
Method call 和 Method execution的區別常拿來比較,其實就是調用與執行的區別,就拿上面Animal的fly方法舉例。demo代碼如下:
Animal a = Animal();
a.fly();
如果我們聲明的織入點為call,再假設Advice類型是before,則織入后代碼結構是這樣的。
Animal a = new Animal();
//…我是織入代碼
a.fly();
如果我們聲明的織入點為execution,則織入后代碼結構就成這樣了。
public class Animal {
public void fly() {
//…我是織入代碼
Log.e(TAG, “animal fly method:” + this.toString() + “#fly”);
}
}
本質上的區別就是織入對象不同,call被織入在指定方法被調用的位置上,而execution被織入到指定的方法內部。
Pointcut
Pointcuts是具體的切入點,基本上Pointcuts 是和 Join Point 相對應的。
| Method call | call(MethodPattern) |
| Method execution | execution(MethodPattern) |
| Constructor call | call(ConstructorPattern) |
| Constructor execution | execution(ConstructorPattern) |
| Static initialization | staticinitialization(TypePattern) |
| Field get | get(FieldPattern) |
| Field set | set(FieldPattern) |
| Handler | handler(TypePattern) |
除了上面與 Join Point 對應的選擇外,Pointcuts 還有其他選擇方法。
| within(TypePattern) | 符合 TypePattern 的代碼中的 Join Point |
| withincode(MethodPattern) | 在某些方法中的 Join Point |
| withincode(ConstructorPattern) | 在某些構造函數中的 Join Point |
| cflow(Pointcut) | Pointcut 選擇出的切入點 P 的控制流中的所有 Join Point,包括 P 本身 |
| cflowbelow(Pointcut) | Pointcut 選擇出的切入點 P 的控制流中的所有 Join Point,不包括 P 本身 |
| this(Type or Id) | Join Point 所屬的 this 對象是否 instanceOf Type 或者 Id 的類型 |
| target(Type or Id) | Join Point 所在的對象(例如 call 或 execution 操作符應用的對象)是否 instanceOf Type 或者 Id 的類型 |
| args(Type or Id, …) | 方法或構造函數參數的類型 |
| if(BooleanExpression) | 滿足表達式的 Join Point,表達式只能使用靜態屬性、Pointcuts 或 Advice 暴露的參數、thisJoinPoint 對象 |
this vs. target
this和target是一個容易混淆的點。
MethodAspect.java
public class MethodAspect {
@Pointcut(“call(* com.wandering.sample.aspectj.Animal.fly(…))”)
public void callMethod() {
Log.e(TAG, “callMethod->”);
}
@Before(“callMethod()”)
public void beforeMethodCall(JoinPoint joinPoint) {
Log.e(TAG, “getTarget->” + joinPoint.getTarget());
Log.e(TAG, “getThis->” + joinPoint.getThis());
}
}
fly調用方:
MainActivity.java
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Animal animal = new Animal();
animal.fly();
}
運行結果如下:
getTarget->com.wandering.sample.aspectj.Animal@509ddfd
getThis->com.wandering.sample.aspectj.MainActivity@98c38bf
也就是說target指代的是切入點方法的所有者,而this指代的是被織入代碼所屬類的實例對象。
我們稍加改動,將切點的call改為execution。
運行結果就成這個樣子了:
getTarget->com.wandering.sample.aspectj.Animal@509ddfd
getThis->com.wandering.sample.aspectj.Animal@509ddfd
按照上面的分析,與這個結果也是吻合的。
條件運算
Pointcut表達式中還可以使用一些條件判斷符,比如 !、&&、||。
以Hugo為例:
Hugo.java
@Pointcut(“within(@hugo.weaving.DebugLog *)”)
public void withinAnnotatedClass() {}
@Pointcut(“execution(!synthetic * *(…)) && withinAnnotatedClass()”)
public void methodInsideAnnotatedType() {}
第一個切點指定范圍為包含DebugLog注解的任意類和方法,第二個切點為在第一個切點范圍內,且執行非內部類的任意方法。結合起來表述就是任意聲明了DebugLog注解的方法。
其中@hugo.weaving.DebugLog *和!synthetic * *(..)分別對應上面表格中提到的TypePattern和MethodPattern。
接下來需要了解這些pattern具體的語法,通過語法我們可以寫出符合自身需求的表達式。
| 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 涉及到的類型規則也是一樣,可以使用 ‘!’、‘’、‘…’、‘+’,‘!’ 表示取反,‘’ 匹配除 . 外的所有字符串,‘*’ 單獨使用事表示匹配任意類型,‘…’ 匹配任意字符串,‘…’ 單獨使用時表示匹配任意長度任意類型,‘+’ 匹配其自身及子類,還有一個 '…'表示不定個數 |
更多語法參見官網[Pointcuts]((),非常有用。
再看幾個例子:
execution(void setUserVisibleHint(…)) && target(android.support.v4.app.Fragment) && args(boolean) — 執行 Fragment 及其子類的 setUserVisibleHint(boolean) 方法時。
execution(void Foo.foo(…)) && cflowbelow(execution(void Foo.foo(…))) — 執行 Foo.foo() 方法中再遞歸執行 Foo.foo() 時。
if條件
通常情況下,Pointcuts注解的方法參數列表為空,返回值為void,方法體也為空。但是如果表達式中聲明了:
- args、target、this等類型參數,則可額外聲明參數列表。
- if條件,則方法必須public static boolean。
來看sample示例MethodAspect8:
@Aspect
public class MethodAspect8 {
@Pointcut(“call(boolean .(int)) && args(i) && if()”)
public static boolean someCallWithIfTest(int i, JoinPoint jp) {
// any legal Java expression…
return i > 0 && jp.getSignature().getName().startsWith(“setAge”);
}
@Before(“someCallWithIfTest(i, jp)”)
public void aroundMethodCall(int i, JoinPoint jp) {
Log.e(TAG, "before if ");
}
}
切點方法 《Android學習筆記總結+最新移動架構視頻+大廠安卓面試真題+項目實戰源碼講義》無償開源 徽信搜索公眾號【編程進階路】 someCallWithIfTest聲明的注解表示任意方法,此方法返回值為boolean,參數簽名為僅一個int類型的參數,后面跟上if條件,表示此int參數值大于0,且方法簽名以setAge開頭。
如此一來切面代碼的執行就具備了動態性,但不是說不滿足if條件的切點就不會織入代碼。依然會織入,只是在調用織入代碼前會執行someCallWithIfTest方法,當返回值為true時才會執行織入代碼,下圖是反編譯class的結果。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-esTc1Np7-1651545443168)(https://user-gold-cdn.xitu.io/2019/12/27/16f455650c61926f?imageView2/0/w/1280/h/960/ignore-error/1)]
了解了原理后,實際上if邏輯也完全可以放到織入點代碼中,理解起來會更容易一些。
Advice
直譯過來是通知,實際上表示一類代碼織入位置,在AspectJ中有五種類型的注解:Before、After、AfterReturning、AfterThrowing、Around,我們將它們統稱為Advice注解。
| @Before | 切入點前織入 |
| @After | 切入點后織入,無論連接點執行如何,包括正常的 return 和 throw 異常 |
| @AfterReturning | 只有在切入點正常返回之后才會執行,不指定返回類型時匹配所有類型 |
| @AfterThrowing | 只有在切入點拋出異常后才執行,不指定異常類型時匹配所有類型 |
| @Around | 替代原有切點,如果要執行原來代碼的話,調用 ProceedingJoinPoint.proceed() |
Advice注解修飾的方法有一些約束:
JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart又是什么呢?
在執行切面代碼時,AspectJ會將連接點處的上下文信息封裝成JoinPoint供我們使用。這些信息中有些是在編譯階段就可以確定的,比如方法簽名 joinPoint.getSignature(),JoinPoint類型 joinPoint.getKind(),切點代碼位置類名+行數joinPoint.getSourceLocation() 等等,我們將他們統稱為JoinPointStaticPart。
而還有一些是在運行時才能確定的,比如前文提到的this、target、實參等等。
- JoinPoint 包含連接點處的靜態信息+動態信息。
- JoinPointStaticPart 連接點處的靜態信息。
- EnclosingStaticPart 包含了連接點的靜態信息,也就是連接點的上下文。
如果不需要動態信息,建議使用靜態類型的參數,以提高性能。
講了這么多理論,看起來比較復雜,實際上我們日常開發中的場景要相對簡單一些。
常用示例
- 等等,我們將他們統稱為JoinPointStaticPart。
而還有一些是在運行時才能確定的,比如前文提到的this、target、實參等等。
- JoinPoint 包含連接點處的靜態信息+動態信息。
- JoinPointStaticPart 連接點處的靜態信息。
- EnclosingStaticPart 包含了連接點的靜態信息,也就是連接點的上下文。
如果不需要動態信息,建議使用靜態類型的參數,以提高性能。
講了這么多理論,看起來比較復雜,實際上我們日常開發中的場景要相對簡單一些。
常用示例
總結
以上是生活随笔為你收集整理的Android AspectJ详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .net 简单的后台合成图片
- 下一篇: 基于android地图国内外研究,开题报