Android中的AOP

在上一篇
使用自定義注解實現MVP中Model和View的注入
中,使用了自定義的方式進行依賴注入這一篇我們將繼續對注解進行深入了解。在日常的開發過程中,我們經常會在同一個地方使用到相同的代碼,以往我們的處理方式是可以將其進行一個封裝,然后在
不同的地方進行調用這樣確實也很方便,但是還有另外的方式,就是自定義注解實現AOP。

需求:在開發過程中有很多頁面需要判斷登錄,實現這樣一個功能,能夠在不同需要實現的地方進行登錄的校驗!

AOP

AOPAspect Oriented Program的首字母縮寫AOP,其意是面向切面編程),其實很多前端的開發可能都沒有聽說過這個,但是對于
后端的小伙伴來說這個是在是太熟悉了,因為很多時候他們就靠這個來進行Log的打印。

那么AOP到底是什么呢?

AOP定義

先看定義:運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想

在解釋AOP之前,首先得說說和面向切面編程相對的另一個編程思想:面向對象編程(OOP。在面向對象的思想中,我們以“一切皆對象”為原則,為不同的對象賦予不同的
功能,在需要使用到的時候,我們就對實例化對象,然后調用其功能,這樣降低了代碼的復雜度,使類可重用。

但是在使用的過程中,會出現這么一種情況,類A和類B,都需要進行實現一個功能(比如:是否登錄的判斷),以往我們的做法很簡單,
將這個登錄判斷的功能寫在一個類中(這里命名為C),然后在各自的引用的地方調用這個類的方法,確實這樣是解決了這個問題,但是
這樣卻使A,B 兩個類與C類之間就會有耦合。有沒有什么辦法,能讓我們在需要的時候,隨意地加入代碼呢?
為了解決這樣的問題就出現了面向切面編程的思想,即是:這種在運行時,動態地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程

AOP和OOP之間的關系

AOP的實際操作是將幾個類之間共有的功能單獨出來,然后在這幾個需要的時候進行切入,改變其本來的運行方式。這樣分析下來,我們可以
得出一個結論,即是:面向切面編程(AOP)其實是面向對象編程(OOP)的一個補充。

加入AspectJ

AspectJ AspectJ實際上是對AOP編程思想的一個實現。

  • 在項目的gradle文件下加入:

     dependencies {
             classpath 'com.android.tools.build:gradle:3.0.0'
             classpath 'org.aspectj:aspectjtools:1.8.9'
             classpath 'org.aspectj:aspectjweaver:1.8.9'
    
             // NOTE: Do not place your application dependencies here; they belong
             // in the individual module build.gradle files
         }
    
  • 在app的gradle文件下加入:

    1. 引入aspectjtools

      import org.aspectj.bridge.IMessage
      import org.aspectj.bridge.MessageHandler
      import org.aspectj.tools.ajc.Main
      
    2. 導入第三方包

      compile 'org.aspectj:aspectjrt:1.8.9'
      
  1. 使用AspectJ編譯器ajc

使用ajc會對所有受 aspect 影響的類進行織入,這樣才能使我們的Aspect

//獲取 log實例
final def log = project.logger
//獲取variants
final def variants = project.android.applicationVariants
variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }

    //編譯時做如下處理
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.8",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}      

至此,我們就將AspectJ的準備工作做好了,那么接下來就是使用了

在Android中使用AOP

先來介紹幾個概念:

  • Pointcut:切入點,就是在程序運行過程中,在何處注入我們想運行的特定代碼。
    注意:這里的何處,并不是真正意義上的具體位置,而是可切入的范圍,比如整個包下面所有類及所有方法,或者某個類下面的所有方法。
  • Joint point:連接點,程序中可能作為代碼注入目標的特定的點,所以此處才是執行注入的具體的位置。
  • Advice: 通知,即是在程序運行過程中,當執行到切點位置時,執行注入到class文件中什么樣的代碼,
    比較常用的類型是beforearoundafter。從字面上面我們就可以看出其意,
    就是在目標方法執行之前,執行之時替代目標方法,執行之后的代碼。
  • Aspect: 切面,其實就是PointcutAdvice的組合,所以如上可以總結為在何處做什么

創建@CheckLogin注解

