AOP

13.3.1 AOP

1. 簡介

OOP(Object Oriented Programming)面向對象編程。在OOP的世界中,問題或者功能都被劃分到一個一個的模塊里邊。每個模塊專心干自己的事情,模塊之間通過設計好的接口交互。如下圖就是Android Framework中的模塊:

Android Framework中的模塊

OOP的精髓是把++功能或問題模塊化,每個模塊處理自己的家務事++。但在現實世界中,并不是所有問題都能完美得劃分到模塊中。舉個最簡單而又常見的例子:現在想為每個模塊加上日志功能,要求模塊運行時候能輸出日志。在不知道AOP的情況下,一般的處理都是:先設計一個日志輸出模塊,這個模塊提供日志輸出API,比如Android中的Log類。然后,其他模塊需要輸出日志的時候調用Log類的幾個方法。這種方式功能是得到了滿足,但是好像沒有Oriented的感覺了。是的,隨意加日志輸出功能,使得其他模塊的代碼和日志模塊耦合非常緊密。而且,將來要是日志模塊修改了API,則使用它們的地方都得改。

AOP(Aspect Oriented Programming)面向切向編程。AOP的目標==是把這些分散在各個模塊中的功能集中起來,放到一個統一的地方來控制和管理==。如果說,OOP如果是把問題劃分到單個模塊的話,那么AOP就是把涉及到眾多模塊的某一類問題進行統一管理。比如我們可以設計兩個Aspects,一個是管理某個軟件中所有模塊的日志輸出的功能,另外一個是管理該軟件中一些特殊函數調用的權限檢查。

2. AspectJ

AOP是一種思想,就好像OOP中的Java一樣,一些先行者也開發了一套語言來支持AOP。目前用得比較火的就是AspectJ了,它是一種幾乎和Java完全一樣的==語言==,而且完全兼容Java。當然,除了使用AspectJ特殊的語言外,AspectJ還支持原生的Java,只要加上對應的AspectJ注解就好。所以,使用AspectJ有兩種方法:

  • 完全使用AspectJ的語言。這語言一點也不難,和Java幾乎一樣,也能在AspectJ中調用Java的任何類庫。AspectJ只是多了一些關鍵詞罷了。
  • 或者使用純Java語言開發,然后使用AspectJ注解,簡稱@AspectJ。(推薦)

不論哪種方法,最后都需要AspectJ的編譯工具ajc來編譯。由于AspectJ實際上脫胎于Java,所以ajc工具也能編譯java源碼。接下來介紹下幾個概念。

(1) Join Points

Join Points(以后簡稱==JPoints==)是AspectJ中最關鍵的一個概念,JPoints就是==程序運行時的一些執行點==。那么,一個程序中,哪些執行點是JPoints呢?比如:

  • 一個函數的調用可以是一個JPoint。比如Log.e()這個函數。e的執行可以是一個JPoint,而調用e的函數也可以認為是一個JPoint。
  • 設置一個變量,或者讀取一個變量,也可以是一個JPoint。比如Demo類中有一個disable的boolean變量。設置它的地方或者讀取它的地方都可以看做是JPoints。
  • for循環可以看做是JPoint。

理論上說,一個程序中很多地方都可以被看做是JPoint,但是AspectJ中,只有如表1所示的幾種執行點被認為是JPoints:

Join Points 說明 示例
method call 函數調用 比如調用Log.e(),這是一處JPoint
method execution 函數執行 比如Log.e()的執行內部,是一處JPoint
constructor call 構造函數調用 和method call類似
constructor execution 構造函數執行 和method execution類似
field get 獲取變量 比如讀取DemoActivity.debug成員
field set 設置變量 比如設置DemoActivity.debug成員
static initialization 類初始化 比如類的static{}
handler 異常處理 比如try catch(xxx)中,對應catch內的執行
(2) Pointcuts

