Android切面编程(AOP)详解

文章目录

  • 什么是AOP
  • AOP使用场景
  • AspectJ
    • AspectJ基本概念
    • AspectJ的使用
      • 引入AspectJ
      • 创建切面AspectJ
      • 添加注解

什么是AOP

AOP是Aspect Oriented Programming的缩写,即『面向切面编程』。它和我们平时接触到的OOP都是编程的不同思想,OOP,即『面向对象编程』,它提倡的是将功能模块化,对象化,而AOP的思想,则不太一样,它提倡的是针对同一类问题的统一处理,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

AOP使用场景

  • 通常App业务中经常需要判定登录状态,如果十几处甚至几十处需要做判定,那工作量可想而知;
  • 在调试函数调用耗时时,通常需要在函数开始和结束分别打印时间戳,如果业务量小,不需要很多的打印点还好, 当业务变得复杂起来,几十甚至几百个地方需要加log,这将很耗时间,并且插入很多不相关代码;
  • Android app中很多API调用前需要动态请求权限,如果可以统一处理将是一件很愉快的事情…
  • App中通常需要涉及多线程的切换,如果仅仅通过一个注解便可达到线程切换的效果,那将是很酷的事情…
    举例说明下AOP的好处吧, 就以一个方法性能打印场景为例
    通常做法:
void methodCall(){
long start = System.currentTimeMillis();
 Logger.d(TAG, "onAttachContentView, start = "+start);
 //doSomething
 Logger.d(TAG, "onAttachContentView, end coast  = "+(System.currentTimeMillis()-start));
}

AOP做法:

@TimeLog
void methodCall(){
 //doSomething
}

只需要在methodCall()上面添加@TimeLog注解就可以打印方法调用耗时,当然你也可以在解析注解时加入方法名称、方法参数等的打印。

AspectJ

AspectJ实际上是对AOP编程思想的一个实践,AOP虽然是一种思想,但就好像OOP中的Java一样,一些先行者也开发了一套语言来支持AOP。目前用得比较火的就是AspectJ了,它是一种几乎和Java完全一样的语言,而且完全兼容Java(AspectJ应该就是一种扩展Java,但它不是像Groovy那样的拓展。)。当然,除了使用AspectJ特殊的语言外,AspectJ还支持原生的Java,只要加上对应的AspectJ注解就好。所以,使用AspectJ有两种方法:

  • 完全使用AspectJ的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
  • 或者使用纯Java语言开发,然后使用AspectJ注解,简称@AspectJ。

AspectJ基本概念

  • Aspect 切面:切面是切入点和通知的集合。
  • PointCut 切入点:切入点是指那些通过使用一些特定的表达式过滤出来的想要切入Advice的连接点。
  • Advice 通知:通知是向切点中注入的代码实现方法。
  • Joint Point 连接点:所有的目标方法都是连接点.
  • Weaving 编织:主要是在编译期使用AJC将切面的代码注入到目标中, 并生成出代码混合过的.class的过程.

AspectJ的使用

引入AspectJ

Project根目录下的build.gradle添加aspectj插件依赖

buildscript {
    ......
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.14'
        classpath 'org.aspectj:aspectjweaver:1.8.14'
    }
    .......
}

app/build.gradle引入aspectj插件

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
apply plugin: 'com.android.application'
android {
defaultConfig {
......
......
}
buildTypes {
......
......
}

final def log = project.logger
    final def variants = applicationVariants
    variants.all { variant ->
        if (!variant.buildType.isDebuggable()) {
            //方法调用性能,需在debug版本下,注解的方法不能混淆,否则无法成功,所以在跟踪新能问题时需要将混淆关掉
            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;
                }
            }
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'org.aspectj:aspectjrt:1.8.14'
}

再考虑登录判定场景

通常做法:

void doSomeThing(){
	// 判断当前用户是否登录
        if(LoginHelper.isLogin(this)) {
            // doSomeThing, 如果已登录,直接处理
        }else{
            //doLogin, 跳转到登录页面,先登录
        }
}

采用AOP的做法:

@CheckLogin
    public void doSomeThing() {
         //DO SOMETHING
    }

那么CheckLogin注解如何处理呢?

创建切面AspectJ

用来处理触发切面的回调

@Aspect
public class CheckLoginAspectJ {
    private static final String TAG = "CheckLogin";
     /**
     * 找到处理的切点
     * * *(..)  可以处理CheckLogin这个类所有的方法
     */
    @Pointcut("execution(@com.calvin.aop.login.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 (MyApplication.isLogin) {
                Log.i(TAG, "checkLogin: 登录成功 ");
                return joinPoint.proceed();
            } else {
                Log.i(TAG, "checkLogin: 请登录");
                Toast.makeText(context, "请登录", Toast.LENGTH_SHORT).show();
                return null;
            }
        }
        return joinPoint.proceed();
    }
}

这里要使用Aspect的编译器编译必须给类打上标注,@Aspect。还有这里的Pointcut注解,就是切点,即触发该类的条件。
Pointcut这里,我使用了execution,也就是以方法执行时为切点,触发Aspect类。而execution里面的字符串是触发条件,也是具体的切点。我来解释一下参数的构成。execution(@com.calvin.aop.login.CheckLogin (..))这个条件是所有加了CheckLogin注解的方法或属性都会是切点,范围比较广。
Pointcut内还可以设定下面这些参数:

切入点类别 切入点语法
Method execution execution(MethodSignature)
Method call call(MethodSignature)
Constructor execution execution(ConstructorSignature)
Constructor call call(ConstructorSignature)
Class initialization staticinitialization(TypeSignature)
Field read access get(FieldSignature)
Field write access set(FieldSignature)
Exception handler execution handler(TypeSignature)
Object initialization initialization(ConstructorSignature)
Object pre-initialization preinitialization(ConstructorSignature)
Advice execution adviceexecution()
  • **:表示是任意包名

  • …:表示任意类型任意多个参数

com.calvin.aop.login.CheckLogin这是我的项目包名下需要指定类的绝对路径。再来看看@Around, Around是指JPoint执行前或执行后被触发,除了Around还有其他几种方式。

类型 描述
Before 前置通知, 在目标执行之前执行通知
After 后置通知, 目标执行后执行通知
Around 环绕通知, 在目标执行中执行通知, 控制目标执行时机
AfterReturning 后置返回通知, 目标返回时执行通知
AfterThrowing 异常通知, 目标抛出异常时执行通知

如果使用的是以方法相关为切点,那么使用MethodSignature来接收joinPointSignature。如果是属性或其他的,那么可以使用Signature类来接收。之后可以使用Signature来获取注解类。,那么通过jointPoint.getThis()获取使用该注解的的上下文对象。

添加注解

创建完Aspect类之后,还需要一个注解类,它的作用是:哪里需要做切点,那么哪里就用注解标注一下,这样方便快捷。

package com.calvin.aop.login;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD) //可以注解在方法 上
@Retention(RetentionPolicy.RUNTIME) //运行时(执行时)存在
public @interface CheckLogin {
}

参考文献

安卓架构师必备之Android AOP面向切面编程详解
AOP实现的工具类AopArms

你可能感兴趣的:(Android开源框架,aop,切面编程,Android,Aop)