What?
As we all know,在進行項目構(gòu)建時,追求各模塊高內(nèi)聚,模塊間低耦合。然而現(xiàn)實并不總是如此美好,某些通用功能是橫跨并嵌入到其他各模塊中的。比如日志打印、方法的執(zhí)行時間統(tǒng)計、參數(shù)校驗等。這些功能星羅棋布得分散在項目工程中,既不方便統(tǒng)一管理,也不利于后期維護。
如何對這些零散卻通用的功能做一些優(yōu)化處理呢? AOP(Aspect-Oriented Programming),為我們提供了一種新的思路。AOP翻譯成中文是面向切面編程,其與OOP一樣,是一種編程范式。如果說OOP是把問題劃分到單個模塊的話,那么AOP就是把涉及到眾多模塊的某一類問題進行統(tǒng)一管理。AOP可通過預(yù)編譯和運行時動態(tài)代理的方式實現(xiàn),從而更好地組織工程和代碼結(jié)構(gòu),AOP的實現(xiàn)原理如圖1所示。

AOP已廣泛應(yīng)用于服務(wù)端編程,并取得了巨大的成功,如大名鼎鼎的Spring框架。而在本文中,我們將AOP引入Android工程,用以解決開篇提到的編程痛點。閱讀完本文后便會發(fā)現(xiàn),AOP在Android項目中同樣可以做一些有趣的事。
Where?
AOP在Android工程中能用在哪?或者說能用AOP來做什么?下面列舉了一些使用場景:
- 日志記錄;
- 持久化;
- 性能監(jiān)控;
- 方法的參數(shù)校驗;
- 緩存;
- 權(quán)限檢查:業(yè)務(wù)權(quán)限(如登陸,或用戶等級)、系統(tǒng)權(quán)限(如拍照定位)。
- 其他更多
當(dāng)然,用AOP還可以實現(xiàn)一些具體的業(yè)務(wù)需求。
How?
介紹了這么多AOP的概念,那么如何進行具體應(yīng)用呢?下面介紹幾種現(xiàn)有的工具和類庫,可以很方便地實現(xiàn)AOP編程。
- AspectJ 一種基于JavaTM編程語言的面向切面編程無縫擴展,適用Android,編譯期織入目標字節(jié)碼文件中,實現(xiàn)無縫侵入。
- Javassist for Android 知名Java類庫Javassist的Android平臺移植版,同樣操作字節(jié)碼文件。
- DexMaker 在Dalvik VM上編譯期或運行時生成目標代碼的Java API。
- ASMDEX 一個類似ASM的字節(jié)碼操作庫,運行在Android平臺,操作字節(jié)碼文件。
琳瑯滿目的兵器庫中,當(dāng)然要選擇一件最趁手的,就是AspectJ了。選擇AspectJ的具體原因主要有三點:1、功能強大,能滿足大部分需求;2、支持編譯期和加載時代碼注入,無侵入實現(xiàn);3、易于使用。
AspectJ是一種幾乎和Java完全一樣的語言而且完全兼容Java。使用AspectJ有兩種方法,除了使用AspectJ的語法之外,還支持原生Java。然而,無論使用哪種方法,最后都需要AspectJ的編譯工具ajc來實現(xiàn)編譯,由于AspectJ實際上脫胎于Java,所以ajc工具也能編譯Java源碼。
AspectJ支持編譯期和加載時代碼注入,可以理解為是一種Hook。在開始之前,我們先看幾個關(guān)鍵詞:Join Point、Pointcut、Advice、Aspect、Weaving。這幾個關(guān)鍵詞在AspectJ中的抽象關(guān)系如圖2所示。