一個程序會有很多的JPoints,即使是同一個函數(比如testMethod這個函數),還分為==call類型和execution類型==的JPoint。顯然,不是所有的JPoint,也不是所有類型的JPoint都是我們關注的。比如我們只要求在Activity的幾個生命周期函數中打印日志,只有這幾個生命周期函數才是我們業務需要的JPoint,而其他的什么JPoint我不需要關注。

怎么從一堆一堆的JPoints中選擇自己想要的JPoints呢?恩,這就是Pointcuts的功能。一句話,==Pointcuts的目標是提供一種方法使得開發者能夠選擇自己感興趣的JoinPoints==。

(i) 一個Pointcuts例子

@Pointcut("call(public * *.println(..)) && !within(TestAspect)")

  • call(public * *.println(..))是一種選擇條件。call表示我們選擇的Joinpoint類型為call類型。
  • public * *.println(..):這小行代碼使用了通配符。由于我們這里選擇的JoinPoint類型為call類型,它對應的目標JPoint一定是某個函數。所以我們要找到這個/些函數。public 表示目標JPoint的訪問類型(public/private/protect)。第一個*表示返回值的類型是任意類型。第二個*用來指明包名。此處不限定包名。緊接其后的println是函數名。這表明我們選擇的函數是任何包中定義的名字叫println的函數。當然,唯一確定一個函數除了包名外,還有它的參數。在(..)中,就指明了目標函數的參數應該是什么樣子的。比如這里使用了通配符..,代表任意個數的參數,任意類型的參數。
  • call后面的&&:AspectJ可以把幾個條件組合起來,目前支持 &&,||,以及!這三個條件。
  • !within(TestAspectJ):前面的!表示不滿足某個條件。within是另外一種類型選擇方法,特別注意,這種類型和前面講到的joinpoint的那幾種類型不同。within的類型是數據類型,而joinpoint的類型更像是動態的,執行時的類型。

上例中的pointcut合起來就是:

  1. 選擇那些調用println(而且不考慮println函數的參數是什么)的Joinpoint。
  2. 調用者的類型不要是TestAspect的。
(ii) 直接針對JoinPoint的選擇

pointcuts中最常用的選擇條件和Joinpoint的類型密切相關:

不同類型的JPoint對應的pointcuts查詢方法

一個==Method Signature==的完整表達式為:@注解 訪問權限 返回值類型 包名.函數名(參數)

  • @注解和訪問權限(public/private/protect,以及static/final)屬于可選項。如果不設置它們那么public,private,protect及static、final的函數都會進行搜索。
  • 返回值類型就是普通的函數的返回值類型。如果不限定類型的話,就用*通配符表示
  • 包名.函數名用于查找匹配的函數。可以使用通配符,包括*和..以及+號。其中*號用于匹配除.號之外的任意字符,而..則表示任意子package,+號表示子類。

java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
Test*:可以表示TestBase,也可以表示TestDervied

java..*:表示java任意子類
java..*Model+:表示Java任意package中名字以Model結尾的子類,比如TabelModel,TreeModel等

  • 最后來看函數的參數。參數匹配比較簡單,主要是參數類型。

(int, char):表示參數只有兩個,并且第一個參數類型是int,第二個參數類型是char
(String, ..):表示至少有一個參數。并且第一個參數類型是String,后面參數類型不限。在參數匹配中,
..代表任意參數個數和類型
(Object ...):表示不定個數的參數,且類型都是Object,這里的...不是通配符,而是Java中代表不定參數的意思

(iii) 間接針對JPoint的選擇

除了根據前面提到的Signature信息來匹配JPoint外,AspectJ還提供其他一些選擇方法來選擇JPoint。比如某個類中的所有JPoint,每一個函數執行流程中所包含的JPoint。

下表列出了一些常用的非JPoint選擇方法:

