1. AOP簡介
??大家都知道OOP(面向對象編程)谭溉,而AOP(Aspect Oriented Programming)面向切面編程和OOP類似,都屬于一種編程思想的方法論橡卤。
??OOP的思想是模塊劃分扮念,模塊內完成各自的事務處理,模塊間通過定義好的接口進行交互碧库。而AOP的思想核心是切面柜与,一個經典的場景就是方法的耗時統(tǒng)計,假如有多個模塊嵌灰,每個模塊有多個類弄匕,每個類有多個方法。現在想統(tǒng)計每個方法的執(zhí)行耗時伞鲫,那么可以在每個方法的執(zhí)行前記錄時間粘茄,執(zhí)行完后輸出方法耗時。如果遵循面向對象的編程思想秕脓,那么每個方法都要添加代碼柒瓣,這種實現方式繁瑣且容易出問題,而AOP就很適合解決這類問題(比如hugo)吠架。下面簡要介紹幾個AOP相關的術語芙贫。
1.1. Advice(增強)
??增強是指被織入到目標代碼連接點上的一段代碼,也可以理解為Hook傍药,不同的AOP框架支持的增強種類不完全一致磺平。
1.2. JoinPoint(連接點)
??簡稱JPoint,程序代碼中的某個特點位置拐辽,可以是類的初始化拣挪、方法調用前、方法異常處理俱诸、變量的讀取等菠劝。意味著可以在這些連接點織入代碼,對原有代碼進行增強睁搭。不同的實現框架赶诊,能支持的連接點會有所區(qū)別笼平,比如AspectJ框架支持變量讀取的連接點,而Spring框架不支持舔痪。
1.3. PointCut(切點)
??切點是指具體要織入代碼的位置寓调,也是我們要關注的連接點。換句話說锄码,切點就是從支持的連接點中選擇我們要關注的那部分夺英,所以切點也是連接點的一個子集。
1.4. Aspect(切面)
??切面由切點和增強兩部分組成巍耗,一般AOP框架所實現的功能就是將切面所定義的增強代碼織入到切面所定義的連接點秋麸。
1.5. Weaving(織入)
??上文中多次提到織入,是指將增強代碼添加到具體的切點的過程炬太【捏。可以根據織入的實現方式對AOP框架進行大致的分類。
1.6. Target(目標)
??用戶定義的切面的切點所屬的目標類亲族,用戶在定義切面時可指定Target炒考,不指定的情況下默認是所有類。
2. AOP的實現方式
??這里按照代碼織入時機將AOP的實現方式分為以下兩種霎迫,對于同一種AOP框架在JVM環(huán)境和在Android環(huán)境下可能會采用不同的接入方式斋枢。
2.1. 編譯期織入
??指織入動作發(fā)生在編譯過程。比如AspectJ框架是通過特殊的Java編譯器(ajc)知给,在編譯階段從Java源代碼編譯成class字節(jié)碼期間織入代碼的瓤帚。
2.2. 運行時織入
??運行時織入最主要的一種體現是使用動態(tài)代理的方式在運行期為目標類生成子類,經典的Spring框架就是采用動態(tài)代理實現涩赢。
??相比編譯期織入方式戈次,運行時織入的優(yōu)點是對編譯時間的影響較小,其缺點也很明顯筒扒,由于在編譯過程可能會動態(tài)生成字節(jié)碼怯邪,所以對運行時的性能會有所影響。
3. AspectJ 框架
??AspectJ(官網)是在Java語言上實現AOP的框架花墩,前面有提到它的織入時機是在編譯期悬秉,使用特殊的Java編譯器:ajc編譯器。正常情況下要使用AspectJ必須使用ajc編譯器來編譯java源代碼冰蘑。ajc編譯器可以直接替代常規(guī)的javac編譯器和泌。但是對于一個有很多依賴組件的項目而言,這種使用方法有一個比較明顯的缺點祠肥,即對依賴組件無效允跑。所有依賴的組件最終是以字節(jié)碼的形式添加到宿主項目中,因此它們無需再次編譯,也就無法在里面織入代碼聋丝。
??由于上述所說的問題,對于Android平臺上如果希望AOP框架能對依賴組件生效工碾,可以選擇通過gradle插件的形式使用AspectJ弱睦,比如滬江技術的AspectJ封裝庫(簡稱滬江AspectJ),下圖是使用常規(guī)方式的AspectJ和使用滬江AspectJ的實現區(qū)別渊额。
3.1. 支持的Advice
??如下表是AspectJ支持的Advice種類和說明况木,對于不同的Advice種類AspectJ會采用不同的方式織入代碼。
Advice 種類 | 說明 |
---|---|
@before(JPoint) | 某個JPoint執(zhí)行前 |
@after(JPoint) | 某個JPoint執(zhí)行后 |
@afterReturning(JPoint) | 某個JPoint(方法)正常返回后 |
@afterThrowing(JPoint) | 某個JPoint(方法)拋出未捕獲異常 |
@around(JPoint) | 包圍JPoint |
??如下是使用各種Advice的示例代碼旬迹,AdviceTest定義兩個方法(在這里就是目標類)火惊,AdviceTestAspect是定義切面的類。
public class AdviceTest {
public String method_1(int arg) {
String result = "result" + arg;
return result;
}
public String method_2(int arg) {
String result = "result" + arg;
return result;
}
}
@Aspect
public class AdviceTestAspect {
private static final String TAG = "AdviceTestAspect";
@Before("execution(* com.aop.test.AdviceTest.method_1(..))")
public void before(JoinPoint jPoint) {
Log.e(TAG, "before");
}
@After("execution(* com.aop.test.AdviceTest.method_1(..))")
public void after(JoinPoint jPoint) {
Log.e(TAG, "after");
}
@AfterReturning("execution(* com.aop.test.AdviceTest.method_1(..))")
public void afterReturning(JoinPoint jPoint) {
Log.e(TAG, "afterReturning");
}
@AfterThrowing("execution(* com.aop.test.AdviceTest.method_1(..))")
public void afterThrowing(JoinPoint jPoint) {
Log.e(TAG, "afterThrowing");
}
@Around("execution(* com.aop.test.AdviceTest.method_2(..))")
public Object around(ProceedingJoinPoint jPoint) throws Throwable{
Log.e(TAG, "around");
return jPoint.proceed();
}
}
??我們看下織入后的代碼是什么樣的奔垦,如下是編譯后再反編譯出來的代碼:
public class AdviceTest {
private static final /* synthetic */ JoinPoint.StaticPart ajc$tjp_0 = null;
private static final /* synthetic */ JoinPoint.StaticPart ajc$tjp_1 = null;
static {
ajc$preClinit();
}
private static /* synthetic */ void ajc$preClinit() {
Factory factory = new Factory("AdviceTest.java", AdviceTest.class);
ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, (Signature) factory.makeMethodSig("1", "method_1", "com.aop.test.AdviceTest", "int", HelpFormatter.DEFAULT_ARG_NAME, "", "java.lang.String"), 5);
ajc$tjp_1 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, (Signature) factory.makeMethodSig("1", "method_2", "com.aop.test.AdviceTest", "int", HelpFormatter.DEFAULT_ARG_NAME, "", "java.lang.String"), 10);
}
public String method_2(int arg) {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_1, (Object) this, (Object) this, Conversions.intObject(arg));
return (String) Log.e("AdviceTestAspect", "around");
}
public String method_1(int arg) {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, (Object) this, (Object) this, Conversions.intObject(arg));
try {
TTest.aspectOf().before(makeJP);
String str = "result" + arg;
TTest.aspectOf().after(makeJP);
TTest.aspectOf().afterReturning(makeJP);
return str;
} catch (Throwable th) {
TTest.aspectOf().afterThrowing(makeJP);
throw th;
}
}
private static final /* synthetic */ String method_2_aroundBody0(AdviceTest ajc$this, int arg, JoinPoint joinPoint) {
return "result" + arg;
}
}
??對于同一個JPoint不能有多個Advice屹耐,會導致不能正常織入代碼椿猎,編譯日志可以看到報錯惶岭。這也是上面示例代碼需要定義兩個方法的原因,around類型不能和其他類型一起使用犯眠,大部分情況下也沒必要按灶,因為around就能實現其他所有類型的功能。
??從上面示例可以看出筐咧,around類型會織入較多的代碼(至少比其他類型多出兩個靜態(tài)方法)鸯旁,有時候還會生成子類,所以相對而言其它類型會更輕量量蕊,實踐中可以考慮優(yōu)先使用其他類型铺罢。
3.2. 支持的JPoint
??如下圖所示是AspectJ支持的JPoint種類說明和使用表達式。
Joint Point 種類 | Pointcut 表達式 |
---|---|
Method call (方法調用) | call(MethodSignature) |
Method execution (方法執(zhí)行) | execution(MethodSignature) |
Constructor call (構造方法調用) | call(MethodSignature) |
Constructor execution (構造方法執(zhí)行) | execution(MethodSignature) |
Field get (讀取某個變量) | get(FieldSignature) |
Field set (設置某個變量) | set(FieldSignature) |
Class initialization (類初始化) | staticinitialization(TypeSignature) |
Exception handler (異常處理) | handler(TypeSignature) |
Object initialization (對象初始化) | initialization(ConstructorSignature) |
Object pre-initialization (對象初始化前) | preinitialization(ConstructorSignature) |
Advice execution | adviceexecution() |
3.3. JPoint的選擇項
??可以使用如下選擇項對JPoint進行篩選過濾危融,選項之間可以使用邏輯表達式結合使用畏铆。
JPoint選擇 | 說明 |
---|---|
within(TypePattern) | TypePattern表示包名或類,支持通配符吉殃。表示某個Package或者類中的所有JPoint辞居,靜態(tài)判斷。 |
withincode( ConstructorSignature| MethodSignature) |
表示某個構造方法或其他方法代碼中涉及到的JPoint |
cflow(pointcuts) | 表示調用某個方法時所包含的所有JPoint蛋勺,包含頂級方法的調用本身 |
cflowbelow(pointcuts) | 和cflow一樣瓦灶,不包含頂級方法的調用本身 |
this(Type) | JPoint 代碼段所屬的 this 對象是否 instanceOf Type,需要動態(tài)判斷抱完。 |
target(Type) | JPoint 所要搜索的目標對象是否 instanceOf Type贼陶,需要動態(tài)判斷。 |
args(TypeSignature) | 用來對JPoint的參數進行條件搜索 |
3.4. 切面的定義語法
??定義一個切面的格式:@Pointcut("execution(Signature)"),下面對Signature分類講解碉怔。
3.4.1. MethodSignature
??格式:@注解 訪問權限 返回值的類型 包名.方法名(參數)
??1. @注解和訪問權限(public/private/protect烘贴,以及static/final)屬于可選項配乓。不設置表示全部包含黍判。
??2. 返回值類型就是普通的方法的返回值類型,可以使用通配符表示不限定類型掀亩。
??3. 包名.方法名用于查找匹配的方法芹啥《屠耄可以使用通配符,包括和..以及+號墓怀。其中*號用于匹配除.號之外的任意字符汽纠,而..則表示任意子package,+號表示子類傀履。
com.*.Log :可以表示com.common.Log虱朵,也可以表示com.util.Log
Log* :可以表示LogUtil,也可以表示Log
com..* :表示com開頭的任意包下的所有類
??4. 方法參數如下啤呼, ..代表任意參數個數和類型:
??(int, char):表示參數有兩個卧秘,第一個參數類型是int,第二個參數類型是char官扣。
??(String, ..):表示至少有一個參數翅敌。第一個參數類型是String,后面參數類型和個數不限惕蹄。
3.4.2. ConstructorSignature
??和MethodSignature類似蚯涮,只不過構造方法沒有返回值,而且方法名必須叫new卖陵,*..代表任意包名遭顶。比如:
??public *..TestAspect.new(..):表示任意包下的TestAspect類的任意構造方法。
3.4.3. FieldSignature
??格式:@注解 訪問權限 類型 類名.成員變量名
??和MethodSignature基本一致泪蔫,比如:
??set(String TestAspect.tag):表示設置TestAspect.tag變量時的連接點棒旗。
3.4.4. TypeSignature
??就是類型,支持通配符撩荣,比較簡單這里就不舉例了铣揉。
4. Android AOP 方案
??AspectJ 并不是Android平臺上AOP框架的唯一選擇,當然可能也并不是最佳選擇餐曹。這一節(jié)主要介紹在Android平臺上實現AOP的可選方案逛拱。
4.1. Javassist
??是一個用來處理class文件的類庫,可以編輯或創(chuàng)建class文件台猴。Javassist在使用上比較友好朽合,是基于java源碼級別來編輯class文件俱两。
4.2. Asm
??Asm是一個功能齊全、應用廣泛且成熟的Java字節(jié)碼分析和處理框架曹步。和Javassist類似宪彩,可以在字節(jié)碼層面增強已有的類,也可以生成新的字節(jié)碼文件箭窜。在JVM環(huán)境下毯焕,Asm支持代碼運行期動態(tài)增強和新增類。在Android平臺下磺樱,由于其環(huán)境的特殊性導致無法支持在運行期動態(tài)增強的特性,Android平臺上使用Asm一般是通過Gradle plugin的Transform來實現婆咸。因此在Android平臺上通過Asm實現AOP只能是編譯期織入的形式竹捉。
4.3. cglib
??cglib是一個動態(tài)代理框架,由于動態(tài)代理框架的實現方式非常符合AOP思想尚骄,因此動態(tài)代理也可以作為實現AOP的一種方式块差。
??jdk有標準的動態(tài)代理實現庫,但有一個比較明顯的缺點:被代理的類必須實現一個公開的接口倔丈,由于現實中的代碼很難都滿足這個條件憨闰,因此誕生了cglib。其實cglib就是通過Asm庫實現的動態(tài)代理需五,它不要求被代理的類一定要實現接口鹉动,因此更靈活。前面提到的Spring框架宏邮,實現原理就是基于jdk標準的動態(tài)代理實現庫和cglib共同來完成其AOP特性(通過被代理的類是否有實現接口來選擇實現方案)泽示。
??cglib雖好,遺憾的是它并不能直接在Android平臺上使用蜜氨,因為它是通過運行期動態(tài)生成字節(jié)碼來實現動態(tài)代理械筛,但動態(tài)生成的字節(jié)碼無法在Android App運行過程中直接使用。Android App運行過程中動態(tài)加載的是dex文件飒炎。因此如果要使用cglib埋哟,當前有一種方案是結合dexmaker動態(tài)生成dex實現(有興趣可參考這里:項目地址)。dexmaker是一種Android App運行時代碼生成器郎汪,可在運行期動態(tài)生成dex文件赤赊,然后通過類加載器加載到內存使用(項目地址)。
4.4. Dexposed 和 epic
??Dexposed項目大概是6年前阿里開源的怒竿,能夠在Dalvik上實現Java運行時AOP砍鸠。實現方式基于Dalvik下的底層Hook技術,跟Dalvik的實現機制緊緊綁定一起耕驰,但從Android M開始爷辱,ART取代了Dalvik成為了Android平臺上的Java運行時環(huán)境,因此Dexposed從M開始就無法正常使用。
??epic是基于ART重新實現了Dexposed的版本饭弓,這種真正運行時的動態(tài)AOP框架双饥,可以用于實現輕量級的熱修復功能。具體實現方式可看這篇博客:我為Dexposed續(xù)一秒弟断∮交ǎ總體來說,基于底層Hook技術的方案的穩(wěn)定性和系統(tǒng)版本兼容性都比較差阀趴,因此不建議用在項目的線上環(huán)境昏翰。
5. AspectJ應用案例——方法日志工具
??基于hugo框架,在其基礎上進行優(yōu)化和擴展刘急,實現一個快速打印方法日志的工具棚菊。如下是使用該日志工具示例代碼,支持兩種使用方式@DebugLog和@CustomLog叔汁,后者支持定制化輸出日志统求。
public class LogTest {
@DebugLog
public String methodDebug(int vInt) throws Exception {
Thread.sleep(300);
return "callLog1 result " + vInt;
}
@CustomLog(tag = "customLog_test", tagClass = false, time = true, parameter = true, thread = true, level = LevelEnum.ERROR)
public String methodCustom(int vInt, List<String> vList, int[] vArray) throws Exception {
Thread.sleep(300);
return "callLog2 result " + vInt;
}
}
??當上面methodDebug和methodCustom方法被調用后,就會輸出如下日志內容:
D/DEBUG_LOG: [LogTest] ? methodDebug(vInt=1) [Thread:"main"] (LogTest.java:16)
D/DEBUG_LOG: [LogTest] ? methodDebug [301ms] = "callLog1 result 1"
E/customLog_test: [LogTest] ? methodCustom(vInt=2, vList=[eme_1, eme_2], vArray=[7, 8, 9]) [Thread:"main"] (LogTest.java:26)
E/customLog_test: [LogTest] ? methodCustom [301ms] = "callLog2 result 2"
??切面定義代碼如下:
@Aspect
public class CustomLogAspect {
/**
* 使用{@link CustomLog}注解的類的所有方法
*/
@Pointcut("within(@com.aop.log.annotation.CustomLog *)")
public void withinAnnotatedClass() {
}
/**
* 使用{@link CustomLog}注解的類的所有方法(排除編譯器生成的內部類)
*/
@Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
public void methodInsideAnnotatedType() {
}
/**
* 使用{@link CustomLog}注解的類的所有方法(排除編譯器生成的內部類的構造方法)
*/
@Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
public void constructorInsideAnnotatedType() {
}
/**
* 使用{@link CustomLog}注解的方法据块,或使用{@link CustomLog}注解的類的所有方法(排除編譯器生成的內部類)
*/
@Pointcut("execution(@com.aop.log.annotation.CustomLog * *(..)) || methodInsideAnnotatedType()")
public void method() {
}
/**
* 使用{@link CustomLog}注解的構造方法码邻,或使用{@link CustomLog}注解的類的所有方法(排除編譯器生成的內部類的構造方法)
*/
@Pointcut("execution(@com.aop.log.annotation.CustomLog *.new(..)) || constructorInsideAnnotatedType()")
public void constructor() {
}
/**
* 包含以上兩種
*/
@Pointcut("method() || constructor()")
public void logMethod() {
}
@Before("logMethod() && @annotation(customLog)")
public void before(JoinPoint joinPoint, CustomLog customLog) {
LogController.before(joinPoint, customLog);
}
@AfterThrowing(value = "method()", throwing = "throwable")
public void afterThrowing(JoinPoint joinPoint, Throwable throwable) {
LogController.afterThrowing(joinPoint, throwable);
}
@AfterReturning(value = "method()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
LogController.afterReturning(joinPoint, result);
}
@After("constructor()")
public void after(JoinPoint joinPoint) {
LogController.after(joinPoint);
}
}
6. 總結
6.1. AOP在Android應用場景
??事件全埋點,慢函數監(jiān)控另假,依賴組件的代碼修復能力像屋,排查未知調用問題的利器,特定方法搜索和攔截等浪谴。
??大部分App都會依賴較多的組件开睡,而有些被依賴的組件的源代碼并不是宿主項目所能控制的,當被依賴的組件出問題時苟耻,宿主項目對于依賴組件代碼的控制能力就顯得尤其重要篇恒,無論是AspectJ還是Asm都是應對這類問題的一種強有力的工具。
6.2. Android平臺下使用AspectJ的注意事項
??1. 觀察編譯日志凶杖,有時候能正常編譯打包并不意識著全部織入成功胁艰,而只要有一個地方織入失敗,都有可能導致在運行期出現不可預期的崩潰(比如找不到類)智蝠,可以通過包體對比來輔助確保打出來的包沒有問題腾么。
??2. 反編譯apk觀察代碼的織入情況。由于Aspectj在Android上的使用還不是很普及杈湾,相關的介紹文檔還不夠齊全解虱,所以這一步很重要,一定要反編譯看漆撞。
??3. 前面有提到around是比較重量的殴泰,所以優(yōu)先選擇其他4種類型會更輕量于宙。
6.3. Android環(huán)境下AOP的選擇和展望
??AspectJ框架是一個較為完備的AOP框架,功能強大靈活悍汛,快速接入使用捞魁,目前來看是一個很好的選擇。但是也存在一些缺點:
??1. 非官方支持离咐,AspectJ官方目前還沒有對其在Android平臺上提供支持方案谱俭。
??2. 在Android平臺上的使用案例不多,需要自行摸索才能搞清楚其最佳使用方法宵蛀。
??考慮到AspectJ的上述缺點昆著,從長遠來看Asm可能是更好的選擇,雖然Asm本身并不是AOP框架术陶,但這并不重要宣吱,重要的是我們可以借助Asm處理AOP以及其它事務,當然Asm的接入成本比AspectJ高一些瞳别,但其優(yōu)點也比較明顯:
??1. 官方支持,使用Asm不需要再借助三方提供的封裝庫杭攻,直接通過Android官方支持的編譯時transform任務祟敛,然后使用官方的Asm庫對字節(jié)碼操作即可。
??2. 靈活兆解,直接對字節(jié)碼操作馆铁,不被限制在AOP框架支持的范圍內,能實現AOP所不具備的能力锅睛。自行實現方式也可以避免產生過多的類文件埠巨。
??3. 高效,Asm的字節(jié)碼處理效率相比Javassist高很多现拒,減少了對編譯速度的影響辣垒。
??4. 使用廣泛成熟,前面有提到cglib和Spring都是基于Asm去實現相應功能印蔬。