一、前言
大家都知道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的这种思想在安卓中应用目前很广泛,比如日志打印,权限申请等,另外目前项目中使用到的神策全埋点也是通过这种方式来实现的。