AOP浅析以及Android对AOP的应用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_33666539/article/details/84927150

一、前言

大家都知道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项目地址

猜你喜欢

转载自blog.csdn.net/qq_33666539/article/details/84927150
AOP
今日推荐