可能有人會問:為什么是創建注解呢?不能是其的什么類或者對象么?
AOP本來就是為了解決耦合才進行使用的,如果使用其他的,或讓AspectJ與其耦合,那我們使用AOP干什么呢?


@Retention(RetentionPolicy.RUNTIME) //保留到源碼中,同時也保留到class中,最后加載到虛擬機中
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR}) //可以注解在方法或構造上
public @interface CheckLogin {
}

在上次的講解中已經提到元注解@Retention,表示注解的表示方式,這里再回顧一下:

  • SOURCE:只保留在源碼中,不保留在class中,同時也不加載到虛擬機中
  • CLASS:保留在源碼中,同時也保留到class中,但是不加載到虛擬機中
  • RUNTIME:保留到源碼中,同時也保留到class中,最后加載到虛擬機中

@Target 這個注解表示注解的作用范圍,主要有如下:

  • ElementType.FIELD 注解作用于變量
  • ElementType.METHOD 注解作用于方法
  • ElementType.PARAMETER 注解作用于參數
  • ElementType.CONSTRUCTOR 注解作用于構造方法
  • ElementType.LOCAL_VARIABLE 注解作用于局部變量
  • ElementType.PACKAGE 注解作用于包

所以如上的CheckLogin表示將注解可以注入到構造方法和其他方法上,并且保留到源碼中,同時也保留到class中,最后加載到虛擬機中。

創建Aspect類

到此,才是我們這章的重點,就是怎么構建一個Aspect類,這里以CheckLoginAspectJ為例。

@Aspect
public class CheckLoginAspectJ {
    private static final String TAG = "CheckLogin";

    /**
     * 找到處理的切點
     * * *(..)  可以處理CheckLogin這個類所有的方法
     */
    @Pointcut("execution(@com.yw.android.aoptest.aop.CheckLogin  * *(..))")
    public void executionCheckLogin() {

    }

    /**
     * 處理切面
     *
     * @param joinPoint
     * @return
     */
    @Around("executionCheckLogin()")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.i(TAG, "checkLogin: ");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class);
        if (checkLogin != null) {
            Context context = (Context) joinPoint.getThis();
            if (BaseApplication.isLogin) {
                Log.i(TAG, "checkLogin: 登錄成功 ");
                return joinPoint.proceed();
            } else {
                Log.i(TAG, "checkLogin: 請登錄");
                Toast.makeText(context, "請登錄", Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(context, LoginActivity.class);
                context.startActivity(intent);
                return null;
            }
        }
        return joinPoint.proceed();
    }

@Pointcut說明

在上方代碼Pointcut之后緊跟了一個execution的表達式,這個就代表切入點的位置,也就是我們上述的何處

解釋一下execution的用法:

execution僅僅是AOP中pointcut expression表達式中的一種。其他還有如下這幾種:

  • args():用于匹配當前執行的方法傳入的參數為指定類型的執行方法
  • @args():用于匹配當前執行的方法傳入的參數持有指定注解的執行
  • execution():用于匹配方法執行的連接點
  • this():用于匹配當前AOP代理對象類型的執行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配
  • target():用于匹配當前目標對象類型的執行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配
  • @target():用于匹配當前目標對象類型的執行方法,其中目標對象持有指定的注解;
  • within():用于匹配指定類型內的方法執行
  • @within():用于匹配所有持有指定注解類型內的方法;
  • @annotation:用于匹配當前執行方法持有指定注解的方法

這里重點解釋一下execution,因為在我們的日常使用中,execution是最多的。

類型匹配語法

  • *:匹配任何數量字符,即是全部;
  • ..:匹配任何數量字符的重復,如在類型模式中匹配任何數量子包;而在方法參數模式中匹配任何數量參數。
  • +:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。
  • ():表示方法沒有任何參數
  • (..):表示匹配接受任意個參數的方法
//匹配String類型
java.lang.String
//匹配java包下任何子包的String類型
java.*.String
//匹配java包及任何子包下的任何類型
java..*

execution表達式

execution的表達式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

  • modifiers-pattern:修飾符匹配,如publicprivateprotect,可選。
  • ret-type-pattern:返回類型匹配,必填。
  • declaring-type-pattern:聲明類型匹配,可選。
  • name-pattern(param-pattern):
    • name-pattern:方法名匹配,必填
    • param-pattern:方法參數匹配,必填
  • throws-pattern:異常匹配,可選。

