1. 程式人生 > >AOP淺析以及Android對AOP的應用

AOP淺析以及Android對AOP的應用

一、前言

大家都知道OOP,即Object-Oriented Programming,面向物件程式設計。本篇我們要講的是AOP,即 Aspect-Oriented Programming,面向切面(方面)程式設計。平常我們開發都是用OOP的程式設計思想,這種思想的精髓是把問題模組化,每個模組專注處理自己的事情,但是在現實世界中,並不是所有問題都能完美的劃分到模組中。比如日誌輸出,這些可能是每個模組都是需要的功能,所以在OOP的世界中,有些功能是橫跨並且嵌入在多個模組中的,那AOP的目標就是能把這些功能集中起來,放到一個統一的地方來控制和管理。所以,總結的來說,OOP是把問題劃分到單個模組,那麼AOP就是把涉及到眾多模組的某一類問題進行統一管理。

二、AspectJ介紹

2.1 AspectJ簡介

OOP最突出的開發語言是Java,那麼針對AOP,一些先行者也開發了一套語言來支援AOP,目前最火的就是AspectJ了,AspectJ就是Java的Aspect,Java的AOP,它是一種幾乎和Java完全一樣的語言,而且完全相容Java,除了AspecJ特殊的語言外,AspectJ還支援原生的Java,只需要加上對應的AspectJ註解就好,所以,使用AspectJ有兩種方法:

  • 完全使用AspectJ的語言,跟Java語言很類似,也能在AspectJ中任意呼叫Java的類庫,只是AspectJ有一些自己的關鍵字,但是由於檔案是.aj的,所以還需要IDE的外掛才能進行語法檢查、編譯。
  • 使用純Java語言開發,然後使用AspectJ註解,簡稱@AspectJ

不管使用哪種方法,最終都需要AspectJ的編譯工具AJC來編譯,但是通常我們選擇第二種,因為第一種無法擺脫ajc的支援,而且跟java語法和檔案都不同,難以統一編碼規範,而且還需要較多的額外學習成本,所以更多的還是採用相容java語法的用註解定義切面的形式。

AspectJ現在託管在Elicpse專案中 AspectJ地址

官網提供了aspectJ.jar的下載連結,下載完成後可以直接安裝,安裝後可以看到如下目錄

xueshanshandeMacBook-Pro:aspectj1.9 xueshanshan$ tree bin/ lib/

bin/
├── aj
├── aj5
├── ajbrowser
├── ajc
└── ajdoc
lib/
├── aspectjrt.jar
├── aspectjtools.jar
├── aspectjweaver.jar
└── org.aspectj.matcher.jar
  • aspectjrt.jar包主要提供執行時的一些註解,靜態方法等等,通常我們使用aspectJ的時候都需要使用這個包
  • aspectjtools.jar主要是提供 ajc編譯器 ,可以在編譯期將java檔案或者class檔案將aspect檔案定義的切面織入到業務程式碼中,通常這個東西會被封裝金各種IDE外掛或者自動化外掛中。
  • aspectjweaver.jar包主要提供了一個java agent用於在類載入期間織入切面,並且提供了對切面語法的相關處理等基礎方法,供ajc使用或者供第三方開發使用,這個包一般我們不需要顯示引用。

所以我們瞭解到,aspectJ有幾種織入方式:

1. 編譯時織入

利用ajc編譯器代替javac編譯器,直接將原始檔編譯成class檔案並將切面織入進程式碼。

2. 編譯後織入

利用ajc編譯器向javac編譯後的class檔案或jar檔案織入切面程式碼

3. 載入時織入

不使用ajc編譯器,利用aspectjweaver.jar工具,使用java agent代理類在類載入期將切面織入程式碼。

2.2 AspectJ語法

1、Join Point

Join Point是指程式執行時的一些執行點。
一個函式的呼叫可以是一個JPoint,比如Log.e()這個函式,e的執行可以是一個JPoint,而呼叫e的函式也可以認為是一個JPoint,設定一個變數,或者讀取一個變數都可以是一個JPoint。
理論上說,一個程式中很多地方都可以被看作是JPoint,但在Aspect中,只有下面表格列出的才會被認為是JPoints:

