Android开发经验谈Android技术知识Android知识

Android中的AOP

2017-11-10  本文已影响166人  奇葩AnJoiner

在上一篇
使用自定义注解实现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编程思想的一个实现。

  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

先来介绍几个概念:

创建@CheckLogin注解

可能有人会问:为什么是创建注解呢?不能是其的什么类或者对象么?
AOP本来就是为了解决耦合才进行使用的,如果使用其他的,或让AspectJ与其耦合,那我们使用AOP干什么呢?


@Retention(RetentionPolicy.RUNTIME) //保留到源码中,同时也保留到class中,最后加载到虚拟机中
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR}) //可以注解在方法或构造上
public @interface CheckLogin {
}

在上次的讲解中已经提到元注解@Retention,表示注解的表示方式,这里再回顾一下:

@Target 这个注解表示注解的作用范围,主要有如下:

所以如上的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表达式中的一种。其他还有如下这几种:

这里重点解释一下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?)

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

"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的使用不光在检测登录,还有其他的一些用处:

这样的方式应该还有很多,只是现在还没有用到,希望大家可以多多提出自己的想法。

查看项目,请戳这里

上一篇下一篇

猜你喜欢

热点阅读