關鍵詞 說明 示例
within(TypePattern) TypePattern標示package或者類,可以使用通配符 表示某個Package或者類中的所有JPoint。比如within(Test):Test類中(包括內部類)所有JPoint。圖2所示的例子就是用這個方法。
this(Type) JPoint的this對象是Type類型。(其實就是判斷Type是不是某種類型,即是否滿足instanceof Type的條件) JPoint是代碼段(不論是函數,異常處理,static block),從語法上說,它都屬于一個類。如果這個類的類型是Type標示的類型,則和它相關的JPoint將全部被選中。圖2示例的testMethod是TestDerived類。所以this(TestDerived)將會選中這個testMethod JPoint
target(Type) JPoint的target對象是Type類型 和this相對的是target。不過target一般用在call的情況。call一個函數,這個函數可能定義在其他類。比如testMethod是TestDerived類定義的。那么target(TestDerived)就會搜索到調用testMethod的地方。但是不包括testMethod的execution JPoint
args(TypeSignature) 用來對JPoint的參數進行條件搜索的 比如args(int,..),表示第一個參數是int,后面參數個數和類型不限的JPoint。
(3) advice

現在,我們知道如何通過pointcuts來選擇合適的JPoint。那么,下一步工作就是選擇這些JPoint后,需要干一些事情的。比如前面例子中的輸出都有before,after之類的。這其實JPoint在執行前,執行后,都執行了一些我們設置的代碼。在AspectJ中,這段代碼叫advice。簡單點說,advice就是一種Hook。

關鍵詞 說明
before() 表示在JPoint執行之前,需要干的事情
after() 表示JPoint自己執行完了后,需要干的事情
返回值類型 around() ==替代了原JPoint==,如果要執行原JPoint的話,需要調用proceed
(4) 例子
(1) 示例一
@Aspect   //必須使用@AspectJ標注
public class DemoAspect {  
    static final String TAG = "DemoAspect";  
/* 
@Pointcut:定義一個pointcut,這個注解是針對一個函數的,比如此處的logForActivity() 
其實它代表了這個pointcut的名字。如果是帶參數的pointcut,則把參數類型和名字放到 
代表pointcut名字的logForActivity中,然后在@Pointcut注解中使用參數名。 
*/  
@Pointcut("execution(* com.androidaop.demo.AopDemoActivity.onCreate(..)) ||"  
        +"execution(* com.androidaop.demo.AopDemoActivity.onStart(..))")  
public void logForActivity(){};  //注意,這個函數必須要有實現,否則Java編譯器會報錯  
   
/* 
@Before:這就是Before的advice。Before后面跟的是pointcut名字,然后其代碼塊由一個函數來實現。比如此處的log。 
*/  
@Before("logForActivity()")  
public void log(JoinPoint joinPoint){  
    //對于使用Annotation的AspectJ而言,JoinPoint就不能直接在代碼里得到多了,而需要通過  
    //參數傳遞進來。  
    Log.e(TAG, joinPoint.toShortString());  
    }  
}  
(2) 示例二
//第一個@Target表示這個注解只能給函數使用  
//第二個@Retention表示注解內容需要包含的Class字節碼里,屬于運行時需要的。  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface SecurityCheckAnnotation {//@interface用于定義一個注解。  
    publicString declaredPermission();  //declarePermssion是一個函數,其實代表了注解里的參數  
}  

怎么使用注解呢?接著看代碼:

//為checkPhoneState使用SecurityCheckAnnotation注解,并指明調用該函數的人需要聲明的權限  
@SecurityCheckAnnotation(declaredPermission="android.permission.READ_PHONE_STATE")  
private void checkPhoneState(){  
    //如果不使用AOP,就得自己來檢查權限  
    if(checkPermission("android.permission.READ_PHONE_STATE") ==false){  
        Log.e(TAG,"have no permission to read phone state");  
        return;  
    }  
    Log.e(TAG,"Read Phone State succeed");  
    return;  
}  

我們來看看如何在AspectJ中,充分利用這注解信息來幫助我們檢查權限。

