本文是對(duì)網(wǎng)絡(luò)中相關(guān)文章的總結(jié)林说,再加上自己的相關(guān)見解得出的,若有侵權(quán)請(qǐng)及時(shí)聯(lián)系
參考的大佬博客:
白話 Android AOP ---> 這個(gè)說的是 ASM + Transform 的方式實(shí)現(xiàn)AOP(難度比較大,維護(hù)起來(lái)比較難)
深入理解Android之AOP ---> 這個(gè)說的是 AspectJ 實(shí)現(xiàn) AOP(相當(dāng)于別人的三方庫(kù)枚钓,使用起來(lái)比較簡(jiǎn)單,容易上手)
Android AOP方案(一)——AspectJ ---> 基本語(yǔ)法加實(shí)現(xiàn)
Android AspectJ詳解 ---> 更為詳細(xì)的基本語(yǔ)法加實(shí)現(xiàn) 主要可以看這篇博客
談?wù)凙ndroid AOP技術(shù)方案
AOP簡(jiǎn)介
AOP(Aspect Oriented Programming 的縮寫),意為:面向切面編程筐带,和OOP(Object Oriented Programming,面向?qū)ο缶幊?以對(duì)象為核心不同缤灵,AOP 則是針對(duì)業(yè)務(wù)處理過程中的相同或者相似的代碼邏輯(切面)進(jìn)行提取伦籍,然后統(tǒng)一處理,它所面對(duì)的是處理過程中的某個(gè)步驟或階段腮出。這兩種設(shè)計(jì)思想在目標(biāo)上有著本質(zhì)的差異帖鸦,但是 AOP 和 OOP 并不是對(duì)立的,相反胚嘲,巧妙的結(jié)合這兩種思想來(lái)指導(dǎo)代碼編寫作儿,會(huì)讓我們的代碼保持可重用性的同時(shí),顯著降低各個(gè)部分之間的耦合度馋劈。
OOP 和 AOP 都是方法論攻锰,是我個(gè)人認(rèn)為對(duì)這兩種思想最準(zhǔn)確的描述和總結(jié)。
自我總結(jié):AOP是一種思想妓雾,其目的是為了完成解耦娶吞,將同一功能從代碼中抽離,然后使用輕量級(jí)的方式注入到代碼中君珠,以實(shí)現(xiàn)某種功能(相同的邏輯在同一個(gè)地方處理)寝志;從這種方面來(lái)想,在 BaseActivity 和 在Application 中添加 ActivityLifecycle 監(jiān)聽的也是一種AOP的實(shí)現(xiàn)方式(可以理解為策添,這種是有現(xiàn)成的切面的AOP)
學(xué)習(xí)之前需要理解一些AOP術(shù)語(yǔ)
AOP術(shù)語(yǔ):
- Cross-cutting concerns(橫切關(guān)注點(diǎn)):監(jiān)管面向?qū)ο竽P椭写蠖鄶?shù)類會(huì)實(shí)現(xiàn)單一特定的功能材部,但通常也會(huì)開放一些通用的附屬功能給其他類。例如唯竹,我們喜歡在數(shù)據(jù)訪問層中的類添加日志乐导,同時(shí)也希望當(dāng)UI層中一個(gè)縣城進(jìn)入或者退出調(diào)用一個(gè)方法時(shí)添加日志。監(jiān)管每個(gè)類都有一個(gè)區(qū)別于其他類的主要功能浸颓,但在代碼里物臂,仍然經(jīng)常需要添加一些相同的附屬功能旺拉。
- Advice(通知):注入到class文件中的代碼。典型的Advice類型有before棵磷、after和around蛾狗,分別表示在目標(biāo)方法執(zhí)行之前、執(zhí)行后和完全代替目標(biāo)方法執(zhí)行的代碼仪媒。除了在方法中注入代碼沉桌,也可能會(huì)對(duì)代碼做其他修改,比如在一個(gè)class中增加字段或者接口算吩。
- Joint Point(連接點(diǎn)):程序中可能作為代碼注入目標(biāo)的特定的點(diǎn)留凭,例如一個(gè)方法調(diào)用或者方法入口。
- Pointcut(切入點(diǎn)偎巢,切點(diǎn)):告訴代碼注入工具蔼夜,在任何注入一段特定代碼的表達(dá)式。例如压昼,在哪些joint points應(yīng)用一個(gè)特定的Advice求冷。切入點(diǎn)可以選擇唯一一個(gè),比如執(zhí)行某一個(gè)方法巢音,也可以有多個(gè)選擇遵倦,比如,標(biāo)記了一個(gè)定義成@DebugLog的自定義注解的所有方法官撼。
- Aspect(切面):Pointcut和Advice的組合看做切面。例如似谁,我們?cè)趹?yīng)用中通過定義一個(gè)pointcut和給定恰當(dāng)?shù)腶dvice傲绣,添加一個(gè)日志切面。
- Weaving(織入):注入代碼(advices)到目標(biāo)位置(joint points)的過程
AOP的Android實(shí)現(xiàn)
實(shí)現(xiàn)AOP的技術(shù)巩踏,主要分為兩大類:
- 采用動(dòng)態(tài)代理技術(shù)秃诵,利用截取消息的方式,對(duì)該信息進(jìn)行裝飾塞琼,以取代原有對(duì)象行為的執(zhí)行
- 采用靜態(tài)織入的方式菠净,引入特定的語(yǔ)句創(chuàng)建“方面”,從而使得編譯器可以在編譯期間織入有關(guān)“方面的代碼
AOP是一種思想彪杉,要使用這種思想就需要擁有這種思想的工具毅往,下面列舉我現(xiàn)在知道的AOP的實(shí)現(xiàn)方式:
- ASM+Transformer庫(kù):ASM是一個(gè)通用的Java字節(jié)碼操作和分析框架, Transfrom允許第三方插件在經(jīng)過編譯的 .class 文件轉(zhuǎn)換為 .dex 文件之前對(duì)其進(jìn)行操縱。ASM的class的字節(jié)碼過于復(fù)雜派近,總是出錯(cuò)攀唯,開發(fā)成本非常高,上手難度很大渴丸,但是ASM庫(kù)非常強(qiáng)大侯嘀,更加靈活
- AsepcJ:它是一種幾乎和Java完全一樣的語(yǔ)言另凌,而且完全兼容Java(AspectJ應(yīng)該就是一種擴(kuò)展Java,但它不是像Groovy那樣的拓展)戒幔。當(dāng)然吠谢,除了使用AspectJ特殊的語(yǔ)言外,AspectJ還支持原生的Java诗茎,只要加上對(duì)應(yīng)的AspectJ注解就好囊卜。所以,使用AspectJ有兩種方法:
- 完全使用AspectJ的語(yǔ)言错沃。這語(yǔ)言一點(diǎn)也不難栅组,和Java幾乎一樣,也能在AspectJ中調(diào)用Java的任何類庫(kù)枢析。AspectJ只是多了一些關(guān)鍵詞罷了玉掸。
- 或者使用純Java語(yǔ)言開發(fā),然后使用AspectJ注解醒叁,簡(jiǎn)稱 @AspectJ司浪。
- Hugo:另一個(gè)是Jake大神實(shí)現(xiàn)的Hugo庫(kù)
- Lancet:一個(gè)輕量級(jí)Android AOP框架
以下以AsepcJ來(lái)講解AOP
AsepcJ的使用實(shí)例
環(huán)境配置
通過插件的形式來(lái)配置AspectJ環(huán)境。 具體可見AspectJX Github地址
- 在項(xiàng)目根目錄的build.gradle里依賴AspectJX
buildscript {
dependencies {
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
}
- 在需要支持AspectJ的module的build.gradle文件中聲明插件把沼。
apply plugin: 'android-aspectjx'
在編譯階段AspectJ會(huì)遍歷工程中所有class文件(包括第三方類庫(kù)的class)尋找符合條件的切入點(diǎn)啊易,為加快這個(gè)過程或縮小代碼織入范圍,我們可以使用exclude排除掉指定包名的class饮睬。
# app/build.gradle
aspectjx {
//排除所有package路徑中包含`android.support`的class文件及庫(kù)(jar文件)
exclude 'android.support'
}
在debug階段我們更注重編譯速度租谈,可以關(guān)閉代碼織入。
# app/build.gradle
aspectjx {
//關(guān)閉AspectJX功能
enabled false
}
基本使用
- @Aspect 用它聲明一個(gè)類捆愁,表示一個(gè)需要執(zhí)行的切面割去。
- @Pointcut 聲明一個(gè)切點(diǎn)。
- @Before/@After/@Around/...(統(tǒng)稱為Advice類型) 聲明在切點(diǎn)前昼丑、后呻逆、中執(zhí)行切面代碼。
舉個(gè)例子:
@Aspect //聲明一個(gè)切面類菩帝,此處是固定的
public class MethodAspect {
// 此處指定一個(gè)切點(diǎn)咖城,后面括號(hào)中的是切點(diǎn)表達(dá)式(個(gè)人理解:其表達(dá)的就是一個(gè)join point),詳細(xì)見Aspect基本語(yǔ)法
@Pointcut("call(* com.wandering.sample.aspectj.Animal.fly(..))")
public void callMethod() {
}
//表示一個(gè)通知呼奢,類型為Before并指定切點(diǎn)為上面callMethod方法所表示的那個(gè)切點(diǎn)
@Before("callMethod()")
public void beforeMethodCall(JoinPoint joinPoint) {
Log.e(TAG, "before->" + joinPoint.getTarget().toString()); //織入的代碼
}}
AsepcJ的基本語(yǔ)法
官網(wǎng)文檔
本處基本摘于: Android AspectJ詳解
Join Point
Joint Point | 含義 |
---|---|
Method call | 方法被調(diào)用 |
Method execution | 方法執(zhí)行 |
Constructor call | 構(gòu)造函數(shù)被調(diào)用 |
Constructor execution | 構(gòu)造函數(shù)執(zhí)行 |
Static initialization | static塊初始化 |
Field get | 讀取屬性 |
Field set | 寫入屬性 |
Handler | 異常處理 |
Method call 和 Method execution的區(qū)別常拿來(lái)比較宜雀,其實(shí)就是調(diào)用與執(zhí)行的區(qū)別
就拿上面Animal的fly方法舉例。demo代碼如下:
Animal a = Animal();
a.fly();
如果我們聲明的織入點(diǎn)為call控妻,再假設(shè)Advice類型是before州袒,則織入后代碼結(jié)構(gòu)是這樣的。
Animal a = new Animal();
//...我是織入代碼
a.fly();
如果我們聲明的織入點(diǎn)為execution弓候,則織入后代碼結(jié)構(gòu)就成這樣了郎哭。
public class Animal {
public void fly() {
//...我是織入代碼
Log.e(TAG, "animal fly method:" + this.toString() + "#fly");
}}
本質(zhì)上的區(qū)別就是織入對(duì)象不同他匪,call被織入在指定方法被調(diào)用的位置上,而execution被織入到指定的方法內(nèi)部夸研。
Pointcut
Pointcuts是具體的切入點(diǎn)邦蜜,基本上Pointcuts 是和 Join Point 相對(duì)應(yīng)的。
Joint Point | Pointcuts 表達(dá)式 |
---|---|
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 對(duì)應(yīng)的選擇外亥至,Pointcuts 還有其他選擇方法悼沈。
Pointcuts表達(dá)式 | 說明 |
---|---|
within(TypePattern) | 符合 TypePattern 的代碼中的 Join Point |
withincode(MethodPattern) | 在某些方法中的 Join Point |
withincode(ConstructorPattern) | 在某些構(gòu)造函數(shù)中的 Join Point |
cflow(Pointcut) | Pointcut 選擇出的切入點(diǎn) P 的控制流中的所有 Join Point,包括 P 本身 |
cflowbelow(Pointcut) | Pointcut 選擇出的切入點(diǎn) P 的控制流中的所有 Join Point姐扮,不包括 P 本身 |
this(Type or Id) | Join Point 所屬的 this 對(duì)象是否 instanceOf Type 或者 Id 的類型 |
target(Type or Id) | Join Point 所在的對(duì)象(例如 call 或 execution 操作符應(yīng)用的對(duì)象)是否 instanceOf Type 或者 Id 的類型 |
args(Type or Id, ...) | 方法或構(gòu)造函數(shù)參數(shù)的類型 |
if(BooleanExpression) | 滿足表達(dá)式的 Join Point絮供,表達(dá)式只能使用靜態(tài)屬性、Pointcuts 或 Advice 暴露的參數(shù)茶敏、thisJoinPoint 對(duì)象 |
Pattern
Pattern類型 | 語(yǔ)法 |
---|---|
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ī)則也是一樣壤靶,可以使用 '!'、''惊搏、'..'贮乳、'+','!' 表示取反恬惯,'' 匹配除 . 外的所有字符串向拆,'*' 單獨(dú)使用事表示匹配任意類型,'..' 匹配任意字符串酪耳,'..' 單獨(dú)使用時(shí)表示匹配任意長(zhǎng)度任意類型浓恳,'+' 匹配其自身及子類,還有一個(gè) '...'表示不定個(gè)數(shù) |
說明:
@注解 訪問權(quán)限 返回值的類型 包名.函數(shù)名(參數(shù))
- @注解和訪問權(quán)限(public/private/protect葡兑,以及static/final)屬于可選項(xiàng)奖蔓。如果不設(shè)置它們,則默認(rèn)都會(huì)選擇讹堤。以訪問權(quán)限為例,如果沒有設(shè)置訪問權(quán)限作為條件厨疙,那么public洲守,private,protect及static沾凄、final的函數(shù)都會(huì)進(jìn)行搜索梗醇。
- 返回值類型就是普通的函數(shù)的返回值類型。如果不限定類型的話撒蟀,就用*通配符表示
- 包名.函數(shù)名用于查找匹配的函數(shù)叙谨。可以使用通配符保屯,包括和..以及+號(hào)手负。其中號(hào)用于匹配除.號(hào)之外的任意字符涤垫,而..則表示任意子package,+號(hào)表示子類竟终。
比如:
java..Date:可以表示java.sql.Date蝠猬,也可以表示java.util.Date
Test:可以表示TestBase,也可以表示TestDervied
java..:表示java任意子類
java..Model+:表示Java任意package中名字以Model結(jié)尾的子類统捶,比如TabelModel榆芦,TreeModel 等- 最后來(lái)看函數(shù)的參數(shù)惠啄。參數(shù)匹配比較簡(jiǎn)單氧骤,主要是參數(shù)類型愕宋,比如:
- (int, char):表示參數(shù)只有兩個(gè)形葬,并且第一個(gè)參數(shù)類型是int外盯,第二個(gè)參數(shù)類型是char
- (String, ..):表示至少有一個(gè)參數(shù)檀何。并且第一個(gè)參數(shù)類型是String扔亥,后面參數(shù)類型不限挂签。在參數(shù)匹配中兑凿,..代表任意參數(shù)個(gè)數(shù)和類型
- (Object ...):表示不定個(gè)數(shù)的參數(shù)凯力,且類型都是Object,這里的...不是通配符礼华,而是Java中代表不定參數(shù)的意思
Advice
直譯過來(lái)是通知咐鹤,實(shí)際上表示一類代碼織入位置,在AspectJ中有五種類型的注解:Before圣絮、After祈惶、AfterReturning、AfterThrowing扮匠、Around捧请,我們將它們統(tǒng)稱為Advice注解。
Advice | 說明 |
---|---|
@Before | 切入點(diǎn)前織入 |
@After | 切入點(diǎn)后織入棒搜,無(wú)論連接點(diǎn)執(zhí)行如何疹蛉,包括正常的 return 和 throw 異常 |
@AfterReturning | 只有在切入點(diǎn)正常返回之后才會(huì)執(zhí)行,不指定返回類型時(shí)匹配所有類型 |
@AfterThrowing | 只有在切入點(diǎn)拋出異常后才執(zhí)行力麸,不指定異常類型時(shí)匹配所有類型 |
@Around | 替代原有切點(diǎn)可款,如果要執(zhí)行原來(lái)代碼的話,調(diào)用 ProceedingJoinPoint.proceed() |
Advice注解修飾的方法有一些約束:
- 方法必須為public克蚂。
- Before闺鲸、After、AfterReturning埃叭、AfterThrowing 四種類型方法返回值必須為void摸恍。
- Around的目標(biāo)是替代原切入點(diǎn),它一般會(huì)有返回值赤屋,這就要求聲明的返回值類型必須與切入點(diǎn)方法的返回值保持一致立镶;不能和其他 Advice 一起使用壁袄,如果在對(duì)一個(gè) Pointcut 聲明 Around 之后還聲明 Before 或者 After 則會(huì)失效。
- 方法簽名可以額外聲明JoinPoint谜慌、JoinPointStaticPart然想、JoinPoint.EnclosingStaticPart。
常見方法
@Aspect 定義類為切入類
@Pointcut 聲明一個(gè)切入策略供
@Before @After @ Around @ AfterReturning選擇
@Before 被切入方法執(zhí)行前執(zhí)行
@After 被切入方法執(zhí)行后執(zhí)行
@Around 被切入方法前后都可以加入一些邏輯
@AfterReturning 被切入方法返回時(shí)執(zhí)行
JoinPoint 加入這個(gè)參數(shù)可以獲取被切入方法的名稱和參數(shù)
JoinPoint 對(duì)象
Signature getSignature();//獲取封裝了署名信息的對(duì)象,在該對(duì)象中可以獲取到目標(biāo)方法名,所屬類的Class等信息 (修飾符+包名+類名+方法名)
Object[] getArgs();//獲取傳入目標(biāo)方法的參數(shù)對(duì)象
Object getTarget();//獲取傳入目標(biāo)方法的參數(shù)對(duì)象
Object getThis();//獲取代理對(duì)象
getSignature().getName();//獲取方法名
ProceedingJoinPoint對(duì)象
只用在@Around的切面方法中欣范,是JoinPoint的子接口
Object proceed() throws Throwable //執(zhí)行目標(biāo)方法
Object proceed(Object[] var1) throws Throwable //傳入的新的參數(shù)去執(zhí)行目標(biāo)方法
實(shí)例(快速點(diǎn)擊)
AspectJ 缺點(diǎn)
- 如果相應(yīng)的class沒有實(shí)現(xiàn)相應(yīng)的切點(diǎn)方法將無(wú)法織入变泄,如上文中的沒有BlankFragment實(shí)現(xiàn)onResume方法的話,將無(wú)法織入代碼恼琼。
- 無(wú)法處理Lambda語(yǔ)法會(huì)有一系列兼容性問題妨蛹,如R8、gradle版本不同等性能較差
- APP項(xiàng)目比較大時(shí)編譯時(shí)間明顯加長(zhǎng)晴竞。
- 兼容性:如果使用的三方庫(kù)也使用了AspectJ蛙卤,可能導(dǎo)致未知的風(fēng)險(xiǎn)。