2. Pointcut(切入點)

上面我們介紹了JPoint,但是在一個類中,肯定會有很多滿足條件的JPoints,我們肯定是隻想關注自己想要的,那麼Pointcut就提供了這個功能,他的目標就是提供一種方法使得開發者能夠選擇出自己感興趣的Join Point。

PointCuts中最常用的選擇條件有:

具體Signature參考

相關萬用字元

上面幾種Pointcuts語法,可以使用‘&&’、‘||’,‘!’等操作符,另外還有一些其他的具體語法進行過濾,具體有:

下面是關於Advice的使用,Advice具體可以理解為定義操作行為執行在Pointcuts的具體位置(前、後、包裹),具體操作符如下:

二、Android使用AspectJ

AOP的用處非常廣,從Spring到Android,各個地方都有使用,特別是在後端,Spring中已經使用的非常方便了,而且功能非常強大,但是在Android中,AspectJ的實現是有所閹割的版本,並不是所有功能都支援,但對於一般的客戶端開發來說,已經完全足夠用了。

在Android中整合AspectJ,主要思想就是hoot Apk打包過程,使用AspectJ提供的工具AJC來編譯.class檔案。

一般來說,如果自己接入AspectJ的話,按照下面的步驟即可

1、在項根目錄的build.gradle下引入aspectjtools外掛

buildscript {
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.10'
    }
}

2、在app的module目錄下的build.gradle中引入

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    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;
            }
        }
    }
}

dependencies {
   ...
  compile 'org.aspectj:aspectjrt:1.8.10'
}

可以看到在Android上整合AspectJ實際上是比較複雜的,不是一行配置就能解決的,但是究其原理其實就是把java編譯器替換為AJC。目前Github上已有多個可以應用在Android Studio上的外掛,通過這些外掛可以簡單地在Android上整合AspectJ,其實他們也就是把上面的程式碼幫你配置了而已,可以瞭解一下

Hugo
AspectJX
gradle-android-aspectj-plugin
T-MVP

下面是在Android中使用AspectJ的栗子,監聽Activity的onCreate方法,並且列印日誌:

/**
 * @author xueshanshan
 * @date 2018/12/1
 */
@Aspect
public class ActivityAspect {
    private static final String TAG = ActivityAspect.class.getSimpleName();

    //@Pointcut("execution(* *..BaseActivity.onCreate(..))") 跟下面一句話效果是一樣的
    @Pointcut("execution(* *..AppCompatActivity+.onCreate(..))")
    public void logForActivity() {}

    @Before("logForActivity()")
    public void log(JoinPoint joinPoint) {
        Log.e(TAG, "getKind = " + joinPoint.getKind());
        int length = joinPoint.getArgs().length;
        for (int i = 0; i < length; i++) {
            Object o = joinPoint.getArgs()[i];
            if (o != null) {
                Log.e(TAG, "args[" + i + "] =" + o.toString());
            }
        }
        Log.e(TAG, "getSignature = " + joinPoint.getSignature().toString());
        Log.e(TAG, "getSourceLocation = " + joinPoint.getSourceLocation().toString());
        Log.e(TAG, "getStaticPart = " + joinPoint.getStaticPart().toString());
        Log.e(TAG, "getTarget = " + joinPoint.getTarget().toString());
        Log.e(TAG, "getThis = " + joinPoint.getThis().toString());
        Log.e(TAG, "toString = " + joinPoint.toString());
    }
}

上述程式碼執行後列印的日誌為:

12-03 11:24:57.968 10885-10885/com.star.testapplication E/ActivityAspect: after OnCreate
    getKind = method-execution
    getSignature = void com.star.testapplication.activitys.BaseActivity.onCreate(Bundle)
    getSourceLocation = BaseActivity.java:13
    getStaticPart = execution(void com.star.testapplication.activitys.BaseActivity.onCreate(Bundle))
    getTarget = com.star.testapplication.activitys.MainActivity@cdb6e41
    getThis = com.star.testapplication.activitys.MainActivity@cdb6e41
    toString = execution(void com.star.testapplication.activitys.BaseActivity.onCreate(Bundle))
    
