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的宣告週期的切入點,但是會遇到兩個問題:
-
如果有一個父類BaseActivity,然後子類Activity也重寫了onCreate方法,那麼這兩個類的onCrate方法都會織入AOP程式碼,所以會有兩次,根據上面日誌可以得出結論,即AspectJ很難直接攔截子類並且不影響父類的某個方法。
-
如果子類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
另外如果想做一個捕捉事件分發的事件,但是實現不了,原因在於:
- 系統的view最終是不打包進apk的,所以是切入不了的
- 針對於自定義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的這種思想在安卓中應用目前很廣泛,比如日誌列印,許可權申請等,另外目前專案中使用到的神策全埋點也是通過這種方式來實現的。