接下來詳細闡述各個關(guān)鍵詞的含義和作用。
- Join point: 程序中可能作為代碼注入目標的特定的入口。通俗來說就是可能被我們Hook的入口,這個入口可以是方法調(diào)用、執(zhí)行、某個變量、類初始化、異常處理等,Join Point的常用類型如表所示。
| Join Point | 描述 | 示例 |
|---|---|---|
| method call | 方法調(diào)用 | 如調(diào)用a.method(),此處為Join Point |
| method execution | 方法執(zhí)行 | 如a.method()執(zhí)行內(nèi)部,此處為Join Point |
| constructor call | 構(gòu)造方法調(diào)用 | 同method call類似 |
| constructor execution | 構(gòu)造方法執(zhí)行 | 同method execution類似 |
| field get | get 變量 | 如讀取a.field成員變量,此處為Join Point |
| field set | set 變量 | 如設(shè)置a.field成員變量,此處為Join Point |
| static initialization | 類初始化 | 如class A中的static{},此處為Join Point |
| handler | 異常處理 | 如try-catch(xxx)中對應(yīng)catch內(nèi)部的執(zhí)行 |
- Pointcut: 告訴代碼注入工具,在何處注入一段特定代碼的表達式。可以將Ponitcut理解為Join Point的過濾規(guī)則,通過過濾規(guī)則篩選出一些特定的Join Point。
舉個Pointcut的栗子可能更容易說明問題:
public pointcut test call(public * *.method(…)) && !within(A);
- 第一個public:表示該pointcut的訪問權(quán)限是public的,這與AspectJ的繼承關(guān)系有關(guān),屬于AspectJ的高級語法,涉及到的應(yīng)用場景較少,此處暫不討論;
- pointcut:關(guān)鍵字,表示該語句是一個pointcut;
- test:pointcut可分為具名和匿名兩種形式,考慮方便調(diào)用起見,建議使用具名的方式。namedPointcut的冒號后面部分是真正pointcut的內(nèi)容,也就是篩選規(guī)則;
- call:表示選擇的Join point是一種“方法調(diào)用”類型,對應(yīng)表一的method call,下表展示了常用的Pointcut類型;
| Join Point類型 | Pointcut類型 |
|---|---|
| method call | call(MethodSignature) |
| method execution | execution(MethodSignature) |
| constructor call | call(ConstructorSignature) |
| constructor execution | execution(ConstructorSignature) |
| Field read access | get(FieldSignature) |
| field write access | set(FieldSignature) |
| static initialzation | staticinitialzation(TypeSignature) |
| handler | handler(TypeSignature) |
Pointcut的過濾規(guī)則語法內(nèi)容較多,規(guī)則如下:
@注解(可選) 訪問權(quán)限(可選) 返回值的類型 包名.函數(shù)名(參數(shù))
- 第二個public:表示的是目標Join Point的訪問權(quán)限,為可選項,如目標Join Point是一個方法,則該方法的訪問類型為public。如果不設(shè)置,則默認所有訪問權(quán)限(public|protected|defult|private)都匹配;
- **.method(..):第一個*表示的是方法返回值類型,此處為任意類型。第二個*表示完整的類名,此處為任意包名和類名。method表示方法名稱,..表示參數(shù)類型和參數(shù)個數(shù),此處意為任意類型和任意參數(shù)個數(shù)。包名.方法名用于查找匹配的方法,可以使用通配符。包括*和..和+。其中*匹配除.之外的任意字符,而..則匹配某個包下的任意子包,+匹配某個類的任意子類。舉些栗子:
com.*.util:可以表示com.netease.util,也可以表示com.google.util;
com.netease.*AOP,可以表示com.netease.AndroidAOP,也可以表示com.neteast.JavaAOP;
com..*表示com包中任意子包下的任意子類;
com..*AOP+表示com包下任意子包下以AOP結(jié)尾的子類,如AndroidAop的子類;
方法參數(shù)也有相應(yīng)的匹配規(guī)則:
(int, String):表示第一個參數(shù)為int,第二個參數(shù)為String;
(int, ..):表示第一個參數(shù)為int,后面參數(shù)任意數(shù)量和類型;
(String …):表示不定參數(shù),且類型都為String;
(..):表示任意數(shù)量和類型的參數(shù);
- !within(A):這是對Join Point的附加篩選。與Java語法相似,!代表非邏輯,within(A)表示某個包或類中的所有Join Point,此處為類A中所有Join Point。常用的附加篩選條件如表所示。
| 附加篩選 | 描述 | 示例 |
|---|---|---|
| within(TypeSignature) | 參數(shù)表示package或class,可使用通配符 | 如within(A),表示class A中的所有Join Point |
| withincode(Constructor&Method) | 與within類似,匹配精確到方法 | 如within(A.method())表示class的method()涉及到的所有Join Point |
| this(Type) | 判斷Join Point是否是Type類型 | Join Point屬于某個類,this用以判斷該Join Point所在的類是否是Type類型 |
| args(TypeSignature) | 對參數(shù)進行條件搜索 | args(String...),表示參數(shù)為String的不定參數(shù) |
所以,總結(jié)起來,上文的Pointcut的栗子的語義就是:
選擇那些任意包名和類名的&&訪問權(quán)限為public的&&方法名為method的&&參數(shù)任意的方法作為目標Join Point。但需要排除class A中的所有Join Point。
- Advice: 典型的 Advice 類型有 before、after 和 around,分別表示在目標方法執(zhí)行前、執(zhí)行后和完全替代目標方法執(zhí)行的代碼。Advice的作用就是,在Pointcut篩選出特定的執(zhí)行點和入口,做真正的Hook操作。常見的Advice類型如表所示。
| Advice類型 | 描述 |
|---|---|
| before() | 表示在Join Points之前前Hook并執(zhí)行 |
| after() | 表示在Join Points執(zhí)行完之后Hook并執(zhí)行 |
| after():returning(返回值類型) | 方法執(zhí)行完正常返回處Hook并執(zhí)行 |
| after():throwing(異常類型) | 方法異常退出點Hook并執(zhí)行 |
| around() | around作用是取代原Join Points |
- Aspect: Pointcut 和 Advice的組合可看做切面Aspect,這是一個抽象的概念,理解即可。例如,我們在應(yīng)用中通過定義一個 Pointcut 和給定恰當(dāng)?shù)腁dvice,添加一個日志切面,將該切面切入原有的業(yè)務(wù)代碼中。
- Weaving: 注入代碼(Advices)到目標位置(Join points)的過程,由編譯器ajc完成。
引入AOP的栗子
在項目中引入AOP的方式共有三種,分別是原生AspectJ語言,在Java代碼中使用AspectJ,結(jié)合自定義注解的方式在Java代碼中使用AspectJ??紤]到易用性和非侵入性,本栗子的方式是在Java代碼中使用AspectJ,在不修改業(yè)務(wù)代碼的前提下,實現(xiàn)onCreate方法性能的監(jiān)控。
首先考慮未引入AOP的情形,我們?nèi)绾瓮瓿蓪I(yè)務(wù)代碼的性能監(jiān)控。
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TimeWatcher watcher = new TimeWatcher();
watcher.start();
initView();
watcher.stop();
Log.i(TAG, "onCreate execute with " + watcher.getTotalTime() + "ms");
}
很容易可以發(fā)現(xiàn),業(yè)務(wù)代碼和性能監(jiān)控的功能性代碼交織在一起,結(jié)構(gòu)不清晰,耦合度較高。而且更關(guān)鍵的是,每當(dāng)一處業(yè)務(wù)代碼需要做性能統(tǒng)計,就需要把統(tǒng)計代碼重新復(fù)制一份。后期如果需要修改性能統(tǒng)計代碼的邏輯,比如修改統(tǒng)計日志內(nèi)容、增加日志上傳功能,需要找到每一處引用到的代碼進行修改,這樣無疑增加了工作量和出錯的概率。
引入AOP可以很好地剝離項目中業(yè)務(wù)代碼和性能統(tǒng)計代碼。首先創(chuàng)建aspect Module,引入AspectJ的相關(guān)類庫,并構(gòu)建主類和輔助類,項目結(jié)構(gòu)如圖3所示。