12-03 11:24:58.013 10885-10885/com.star.testapplication E/ActivityAspect: after OnCreate
    getKind = method-execution
    getSignature = void com.star.testapplication.activitys.MainActivity.onCreate(Bundle)
    getSourceLocation = MainActivity.java:51
    getStaticPart = execution(void com.star.testapplication.activitys.MainActivity.onCreate(Bundle))
    getTarget = com.star.testapplication.activitys.MainActivity@cdb6e41
    getThis = com.star.testapplication.activitys.MainActivity@cdb6e41
    toString = execution(void com.star.testapplication.activitys.MainActivity.onCreate(Bundle))
    

如果想要模仿Application.registerActivityLifecycleCallbacks獲取每個Activity生命週期的回撥,那麼我們需要獲取每個Activity的宣告週期的切入點,但是會遇到兩個問題:

  1. 如果有一個父類BaseActivity,然後子類Activity也重寫了onCreate方法,那麼這兩個類的onCrate方法都會織入AOP程式碼,所以會有兩次,根據上面日誌可以得出結論,即AspectJ很難直接攔截子類並且不影響父類的某個方法。

  2. 如果子類Activity沒有重寫某個方法,比如onDestroy方法,那麼該Activity的onDestroy方法就無法作為一個切入點,即AspectJ無法攔截子類未重寫父類的方法。

所以如果自己想做類似於Application.ActivityLifecycleCallbacks這個功能,有一種簡單的做法就是:有一個BaseActivity,重寫所有生命週期方法,定義一個註解,在這些生命週期方法上面加上註解,然後定義Pointcut的時候切入這個註解,下面是我寫的栗子:

/**
 * 註解類 ActivityLifeCycle
 * @author xueshanshan
 * @date 2018/12/3
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ActivityLifeCycle {
}
/** 
 * 常量類 LifeCycleMethod
 * @author xueshanshan
 * @date 2018/12/3
 */
public class LifeCycleMethod {
    public static final String LIFECYCLE_METHOD_ON_CREATE = "onCreate";
    public static final String LIFECYCLE_METHOD_ON_RESUME = "onResume";
    public static final String LIFECYCLE_METHOD_ON_PAUSE = "onPause";
    public static final String LIFECYCLE_METHOD_ON_STOP = "onStop";
    public static final String LIFECYCLE_METHOD_ON_DESTROY = "onDestroy";

    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.METHOD)
    @StringDef({LIFECYCLE_METHOD_ON_CREATE,
            LIFECYCLE_METHOD_ON_RESUME,
            LIFECYCLE_METHOD_ON_PAUSE,
            LIFECYCLE_METHOD_ON_STOP,
            LIFECYCLE_METHOD_ON_DESTROY})
    public @interface MethodName {

    }
}
//BaseActivity
public abstract class BaseActivity extends AppCompatActivity {
    protected Context mContext;

    @ActivityLifeCycle()
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;
    }

    @ActivityLifeCycle()
    @Override
    protected void onResume() {
        super.onResume();
    }

    @ActivityLifeCycle()
    @Override
    protected void onPause() {
        super.onPause();
    }

    @ActivityLifeCycle()
    @Override
    protected void onStop() {
        super.onStop();
    }

    @ActivityLifeCycle()
    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}
/**
 * 切面類
 * @author xueshanshan
 * @date 2018/12/3
 */
@Aspect
public class ActivityAnnotationAspect {
    private static final String TAG = ActivityAnnotationAspect.class.getSimpleName();
    private boolean foreground = false;
    private boolean paused = true;
    private Handler handler = new Handler();
    private static final long CHECK_DELAY = 500L;
    private Runnable pauseRunnable = new Runnable() {
        @Override
        public void run() {
            if (!paused) {
                return;
            }
            if (foreground) {
                foreground = false;
                notifyStatusChanged(false);
            }
        }
    };

    @Pointcut("execution(@com.huli.hulitestapplication.annotations.ActivityLifeCycle * *(..)) && @annotation(activityLifeCycle)")
    public void activitylifeCycle(ActivityLifeCycle activityLifeCycle) {}


