AOP的基本概念
什么是AOP
AOP(Aspect Oriented Programming)甜奄,面向切面編程捍掺,是OOP(面向?qū)ο缶幊蹋┑难永m(xù)启妹。
在OOP思想中训枢,我們會把問題劃分為各個模塊托修,如語音、表情等肮砾。在劃分這些模塊的過程中诀黍,也會出現(xiàn)一些共同特征(如埋點)。它的邏輯被分散到了各個模塊仗处,導(dǎo)致了代碼代碼復(fù)雜度提高眯勾,可復(fù)用性降低。
而AOP婆誓,就是將各個模塊中的通用邏輯抽離出來吃环。我們將這些邏輯視作Aspect(切面),然后動態(tài)地把代碼插入到類的指定方法洋幻、指定位置中郁轻。
Aspect(切面)是AOP 中的一個很重要的概念。切面一種新的模塊化機制,用來描述分散在對象好唯、類或函數(shù)中的橫切關(guān)點(crosscutting concern)竭沫。
下面兩張圖說明了使用分別OOP和AOP思想實現(xiàn)上的不同:
AOP中的幾個重要概念
Aspect 切面
一個關(guān)注點的模塊化,這個關(guān)注點實現(xiàn)可能橫切多個對象骑篙。如日志切面蜕提、權(quán)限切面等。
JPoints 執(zhí)行點
Joint Points靶端,簡稱JPoints谎势,表示的是程序運行時的一些執(zhí)行點(可切入點)。
在一個程序中杨名,一個類的構(gòu)造脏榆,一個方法的執(zhí)行,一個變量的設(shè)置台谍,一個異常的捕獲须喂,都可以看成是一個執(zhí)行點。
Pointcuts 切入點
一個程序可以有很多的Jpoints典唇,一個Jpoints還分為call(調(diào)用)和execution(執(zhí)行)镊折。但不是所有的Jpoints都需要關(guān)心。
Pointcuts定義了如何在眾多的Jpoints中選擇想要的切入點介衔。
Target Object 目標(biāo)對象
包含JPoints的對象恨胚,也被稱作被通知或被代理對象。這些對象中已經(jīng)只剩下干干凈凈的核心業(yè)務(wù)邏輯代碼了炎咖,所有的共有功能等代碼則是等待AOP容器的切入赃泡。
Advice 執(zhí)行時機
Advice簡單來說就是hook點,常見的有before乘盼、after升熊、around三種類型。
Aspect 切面
Pointcut和Advice的組合可以看做切面绸栅,它是一個關(guān)注點的模塊化级野,這個關(guān)注點可能會橫切多個對象。
Weaving 織入
把代碼織入到目標(biāo)對象的過程粹胯。
AOP的主要應(yīng)用場合
在開發(fā)中蓖柔,我們通常會把核心功能劃分為一個個模塊開發(fā),再把各個核心模塊中的通用邏輯抽離出來风纠,使用AOP的思想動態(tài)織入業(yè)務(wù)邏輯中况鸣。
所以,根據(jù)AOP的特性竹观,AOP更適合與核心業(yè)務(wù)相關(guān)的通用邏輯镐捧,如:
- 權(quán)限檢查
- 日志記錄
- 性能監(jiān)控
- 埋點操作
- 異常處理
- 參數(shù)校驗
Android下實現(xiàn)AOP的幾種工具
無論是OOP還是AOP潜索,它們都是方法論。
在Android中懂酱,AOP可以通過預(yù)編譯竹习,或者在運行期動態(tài)代理的實現(xiàn)。
動態(tài)代理
實現(xiàn)AOP最基礎(chǔ)的方案玩焰,可能就是Java語言的反射機制與動態(tài)代理機制了由驹。
業(yè)務(wù)邏輯組件在運行過程中,AOP容器會動態(tài)創(chuàng)建一個代理對象供使用者調(diào)用昔园,該代理對象已經(jīng)按程序員的意圖將切面成功切入到目標(biāo)方法的連接點上,從而使切面的功能與業(yè)務(wù)邏輯的功能都得以執(zhí)行并炮。
從原理上講默刚,調(diào)用者直接調(diào)用的其實是AOP容器動態(tài)生成的代理對象,再由代理對象調(diào)用目標(biāo)對象完成原始的業(yè)務(wù)邏輯處理逃魄,而代理對象則已經(jīng)將切面與業(yè)務(wù)邏輯方法進行了合成荤西。
動態(tài)代理又可細分為JDK動態(tài)代理和CGLib動態(tài)代理。
JDK動態(tài)代理利用接口實現(xiàn)伍俘。被代理的對象必須實現(xiàn)業(yè)務(wù)接口邪锌,代理對象必須實現(xiàn)InvocationHanlder接口。代理對象在調(diào)用具體方法前將其攔截癌瘾。
而CGLIB動態(tài)代理則利用繼承實現(xiàn)觅丰。通過ASM(一個開源的字節(jié)碼修改工具),將代理對象類的class文件加載進來妨退,通過修改其字節(jié)碼生成子類來處理妇萄。
一個簡單的JDK動態(tài)代理的代碼示例如下:
/**
* 業(yè)務(wù)接口
*/
public interface Subject {
void call();
}
/**
* 業(yè)務(wù)接口的實現(xiàn)(被代理的類)
*/
public class RealSubjcet implements Subject {
@Override
public void call() {
Log.d("denny", "RealSubject#call");
}
}
/**
* 代理類
*/
public class ProxyHandler implements InvocationHandler {
private final Object realSubject;
public ProxyHandler(Object realSubject) {
this.realSubject = realSubject;
}
@Override
public Object invoke(Object o, Method method, Object[] args) throws Throwable {
Log.d("denny", "before ProxyHandler#invoke");
Object result = method.invoke(realSubject, args);
Log.d("denny", "after ProxyHandler#invoke");
return result;
}
}
// 通過Proxy創(chuàng)建代理類對象
RealSubjcet realSubjcet = new RealSubjcet();
Subject proxySubject = (Subject) Proxy.newProxyInstance(
Subject.class.getClassLoader(),
new Class[]{Subject.class},
new ProxyHandler(realSubjcet));
proxySubject.call();
以及,最終的結(jié)果:
D/denny: before ProxyHandler#invoke
D/denny: RealSubject#call
D/denny: after ProxyHandler#invoke
APT
在編譯的時候咬荷,利用APT生成.java文件冠句。例如Dagger2、ButterKnife幸乒、EventBus3等懦底。
APT(Annotation Process Tool)是一種注解處理工具,它對源代碼文件進行檢測并找出其中的Annotation罕扎,使用Annotation進行額外的處理聚唐。
使用APT主要有以下幾個缺點:
- 相關(guān)API晦澀難懂,需要一定的編譯基礎(chǔ)壳影。
- APT無法掃描其他module拱层。
- 很難把相關(guān)代碼插入一個帶有返回值的方法之后(也就是在return之后)。
我們也可以通過繼承AbstractProcessor等方式宴咧,實現(xiàn)自己的APT根灯。
AspectJ
在.java編譯為.class(java字節(jié)碼)的時候,進行代碼注入。
AspectJ功能強大烙肺。語法較多纳猪,但是難度不大,掌握幾個常用的就能應(yīng)付大部分場合桃笙。
Javassist氏堤、ASM等字節(jié)碼操作類庫
這兩個工具比較相似,都是對已經(jīng)編譯好的class文件進行操作搏明。相比于AspectJ鼠锈,這兩個工具顯得更加強大靈活,可以直接對Java的字節(jié)碼進行修改星著,但是難度也較高购笆。
Java的二進制被存儲在嚴(yán)格格式定義的.class文件中,這些字節(jié)碼文件擁有足夠的元數(shù)據(jù)信息來表示類中的所有元素虚循,包括類名稱同欠、方法、屬性以及Java字節(jié)碼指令横缔。
ASM可以動態(tài)生成類或者增強既有類的功能铺遂。使用ASM,可以直接生成二進制.class文件茎刚,也可以在類被加載入Java虛擬機前動態(tài)改變既有類的行為襟锐。
下面這張圖說明了各個工具的作用時機。
AspectJ
AspectJ簡介
AspectJ是目前應(yīng)用最為廣泛的AOP實現(xiàn)方案斗蒋。
使用AspectJ開發(fā)有兩種打開方式:
- 使用AspectJ的語言編寫.aj文件進行開發(fā)捌斧。這種語言與Java十分相似,只是多了幾個關(guān)鍵字而已
- 使用AspectJ提供的注解泉沾,直接在Java語言上進行開發(fā)
但在Android開發(fā)中捞蚂,由于Android Studio并不認(rèn)識.aj文件,因此建議使用注解進行開發(fā)跷究。后續(xù)的例子也都使用注解的方式姓迅。一般情況下,使用提供的注解和一些簡單的語法就可以實現(xiàn)絕大部分功能上的需求了俊马。
Android中集成AspectJ
AspectJ的引入很簡單丁存。
首先,我們在項目根目錄的gradle中引入hujiang的gradle插件柴我。
當(dāng)然解寝,如果你愿意,也可以自己寫個gradle插件來實現(xiàn)艘儒。
Hujiang的插件是一個基于AspectJ并在此基礎(chǔ)上擴展出來可應(yīng)用于Android開發(fā)平臺的AOP框架聋伦,可作用于java源碼夫偶,class文件及jar包,同時支持kotlin的應(yīng)用觉增。感興趣的小伙伴可以看看他們的github
classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4"
之后兵拢,在app的gradle下添加AspectJ的依賴
apply plugin: 'android-aspectjx'
dependencies {
compile 'org.aspectj:aspectjrt:1.8.10'
}
先舉個栗子
讓我們先看一個簡單的例子。
@After("execution(protected void com.example.myapplication.MainActivity.onCreate(android.os.Bundle))")
public void logCreate(JoinPoint joinPoint) {
Log.d("denny", "MainActivity#onCreate");
}
在這個例子中逾礁,
- @After定義了Advice说铃,After的含義是在切入點之后執(zhí)行。
- execution(XXX)定義了JPoints的類型嘹履,execution的含義是截獲方法的執(zhí)行腻扇,并在執(zhí)行前/后插入切點。
- protected void com.example.myapplication.MainActivity.onCreate(android.os.Bundle))定義了過濾條件植捎。在這里衙解,我們選擇的切入點是call,因此它的過濾條件一定是某個(或某些)函數(shù)焰枢。
- protect過濾了Jpoint的訪問權(quán)限
- void表示Jpoint無返回值
- com.example.myapplication.MainActivity是函數(shù)的包名,緊跟其后的onCreate是函數(shù)名
- android.os.Bundle同理舌剂,指明了函數(shù)只有一個參數(shù)济锄,參數(shù)的類型是位于android.os包下的Bundle
以上這個注解連起來的意思就是,選擇一個截獲了onCreate方法調(diào)用的切入點霍转,在該切入點后執(zhí)行l(wèi)ogCreate方法(打印一條log)荐绝。
其中,onCreate方法的過濾條件為:訪問類型為protect的方法避消,該方法位于com.example.myapplication.MainActivity中低滩,方法名為onCreate,參數(shù)為android.os.Bundle
AspectJ語法解析
JPoints 執(zhí)行點
Jpoint是指程序運行中可切入的點岩喷。
在Aspect中支持的Jpoints如下:
Jpoints | 說明 |
---|---|
Method call | 方法被調(diào)用 |
Method execution | 方法執(zhí)行 |
Constructor call | 構(gòu)造函數(shù)被調(diào)用 |
Constructor execution | 構(gòu)造函數(shù)執(zhí)行 |
Field get | 讀取屬性 |
Field set | 寫入屬性 |
Pre-initialization | 與構(gòu)造函數(shù)有關(guān)恕沫,很少用到 |
Initialization | 與構(gòu)造函數(shù)有關(guān),很少用到 |
Static initialization | static 塊初始化 |
Handler | 異常處理 |
Advice execution | 所有 Advice 執(zhí)行 |
execution和call的含義是不同的纱意。
execution截獲的是方法真正執(zhí)行的代碼區(qū)婶溯,使用@Around可以控制原方法執(zhí)行與否,可以選擇執(zhí)行或者替換偷霉;
而call截獲的是方法的調(diào)用區(qū)迄委,它無法控制原來方法的執(zhí)行與否,只是在方法調(diào)用前后插入切點类少,因此比較適合做一些輕量的監(jiān)控(方法調(diào)用耗時等)
Pointcuts 切入點
概覽
Pointcuts定義了如何在眾多的Jpoints中選擇想要的切入點叙身。
AspectJ中,對于Pointcuts有一套標(biāo)準(zhǔn)的語法硫狞。使用這套語法信轿,可以實現(xiàn)許多強大的功能晃痴。在實際使用中,我們只需要掌握一些簡單的應(yīng)用就可以了虏两。至于那些高級玩法愧旦,等到項目需要的時候再去查詢文檔也不遲。
PointCuts中最常選擇的點與JPoint密切相關(guān)定罢,下面的表格給出了二者之間的關(guān)系:
Join Point | 說明 | Pointcuts語法 |
---|---|---|
Method call | 方法被調(diào)用 | call(MethodPattern) |
Method execution | 方法執(zhí)行 | execution(MethodPattern) |
Constructor call | 構(gòu)造函數(shù)被調(diào)用 | call(ConstructorPattern) |
Constructor execution | 構(gòu)造函數(shù)執(zhí)行 | execution(ConstructorPattern) |
Field get | 讀取屬性 | get(FieldPattern) |
Field set | 寫入屬性 | set(FieldPattern) |
Pre-initialization | 與構(gòu)造函數(shù)有關(guān)笤虫,很少用到 | preinitialization(ConstructorPattern) |
Initialization | 與構(gòu)造函數(shù)有關(guān),很少用到 | initialization(ConstructorPattern) |
Static initialization | static 塊初始化 | staticinitialization(TypePattern) |
Handler | 異常處理 | handler(TypePattern) |
Advice execution | 所有 Advice 執(zhí)行 | adviceexcution() |
除了上表中的Jpoint祖凫,AspectJ還提供其他一些選擇方法琼蚯,下表列出了一些常用的選擇非Jpoint的方法:
Pointcuts synatx | 說明 |
---|---|
within(TypePattern) | 符合 TypePattern 的代碼中的 Join Point |
withincode(MethodPattern) | 在某些方法中的 Join Point |
withincode(ConstructorPattern) | 在某些構(gòu)造函數(shù)中的 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 操作符應(yīng)用的對象)是否 instanceOf Type 或者 Id 的類型 |
args(Type or Id, ...) | 方法或構(gòu)造函數(shù)參數(shù)的類型 |
if(BooleanExpression) | 滿足表達式的 Join Point遭庶,表達式只能使用靜態(tài)屬性、Pointcuts 或 Advice 暴露的參數(shù)稠屠、thisJoinPoint 對象 |
上面Pointcuts的語法中涉及到一些Pattern峦睡,這些Pattern的具體規(guī)則如下表所示,[]里的內(nèi)容是可選的:
Pattern | 規(guī)則 |
---|---|
MethodPattern | [!] [@Annotation] [public,protected,private] [static] [final] 返回值類型 [類名.]方法名(參數(shù)類型列表) [throws 異常類型] |
ConstructorPattern | [!] [@Annotation] [public,protected,private] [final] [類名.]new(參數(shù)類型列表) [throws 異常類型] |
FieldPattern | [!] [@Annotation] [public,protected,private] [static] [final] 屬性類型 [類名.]屬性名 |
TypePattern | 其他 Pattern 涉及到的類型規(guī)則也是一樣权埠,可以使用 '!'榨了、''、'..'攘蔽、'+'龙屉,'!' 表示取反,'*' 匹配除 . 外的所有字符串满俗,'*' 單獨使用事表示匹配任意類型转捕,'..' 匹配任意字符串,'..' 單獨使用時表示匹配任意長度任意類型唆垃,'+' 匹配其自身及子類五芝,還有一個 '...'表示不定個數(shù) |
Pointcut和Pattern都可以使用!降盹、&&与柑、|| 來組合選取,其中的含義和Java一樣蓄坏,這里就不再贅述了价捧。
舉例
以MethodPattern為例:
一個MethodPattern的完整表達式為:@注解 訪問權(quán)限 返回值的類型 包名.函數(shù)名(參數(shù))
@注解和訪問權(quán)限(public/private/protect,以及static/final)屬于可選項涡戳。如果不設(shè)置它們结蟋,則默認(rèn)都會選擇。以訪問權(quán)限為例渔彰,如果沒有設(shè)置訪問權(quán)限作為條件嵌屎,那么public推正,private,protect及static宝惰、final的函數(shù)都會進行搜索植榕。
返回值類型就是普通的函數(shù)的返回值類型。如果不限定類型的話尼夺,就用*通配符表示
-
包名.函數(shù)名用于查找匹配的函數(shù)尊残。可以使用通配符淤堵,包括*和..以及+號寝衫。其中*號用于匹配除.號之外的任意字符,而..則表示任意子package拐邪,+號表示子類慰毅。比如:
- java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
- Test*:可以表示TestBase扎阶,也可以表示TestDervied
- java..*:表示任意以java開頭的包名汹胃,如java.a.Test
- *..Date: 表示任意以Date結(jié)尾的包名,如a.b.Date
- java..Date: 表示任意以java開頭东臀,以Date結(jié)尾的包名
- java..*Model+:表示Java任意package中统台,名字以Model結(jié)尾的子類,比如TabelModel啡邑,TreeModel 等
-
最后來看函數(shù)的參數(shù)。參數(shù)匹配比較簡單井赌,主要是參數(shù)類型谤逼,比如:
- (int, char):表示參數(shù)只有兩個,并且第一個參數(shù)類型是int仇穗,第二個參數(shù)類型是char
- (String, ..):表示至少有一個參數(shù)流部。并且第一個參數(shù)類型是String,后面參數(shù)類型不限纹坐。在參數(shù)匹配中枝冀,..代表任意參數(shù)個數(shù)和類型
- (Object ...):表示不定個數(shù)的參數(shù),且類型都是Object耘子,這里的...不是通配符果漾,而是Java中代表不定參數(shù)的意思
更多
更詳細的Pointcuts定義,可以查看官方Pointcus說明谷誓。
Advice 執(zhí)行時機
Advice簡單來說就是hook點绒障,常見的有before、after捍歪、around三種類型户辱。
Advice | 說明 |
---|---|
@Before | 在執(zhí)行Join Point之前 |
@After | 在執(zhí)行Join Point之后鸵钝,包括正常的return和throw異常 |
@AfterReturning | Join Point為方法調(diào)用且正常return時 |
@AfterThrowing | Join Point為方法調(diào)用且拋出異常時 |
@Around | 替代 Join Point 的代碼,如果要執(zhí)行原來代碼的話庐镐,要使用 ProceedingJoinPoint.proceed() |
@After默認(rèn)包含了@AfterReturning和@AfterThrowing兩種類型恩商。
理論上說,@Before和@After能夠?qū)崿F(xiàn)的必逆,@Around也完全能夠?qū)崿F(xiàn)怠堪。@Around的目標(biāo)是替換原Jpoint。
對于同一個但是在對一個但是在對一個Pointcut聲明末患,@Before和@After可以同時使用研叫,但是在聲明@Around后再聲明@Before或者@After,則會報錯璧针。
它們的區(qū)別如下:
- @Before和@After沒有返回值嚷炉,而@Around的返回值與原Jpoint匹配。
- @Around可以決定目標(biāo)方法是否執(zhí)行探橱,甚至可以替換目標(biāo)為另一方法申屹。
AspectJ 切面
使用注解的方式開發(fā)AspectJ,需要在類的頭部聲明@AspectJ隧膏,如:
@Aspect
public class ActivityLifeCycle {
// your methods
}
這個類就相當(dāng)于一個關(guān)注面哗讥。我們可以再定義一個PermisssionCheckAspect進行權(quán)限檢查,定義一個PerformancMonitorAspect進行性能檢測……所有的關(guān)注點的相關(guān)代碼都挪到一個類型進行控制胞枕。
@Pointcut聲明
@Pointcuts由org.aspectj.lang.annotation.Pointcut注解修飾的方法聲明杆煞,方法返回值只能是void。@Pointcutz修飾的方法只能由空的方法實現(xiàn)而且不能有throws語句腐泻,方法的參數(shù)和pointcut中的參數(shù)相對應(yīng)决乎。
比如,下面這種方式
@Before("execution(* *..MainActivity.onCreate(..))")
public void logCreate(JoinPoint joinPoint) {
Log.d("denny", "MainActivity#onCreate");
}
也可以用@Pointcut寫成:
@Pointcut("execution(* *..MainActivity.onCreate(..))")
public void onCreatePointcut() {
}
@Before("onCreatePointcut()")
public void logCreate(JoinPoint joinPoint) {
Log.d("denny", "MainActivity#onCreate");
}
call與execution
舉個例子派桩,進一步說明call和execution的區(qū)別构诚。
首先,定義Animal接口:
public interface Animal {
void execute();
}
接著铆惑,定義Rabbit與Tiger實現(xiàn)Animal接口:
public class Rabbit implements Animal {
@Override
public void execute() {
Log.d("denny", "Rabbit is running");
}
}
public class Tiger implements Animal {
@Override
public void execute() {
Log.d("denny", "Tiger is running");
}
}
然后范嘱,在MainActivity的onCreate中:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
execute();
}
private void execute() {
new Rabbit().execute();
new Tiger().execute();
}
}
最后,我們來看使用call和execute的區(qū)別:
@Before("call(* *.execute(..))")
public void logExecute(JoinPoint joinPoint) {
Log.d("denny", "aspectj: " + joinPoint.getSourceLocation());
}
D/denny: aspectj: MainActivity.java:15
D/denny: aspectj: MainActivity.java:19
D/denny: Rabbit is running
D/denny: aspectj: MainActivity.java:20
D/denny: Tiger is running
@Before("execution(* *.execute(..))")
public void logExecute(JoinPoint joinPoint) {
Log.d("denny", "aspectj: " + joinPoint.getSourceLocation());
}
打印的結(jié)果為:
D/denny: aspectj: MainActivity.java:19
D/denny: aspectj: Rabbit.java:8
D/denny: Rabbit is running
D/denny: aspectj: Tiger.java:8
D/denny: Tiger is running
結(jié)論:call攔截的是方法的調(diào)用员魏,所有execute的調(diào)用都在MainActivity中丑蛤。而execution攔截的是方法的執(zhí)行,分別在MainActivity逆趋、Rabbit和Tiger中盏阶。
within
within用來選取符合條件的Pointcut。還是使用上面的例子來說明:
@Before("call(* *.execute(..)) && within(*..MainActivity)")
public void executeAOP(JoinPoint joinPoint) {
Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation());
}
D/denny: aspectj before: MainActivity.java:15
D/denny: aspectj before: MainActivity.java:19
D/denny: Rabbit is running
D/denny: aspectj before: MainActivity.java:20
D/denny: Tiger is running
攔截的是MainActivity中的execute方法的調(diào)用闻书。
@Before("execution(* *.execute(..)) && within(*..MainActivity)")
public void executeAOP(JoinPoint joinPoint) {
Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation());
}
D/denny: aspectj before: MainActivity.java:19
2019-09-04 20:39:03.373 17272-D/denny: Rabbit is running
2019-09-04 20:39:03.373 17272-D/denny: Tiger is running
攔截的是MainActivity中的execute方法的執(zhí)行名斟,MainActivity中只有一個execute方法脑慧,因此只攔截到了一處。
target與this
前面說過砰盐,AspetcJ是屬于靜態(tài)織入的闷袒,但其實AspectJ也有動態(tài)織入的部分,而target()與this()就是屬于它動態(tài)織入的方式岩梳。所以target()與this()需要在在運行時才能確定那些被攔截囊骤。
先給結(jié)論:
- target是指:我們pointcut所選取的Join point的所有者,直白點說就是: 指明攔截的方法屬于那個類冀值。
- this是指: 我們pointcut所選取的Join point的調(diào)用/執(zhí)行的所有者也物,就是說:方法是在那個類中被調(diào)用/執(zhí)行的。
下面的例子可以幫助大家理解兩者的微妙區(qū)別:
@Before("call(* *.execute(..))")
public void executeAOP(JoinPoint joinPoint) {
Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
+ " this: " + joinPoint.getThis().getClass().getSimpleName()
+ " target: " + joinPoint.getTarget().getClass().getSimpleName());
}
打印的結(jié)果為:
D/denny: aspectj before: MainActivity.java:15 this: MainActivity target: MainActivity
D/denny: aspectj before: MainActivity.java:19 this: MainActivity target: Rabbit
D/denny: Rabbit is running
D/denny: aspectj before: MainActivity.java:20 this: MainActivity target: Tiger
D/denny: Tiger is running
可以看到列疗,log中所有的this都是MainActivity滑蚯,事實上,我們所選擇的切入點(execute方法)確實都是在MainActivity中被調(diào)用的抵栈。而target是指所攔截的方法屬于哪個類告材,因此打印出來的分別是MainActivity、Rabbit和Tiger古劲。
@Before("execution(* *.execute(..))")
public void executeAOP(JoinPoint joinPoint) {
Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
+ " this: " + joinPoint.getThis().getClass().getSimpleName()
+ " target: " + joinPoint.getTarget().getClass().getSimpleName());
}
打印的結(jié)果為:
D/denny: aspectj before: MainActivity.java:19 this: MainActivity target: MainActivity
D/denny: aspectj before: Rabbit.java:8 this: Rabbit target: Rabbit
D/denny: Rabbit is running
D/denny: aspectj before: Tiger.java:8 this: Tiger target: Tiger
D/denny: Tiger is running
和上一個例子相比斥赋,這里使用execution攔截方法的執(zhí)行。target沒有發(fā)生變化产艾,但是this發(fā)生了變化疤剑。對于Rabbit和Tiger來說,它們的execute()方法是在MainActivity中被調(diào)用闷堡,但是執(zhí)行當(dāng)然是在自己的類中啦骚露。
target()和this()還存在繼承關(guān)系作用,也就是說:如果你的signature是一個基類缚窿,那么這個pointcut同時也會對他的子類也起作用。這里就不舉例了焰扳,有興趣的小伙伴可以自行實驗一下倦零。
另外,target和this可以獲取他們對應(yīng)的實例吨悍,但within不行扫茅。看下面這個例子:
@Before("call(* *.execute(..)) && this(mainActivity) && target(tiger)")
public void executeAOP(JoinPoint joinPoint, MainActivity mainActivity, Tiger tiger) {
Log.d("denny", "aspectj before: " + joinPoint.getSourceLocation()
+ " this: " + mainActivity.getClass().getSimpleName()
+ " target: " + tiger.getClass().getSimpleName());
}
if表達式
在基于AspectJ注解的開發(fā)方式中育瓜,if(...)表達式的用法與其他的選擇操作符不同葫隙,在@Pointcut的語句中if表達式只能是if()、if(true)或if(false)躏仇,而且@Pointcut方法必須為public static boolean恋脚,方法體內(nèi)就是if表達式的內(nèi)容腺办,可以使用暴露的參數(shù)、靜態(tài)屬性糟描、JoinPoint怀喉、JoinPoint.StaticPart、JoinPoint.EnclosingStaticPart。
JoinPoint.StaticParts僅包含join point的靜態(tài)信息。
下面這個例子中鱼的,“ImeService#onStartInputView_before”只會在打印五次闪盔。
private static int COUNT = 0;
@Pointcut("execution(* *..ImeService.onStartInputViewInternal(..)) && if()")
public static boolean onStartInputView() {
return COUNT++ < 5;
}
@Before("onStartInputView()")
public void onBeforeStartInputView(JoinPoint.StaticPart joinPoint) {
Log.d("denny", "ImeService#onStartInputView_before");
}
代碼混淆
通過上述方式切入的代碼是可以混淆的。
代碼是在編譯階段織入卷胯,所以混淆是不會有影響,只有在運行時你需要通過類名,方法名去做一些事情的時候才不能混淆菱蔬,比如用到了反射技術(shù)等。任何在編譯階段植入代碼的AOP方案混淆都不會受影響荒辕,和混淆無關(guān)汗销。而在運行時的AOP方案就會受混淆影響。
反編譯查看實現(xiàn)
我們可以通過反編譯抵窒,看看AspectJ是如何在不修改原有代碼的情況下弛针,實現(xiàn)無縫插入的。
源碼:
public class MainActivity extends AppCompatActivity {
private void testA() {
Log.d("denny", "initial method");
}
}
切面代碼:
@Aspect
public class AspectJ {
@Around("execution(* *..testA(..))")
public void log(ProceedingJoinPoint point) throws Throwable {
point.proceed();
Log.d("denny", "aspectj test");
Log.d("denny", Log.getStackTraceString(new Throwable()));
}
}
反編譯后李皇,MainActivity的代碼如下:
public class MainActivity extends AppCompatActivity {
private static final /* synthetic */ StaticPart ajc$tjp_0 = null;
static {
ajc$preClinit();
}
private static /* synthetic */ void ajc$preClinit() {
Factory factory = new Factory("MainActivity.java", MainActivity.class);
ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, (Signature) factory.makeMethodSig("2", "testA", "com.example.dany.aspectjapplication.MainActivity", "", "", "", "void"), 61);
}
private void testA() {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, this, this);
testA_aroundBody1$advice(this, makeJP, AspectJ.aspectOf(), (ProceedingJoinPoint) makeJP);
}
private static final /* synthetic */ void testA_aroundBody1$advice(MainActivity ajc$this, JoinPoint thisJoinPoint, AspectJ ajc$aspectInstance, ProceedingJoinPoint point) {
Log.d("denny", "initial method");
Log.d("denny", "aspectj test");
Log.d("denny", Log.getStackTraceString(new Throwable()));
}
}
我們主要關(guān)心testA方法即可削茁。不難發(fā)現(xiàn),我們的代碼被AspectJ重構(gòu)了掉房。在不修改源代的前提下茧跋,對java字節(jié)碼進行操作,實現(xiàn)無縫插入卓囚。
Best Practice
幫助定位問題
如果我們需要定位一個問題瘾杭,但是又不便于調(diào)試,那么AspectJ或許可以幫助我們哪亿。
舉一個在實際項目中遇到的例子粥烁。
用戶反饋了一個問題,通過查看代碼推測蝇棉,可能是網(wǎng)絡(luò)狀態(tài)的判斷出現(xiàn)了異常讨阻。但是,在調(diào)試的狀態(tài)下(更準(zhǔn)確地說篡殷,是在充電的情況下)钝吮,網(wǎng)絡(luò)狀態(tài)的判斷是正常的。
因此,接下來的思路就是奇瘦,通過log的形式記錄當(dāng)前的網(wǎng)絡(luò)狀態(tài)棘催,寫入文件幫助確定問題。
對網(wǎng)絡(luò)狀態(tài)的判斷的相關(guān)代碼在另一個組件中链患,無法直接在其中添加代碼巧鸭。此時,我想到了AspectJ:
@Aspect
public class AspectJTest {
@After("execution(* *..A.B(..))")
public void logXg(ProceedingJoinPoint point) throws Throwable {
Log.d("denny", "current state: " + state);
}
}
類似的例子還有很多麻捻,對于一些無法或者不方便修改源碼的第三方庫纲仍,我們都可以借助AspectJ切入。例如贸毕,在Bitmap的createBitmap方法之后記錄相關(guān)信息郑叠、在OnClickListener#onClick中記錄View的狀態(tài)等等。
利用AspectJ進行數(shù)據(jù)統(tǒng)計/埋點
AOP最直觀的一個實踐明棍,就是在數(shù)據(jù)統(tǒng)計中了乡革。
下面給出一個統(tǒng)計方法執(zhí)行時間的例子。
首先摊腋,我們定義注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerformanceTrace {
}
然后沸版,定義AspectJ:
@Aspect
public class AspectJTest {
@Around("execution(@*..PerformanceTrace * *(..))")
public Object logPerformance(ProceedingJoinPoint point) throws Throwable {
final long start = System.currentTimeMillis();
final Object result = point.proceed();
final long end = System.currentTimeMillis();
Log.d("denny", "time cost: " + (end - start) + "ms"
+ " | method signature: " + point.getSignature());
return result;
}
}
最后,只需要在需要進行性能測試的方法前兴蒸,加上@PerformanceTrace
注解就可以了视粮。
jake Wharton大神已經(jīng)為我們提供了開源工具實現(xiàn)類似的功能:Hugo。其內(nèi)部實現(xiàn)橙凳,正是借用了AspectJ蕾殴。
檢查權(quán)限
與上面的例子相似,我們首先定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionAnnotation {
String[] declaredPermission() default "";
}
之后岛啸,定義切點:
@Aspect
public class AspectJTest {
@Around("execution(@*..PermissionAnnotation * *(..)) && @annotation(annotation)")
public void permissionCheck(ProceedingJoinPoint point, PermissionAnnotation annotation) throws Throwable {
final String[] declaredPermission = annotation.declaredPermission();
final Context context = (Context) point.getThis(); // could lead to a null pointer
AndPermission.with(context)
.permission(declaredPermission)
.onGranted(action -> {
try {
point.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
})
.onDenied(action -> Toast.makeText(context, "Permission Deny", Toast.LENGTH_SHORT).show())
.start();
}
}
最后钓觉,在需要進行權(quán)限檢查的地方加上注解即可:
@PermissionAnnotation(declaredPermission = {Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS})
更多例子
更多的例子,可以參考Hujiang的這個Demo
AspectJ存在的問題
無法織入的問題
舉個例子坚踩,如果我們想要統(tǒng)計所有的Activity的onPause方法的耗時荡灾,我們可以這樣定義PointCut:
@Around("execution(* android.app.Activity+.onPause(..))")
public void logPerformance(ProceedingJoinPoint point) throws Throwable {
// do something you want
}
但是,這樣做會導(dǎo)致兩個問題:
- 如果我們的Activity沒有復(fù)寫onPause方法瞬铸,那么將不會織入卧晓。這是因為android.app.Activity位于android設(shè)備內(nèi),不參與打包的過程赴捞。如果是support包中的Activity或者Fragment,就不會受到影響郁稍。
- 如果我們的Activity繼承了BaseActivity赦政,BaseActivity又繼承了android.app.Activity,那么這兩個Activity都會被織入,這就造成了重復(fù)統(tǒng)計的問題恢着。
出現(xiàn)問題較難排查
AspectJ是通過修改字節(jié)碼的方式實現(xiàn)AOP桐愉,對上層無感知,因此出現(xiàn)問題的時候較難定位問題原因掰派。在項目中如果使用了AOP从诲,應(yīng)當(dāng)盡可能留下文檔。
編譯時間變長
在Gradle的Transform過程靡羡,會遍歷所有class文件系洛,查找符合需求的切入點,然后插入字節(jié)碼略步。如果項目較大且織入代碼較多描扯,會增加編譯時間。
解決辦法包括:
使用exclude過濾掉不需要執(zhí)行織入的包名趟薄。
如果織入代碼在debug環(huán)境不需要織入绽诚,比如埋點,可以主動關(guān)閉AspectJ功能杭煎。