至此,我們可以知道,上述中代碼代表的匹配意思了

"execution(@com.yw.android.aoptest.aop.CheckLogin  * *(..))"

返回類型:com.yw.android.aoptest.aop.CheckLogin;
聲明類型: * ,表示任何
方法名: *,任何方法
參數:(..),任意個參數

即是:匹配com.yw.android.aoptest.aop.CheckLogin類下的所有聲明和所以任意參數方法。

@Advice說明

@Around("executionCheckLogin()")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        ...
    }

在上述代碼中我們使用的是@Around,這個也是很常用的。

@Around("executionCheckLogin()")將切面表達式與通知進行綁定,使用我們的代碼注入在使用@CheckLogin的地方生效
,其中參數是上面切面的方法名。

而在方法中參數就是JoinPoint,常用的也就是這個ProceedingJoinPoint

JoinPoint

public interface JoinPoint {
    String toString();         //連接點所在位置的相關信息
    String toShortString();     //連接點所在位置的簡短相關信息
    String toLongString();     //連接點所在位置的全部相關信息
    Object getThis();         //返回AOP代理對象
    Object getTarget();       //返回目標對象
    Object[] getArgs();       //返回被通知方法參數列表
    Signature getSignature();  //返回當前連接點簽名
    SourceLocation getSourceLocation();//返回連接點方法所在類文件中的位置
    String getKind();        //連接點類型
    StaticPart getStaticPart(); //返回連接點靜態部分
}

ProceedingJoinPoint

ProceedingJoinPoint繼承了JoinPoint

public interface ProceedingJoinPoint extends JoinPoint {
    public Object proceed() throws Throwable;
    public Object proceed(Object[] args) throws Throwable;
}

使用proceed()方法來執行目標方法,即是被@CheckLogin注解的方法,我們再來看看我們的方法

@Around("executionCheckLogin()")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.i(TAG, "checkLogin: ");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class);
        if (checkLogin != null) {
            Context context = (Context) joinPoint.getThis();
            if (BaseApplication.isLogin) {
                Log.i(TAG, "checkLogin: 登錄成功 ");
                return joinPoint.proceed();
            } else {
                Log.i(TAG, "checkLogin: 請登錄");
                Toast.makeText(context, "請登錄", Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(context, LoginActivity.class);
                context.startActivity(intent);
                return null;
            }
        }
        return joinPoint.proceed();
    }

  1. 先獲取一個方法前面對象MethodSignature,這個對象有兩個方法:
public interface MethodSignature extends CodeSignature {
    Class getReturnType();      /* name is consistent with reflection API */
    Method getMethod();
}

一個是獲取目標方法的返回類型,一個是目標方法的Methond對象。
然后通過:

signature.getMethod().getAnnotation(CheckLogin.class);

就可以獲取目標方法的注解,如果注解實例不為空,說明加了CheckLogin注解。

Context context = (Context) joinPoint.getThis();

通過上述方法,可以獲取目標方法所在類的對象,但是這里強轉成了Context,也就是說,改注解只能在有上下文的類里使用。
然后通過登錄的標志進行判斷,是讓目標方法繼續執行,還是跳轉至登錄。

簡單測試

private Button btnAop;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    btnAop = (Button) findViewById(R.id.btn_aop);
    btnAop.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
             onAop();
        }
    });
}

@CheckLogin
public void onAop(){
    Log.d("tag","執行方法參數");
}
  1. 設置登錄標志為未登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 請登錄

檢測出未登錄,跳轉到了登錄界面

  1. 設置登錄標志為已登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 登錄成功
D/tag: 執行方法參數

檢測出已登錄,執行目標方法。

總結

AOP的使用不光在檢測登錄,還有其他的一些用處:

  • 打印日志,在需要打印日志的地方加上這樣的方式,就可以打印日志,是不是比寫一個打印方法簡單多了
  • 緩存,假設目標方法是個數據請求,那么是不是可以在目標方法執行之后,進行緩存
  • 數據校驗,我們的代碼中很多地方都會去校驗數據,那么自定義一個AOP,然后傳入你需要注解的對象進行校驗。

這樣的方式應該還有很多,只是現在還沒有用到,希望大家可以多多提出自己的想法。

查看項目,請戳這里

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

推薦閱讀更多精彩內容