在aspect這個module下新建一個Java類。
@Aspect
public class TimeWatchAspect {
private static final String POINTCUT_TIME_WATCH =
"execution(* com.netease.aopdemo.MainActivity.onCreate(..))";
@Pointcut(POINTCUT_TIME_WATCH)
public void timeWatch() {}
@Around("timeWatch()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
String methodName = ms.getName();
String className = ms.getDeclaringType().getSimpleName();
TimeWatcher watcher = new TimeWatcher();
watcher.start();
Object result = joinPoint.proceed();
watcher.stop();
Log.i(className, methodName + " execute with " + watcher.getTotalTime() + "ms");
return result;
}
}
打上@Aspect注解,則該類可以被ajc編譯器識別為一個Asepct,在工程項目編譯時便能非常方便地實現(xiàn)代碼織入。圖5中可以看到AspectJ的三個要素,Join Point、Advice和Aspect。好像少了Join Point?Join Point早已定義在Pointcut的字符串常量中,即MainActivity的onCreate方法。Pointcut以注解的形式定義,注解了timeWatch方法,從而timeWatch就是這個Pointcut的名稱,注解參數(shù)則使用定義好的字符串常量,作為Join Point的過濾規(guī)則。同樣,Advice也是將類型關(guān)鍵字(此處為Around)注解在特定的方法weaveJoinPoint之上,注解的參數(shù)為具名的Pointcut,即timeWatch。上文提到Around類型即用該方法替換原Join Point的實現(xiàn),圖5中Object result = joinPoint.proceed()等價于原有的被Hook方法,即MainActivity的onCreate()。在該語句的前后,是性能統(tǒng)計的代碼片段。
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener {
private static final String TAG = MainActivity.class.getSimpleName();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
}
再看一眼我們重構(gòu)后的業(yè)務(wù)代碼,僅需要專注于業(yè)務(wù)邏輯處理,是不是變得非常簡潔。
編譯并運行項目代碼后,結(jié)果如圖4所示。