/* 
來看這個Pointcut,首先,它在選擇Jpoint的時候,把@SecurityCheckAnnotation使用上了,
這表明所有那些public的,并且攜帶有這個注解的API都是目標JPoint。如果是帶參數的pointcut,
則把參數類型和名字放到代表pointcut名字的checkPermssion中,然后在@Pointcut注解中使用參數名。
接著,由于我們希望在函數中獲取注解的信息,所以這里的poincut函數有一個參數,參數類型是 
SecurityCheckAnnotation,參數名為ann。
這個參數我們需要在后面的advice里用上,所以pointcut還使用了@annotation(ann)這種方法來告訴 
AspectJ,這個ann是一個注解 
*/ 
@Pointcut("execution(@SecurityCheckAnnotation public * *..*.*(..)) && @annotation(ann)")  
public void checkPermssion(SecurityCheckAnnotation ann){};  
   
/* 
接下來是advice,advice的真正功能由check函數來實現,這個check函數第二個參數就是我們想要 
的注解。在實際運行過程中,AspectJ會把這個信息從JPoint中提出出來并傳遞給check函數。 
*/  
@Before("checkPermssion(securityCheckAnnotation)")  
public void check(JoinPoint joinPoint,SecurityCheckAnnotation securityCheckAnnotation){  
    //從注解信息中獲取聲明的權限。  
    String neededPermission = securityCheckAnnotation.declaredPermission();  
    Log.e(TAG, joinPoint.toShortString());  
    Log.e(TAG, "\t needed permission is " + neededPermission);  
    return;  
}  
(3) 示例三
@Aspect
public class FragmentAspectj {
    private final static String TAG = FragmentAspectj.class.getCanonicalName();

    @Around("execution(* android.support.v4.app.Fragment.onCreateView(..))")
    public Object fragmentOnCreateViewMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        return trackFragmentView(joinPoint);
    }

    @Around("execution(* android.app.Fragment.onCreateView(..))")
    public Object fragmentOnCreateViewMethod2(ProceedingJoinPoint joinPoint) throws Throwable {
        return trackFragmentView(joinPoint);
    }

    private Object trackFragmentView(final ProceedingJoinPoint joinPoint) throws Throwable {
        // 被注解的方法在這一行代碼被執行
        Object result = joinPoint.proceed();
        AopUtil.sendTrackEventToSDK3(joinPoint, "trackFragmentView", result);
        return result;
    }

    @After("execution(* android.support.v4.app.Fragment.onHiddenChanged(boolean))")
    public void onHiddenChangedMethod(JoinPoint joinPoint) throws Throwable {
        AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentHiddenChangedMethod");
    }

    @After("execution(* android.support.v4.app.Fragment.setUserVisibleHint(boolean))")
    public void setUserVisibleHintMethod(JoinPoint joinPoint) throws Throwable {
        AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentSetUserVisibleHintMethod");
    }

    @After("execution(* android.support.v4.app.Fragment.onResume())")
    public void onResumeMethod(JoinPoint joinPoint) throws Throwable {
        AopUtil.sendTrackEventToSDK(joinPoint, "onFragmentOnResumeMethod");
    }
}

參考文獻

深入理解Android之AOP

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容

  • 里烈:2018-01-30 目標:女兒能夠學會學習,喜歡學習。順利通過國考 好種子: 1、懷著感恩,高興愉悅的心情...
    里喻棋閱讀 97評論 0 0
  • 從不知道自己不知道~知道自己不知道~不知道自己知道,是一個認知的歷程。我們大多數人都在第一或第二階段。 曾經很苦惱...
    milk76閱讀 294評論 2 1
  • 如果有人要我推薦種植一種美麗的花在庭院、在陽臺、在花園小徑邊。這種植物能開出美麗的花、一年四季枝葉濃綠、不用怎么打...
    魚兒水中游閱讀 1,469評論 0 0