    @After("activitylifeCycle(activityLifeCycle)")
    public void afterActivitylifeCycle(JoinPoint joinPoint, ActivityLifeCycle activityLifeCycle) {
        String methodName = joinPoint.getSignature().getName();
        Class<?> aClass = joinPoint.getThis().getClass();
        String className = aClass.getSimpleName();
        Log.e(TAG, "after " + className + "->" + methodName);
        switch (methodName) {
            case LifeCycleMethod.LIFECYCLE_METHOD_ON_CREATE:
                break;
            case LifeCycleMethod.LIFECYCLE_METHOD_ON_RESUME:
                if (!foreground) {
                    notifyStatusChanged(true);
                }
                foreground = true;
                paused = false;
                handler.removeCallbacks(pauseRunnable);
                break;
            case LifeCycleMethod.LIFECYCLE_METHOD_ON_PAUSE:
                paused = true;
                handler.removeCallbacks(pauseRunnable);
                handler.postDelayed(pauseRunnable, CHECK_DELAY);
                break;
            case LifeCycleMethod.LIFECYCLE_METHOD_ON_STOP:
                break;
            case LifeCycleMethod.LIFECYCLE_METHOD_ON_DESTROY:
                break;
            default:
                break;
        }
    }

    private void notifyStatusChanged(boolean foreground) {
        if (foreground) {
            Log.d(TAG, "app become foreground");
        } else {
            Log.d(TAG, "app become background");
        }
    }
}

下面是列印的日誌:

12-03 18:28:48.385 18001-18001/? E/ActivityAnnotationAspect: after MainActivity->onCreate
12-03 18:28:48.435 18001-18001/? E/ActivityAnnotationAspect: after MainActivity->onResume
12-03 18:28:48.435 18001-18001/? D/ActivityAnnotationAspect: app become foreground
12-03 18:28:52.665 18001-18001/? E/ActivityAnnotationAspect: after MainActivity->onPause
12-03 18:28:52.685 18001-18001/? E/ActivityAnnotationAspect: after TestActivity->onCreate
12-03 18:28:52.695 18001-18001/? E/ActivityAnnotationAspect: after TestActivity->onResume
12-03 18:28:53.185 18001-18001/? E/ActivityAnnotationAspect: after MainActivity->onStop
12-03 18:28:56.125 18001-18001/? E/ActivityAnnotationAspect: after TestActivity->onPause
12-03 18:28:56.465 18001-18001/? E/ActivityAnnotationAspect: after TestActivity->onStop
12-03 18:28:56.625 18001-18001/? D/ActivityAnnotationAspect: app become background
12-03 18:29:03.535 18001-18001/? E/ActivityAnnotationAspect: after TestActivity->onResume
12-03 18:29:03.535 18001-18001/? D/ActivityAnnotationAspect: app become foreground

另外如果想做一個捕捉事件分發的事件,但是實現不了,原因在於:

  1. 系統的view最終是不打包進apk的,所以是切入不了的
  2. 針對於自定義View,如果不重寫相應的事件分發方法也是切入不了的,當然也可以通過上面註解的方式來實現,但是這種方式也只能針對於自定義View

AOP還可以實現點選事件等的埋點:

 @After("execution(* android.view.View.OnClickListener.onClick(android.view.View))")
    public void callOnClick(JoinPoint joinPoint) {
        Log.d(TAG, "callOnClick");
    }

上面對view的onClick事件進行的切入,其他事件類似,通過這種就可以做埋點操作,非常方便,如果不這樣埋點就會分佈在各個類中,使程式碼看起來非常臃腫

AOP這種方式在android中是在編譯期就把程式碼織入了,我們可以在build目錄下找到答案:

三、總結

雖然在有些時候,使用這種方式可能會面臨一些其他問題,比如view事件分發切入不了,但是還有很多未開發的場景需要去思考和探索,而且這是一種思想,如果某些時候你只需要關注問題的一個方面,並且不想去大量的改動程式碼,那AOP切入的思想就很適合,但關鍵是去找一種合適的切入方式以及合適的應用場景,使用註解目前來說是一種比較好的切入方式。

AOP的這種思想在安卓中應用目前很廣泛,比如日誌列印,許可權申請等,另外目前專案中使用到的神策全埋點也是通過這種方式來實現的。

github專案地址