那么ajc編譯器在字節(jié)碼層面到底做了什么呢?
protected void onCreate(Bundle paramBundle)
{
JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_0, this, this, paramBundle);
TimeWatchAspect.aspectOf().weaveJoinPoint(new MainActivity.AjcClosure1(new Object[] { this, paramBundle, localJoinPoint }).linkClosureAndJoinPoint(69648));
}
public class MainActivity$AjcClosure1 extends AroundClosure
{
public MainActivity$AjcClosure1(Object[] paramArrayOfObject)
{
super(paramArrayOfObject);
}
public Object run(Object[] paramArrayOfObject)
{
Object[] arrayOfObject = this.state;
MainActivity.onCreate_aroundBody0((MainActivity)arrayOfObject[0], (Bundle)arrayOfObject[1], (JoinPoint)arrayOfObject[2]);
return null;
}
}
反編譯項目生成的apk后可以看到,ajc在Join Point處織入了代碼,用TimeWatchAspect.aspectOf().weaveJoinPoint()實現(xiàn)了替換。
踩過的坑
正所謂人不能兩次踏進同一條河流,我踩過的坑希望大家可以避免。
由于AspectJ在字節(jié)碼層面將功能性代碼織入業(yè)務(wù)代碼中,源碼層面無法看到變化,且無法在功能性代碼中進行斷點調(diào)試。所以一旦出錯,調(diào)試成本相對較高。如果項目運行結(jié)果與預(yù)期不符,首先檢查編譯問題,能否正常實現(xiàn)代碼織入(可以看apk中的class文件樹結(jié)構(gòu)),再檢查Join Point、Pointcut和Advice是否符合AspectJ語法,Hook是否正確。
如果Android Studio中的Instant Run開啟,則在編譯時可能會影響代碼的正??椚耄越ㄗh關(guān)閉Instant Run。
另外,一般初級階段會選擇日志打印的方式驗證AspectJ接入的可行性。如果測試機是魅族系列手機,則注意把項目中Log等級提升到D以上,或者在手機的開發(fā)者選項中選擇顯示所有等級的日志,否則默認情況下你看不到D及D以下等級日志的輸出(慘痛的教訓(xùn),浪費了兩天時間排查問題)。
總結(jié)
本文主要介紹了Android AOP的編程思想和應(yīng)用場景,AOP這種編程范式已經(jīng)在服務(wù)端取得了成功,我相信在Android客戶端編程中,也一樣可以有用武之地。通過AspectJ這種工具可以很方便地實現(xiàn)AOP編程,在文中重點介紹了AspectJ的語法和使用方式,并以實例的方式展示了如何通過AspectJ重構(gòu)代碼,降低了耦合度并利于后期維護。最后介紹了我自己踩過的坑,避免重復(fù)入坑。AOP是一種編程思想,理解并運用到工程中將會大有裨益,AspectJ同樣是非常強大的工具,文中只介紹了一些基本用法,還有很多高級特性待探索和實踐。只要腦洞夠大,一定能實現(xiàn)一些意想不到的效果。最后,希望我的文章能給大家?guī)硪恍﹩l(fā),在今后的開發(fā)過程中,可以用AOP來做一些exciting的事情。
參考文章
- http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
- https://blog.egorand.me/going-aspect-oriented-with-aspectj-and-google-analytics/
- http://blog.csdn.net/innost/article/details/49387395