Android之AOP架构<第一篇>:入门

(1)AOP的概念

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

场景

首先来看以下代码:

private void method_1(){

    long startTime = System.currentTimeMillis();

    //...业务逻辑

    //...业务逻辑

    long endTime = System.currentTimeMillis();
    LogUtil.i(TAG, "method_1耗时时长:"+(endTime-startTime));

}

private void method_2(){

    long startTime = System.currentTimeMillis();

    //...业务逻辑

    //...业务逻辑

    long endTime = System.currentTimeMillis();
    LogUtil.i(TAG, "method_1耗时时长:"+(endTime-startTime));

}

private void method_3(){

    long startTime = System.currentTimeMillis();

    //...业务逻辑

    //...业务逻辑

    long endTime = System.currentTimeMillis();
    LogUtil.i(TAG, "method_1耗时时长:"+(endTime-startTime));

}

有三个方法,这三个方法实现了不同的业务逻辑,此时,需要统计它们执行的耗时时间,那么如上代码的实现方式是可以实现的。但是在某些大厂是不允许这样实现的,因为这使本身的业务逻辑与日志逻辑相互耦合,很显然,这违反了单一功能原则

以上代码可以绘制一张图来表示,如下:

图片.png

现在大致介绍一下上图,在一个大型项目,有多个方法,一般情况下,一个方法只做一件事,但是呢?因为需求的必要性,有些时候会发生一个方法里面存在和业务无关的代码,比如日志打印,如果每个方法都加上日志,那么就破坏了方法的单一原则,为了使代码简洁, 单一原则是必须遵守的,所以原则上,业务逻辑日志逻辑必须解耦。

这里就需要采用面向切面编程(AOP)来实现解耦了。

再来看下图:

图片.png

图中画出了性能检测切面日志切面,这两个功能的代码需要放在某方法中才能实现,切面的代码和业务逻辑完全解耦,原理是:定义一个切面,从源码层看,切面和业务逻辑完全解耦,当编译生成字节码(class)文件时,将对应的代码动态注入到指定方法中,这个动态注入可以描述成动态代理

所以,可以总结出AOP的优势:减少重复代码、提高开发效率、维护方便,简单说就是:解耦!简单!好维护

(2)AOP在Android中的实现

[AspectJX框架]

在Android中实现AOP,一般采用AspectJX框架,工欲善其事,必先利其器,我们有必要引用已有三方库,在github搜索下AspectJX,可以找到一些AspectJX框架的远程仓库,本文引用以下依赖库,如下:

https://github.com/JakeWharton/hugo

打开这个链接,可以发现,AspectJX的依赖配置方式已经为我们提供了。

[AOP基本术语]

  • Joinpoint(连接点):类里面可以被增强的方法,这些方法成为连接点

  • Pointcut(切入点:):所谓切入点就是我们实际增强的那些方法

  • Advice(通知/增强):增强的逻辑,称为增强,比如扩展日志功能,这个日志功能称为增强

  • 前置通知:在方法之前执行

  • 后置通知:在方法之后执行

  • 异常通知:方法出现异常

  • 最终通知:在后置之后执行

  • 环绕通知:在方法之前和之后执行

  • 切面:把增强应用到具体方法上面,过程称为切面把增强用到切入点过程

[常用注解]

@Aspect:声明切面,标记类
@Pointcut(切点表达式):定义切点,标记方法
@Before(切点表达式):前置通知,切点之前执行
@Around(切点表达式):环绕通知,切点前后执行
@After(切点表达式):后置通知,切点之后执行
@AfterReturning(切点表达式):返回通知,切点方法返回结果之后执行
@AfterThrowing(切点表达式):异常通知,切点抛出异常时执行
@Pointcut、@Before、@Around、@After、@AfterReturning、@AfterThrowing需要在切面类中使用,即在使用@Aspect的类中。

[切面(Aspect)]

定义一个切面比较简单,只需要在类上加上一个@Aspect注解即可。

@Aspect
public class TestAnnoAspect {

}

TestAnnoAspect就是所谓的切面了,切面中主要定义一些切点。

[切点(Pointcut)]

@Aspect
public class TestAnnoAspect {

    @Pointcut("execution(* com.example.aopdemo.MainActivity.test(..))")
    public void pointcut() {
        Log.i("yunchong", "pointcut");
    }
}

这个切点对应MainActivity的test方法,如下:

private void test(){
    Log.i("yunchong", "执行了MainActivity中的test方法");
}

@Pointcut用于定义一个切点,所以pointcut方法就是一个切点,execution后面括号中的内容是切点的语法,常用的切点种类有两种:

方法执行:execution(MethodSignature)
方法调用:call(MethodSignature)

本文就以execution为例,那么* com.example.aopdemo.MainActivity.test(..)是什么意思呢?

号代表任意返回值类型,由于test方法的返回值类型是void,所以号也可以改成void,test后面的两个点表示任意参数。

[切点的处理]

切点定义好之后就可以使用切点表达式来处理这个切点。切点表达式有@Before@Around@After@AfterReturning@AfterThrowing

看一下如下代码:

@Aspect
public class TestAnnoAspect {

    @Pointcut("execution(void com.example.aopdemo.MainActivity.test(..))")
    public void pointcut() {
        Log.i("yunchong", "pointcut");
    }

    @Before("pointcut()")
    public void before(JoinPoint point) {
        Log.i("yunchong", "before");
    }


    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.i("yunchong", "around");
        joinPoint.proceed();// 目标方法执行完毕
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        Log.i("yunchong", "after");
    }

    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point, Object returnValue) {
        Log.i("yunchong", "afterReturning");
    }

    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        Log.i("yunchong", "afterThrowing:"+ex.getMessage());
    }
}

@Before("pointcut()")表示先执行被@Before修饰的方法,编译后,MainActivity.class文件中的test方法如下:

private void test() {
    JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
    TestAnnoAspect.aspectOf().before(var1);
    Log.i("yunchong", "执行了MainActivity中的test方法1");
    Log.i("yunchong", "执行了MainActivity中的test方法2");
    Log.i("yunchong", "执行了MainActivity中的test方法3");
    Log.i("yunchong", "执行了MainActivity中的test方法4");
    Log.i("yunchong", "执行了MainActivity中的test方法4");
}

打印结果如下:

图片.png

JoinPoint代码一个点,用专业的说法叫埋点,也就是说,@Before表达式在test方法的开头埋下了一个点。

@After("pointcut()")表示先执行MainActivity中的test方法,后执行切面中被@After修饰的方法,编译后,MainActivity.class文件中的test方法如下:

private void test() {
    JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);

    try {
        Log.i("yunchong", "执行了MainActivity中的test方法1");
        Log.i("yunchong", "执行了MainActivity中的test方法2");
        Log.i("yunchong", "执行了MainActivity中的test方法3");
        Log.i("yunchong", "执行了MainActivity中的test方法4");
        Log.i("yunchong", "执行了MainActivity中的test方法4");
    } catch (Throwable var3) {
        TestAnnoAspect.aspectOf().after(var1);
        throw var3;
    }

    TestAnnoAspect.aspectOf().after(var1);
}

打印结果如下:

图片.png

@Around("pointcut()") 需要也别注意

假设切点是这样的

@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    Log.i("yunchong", "around");
}

打印结果如下:

图片.png

实际上,test方法中的原有代码根本就没执行到,因为切点中还需要这样一句代码:

joinPoint.proceed();// 目标方法执行完毕

假设,切点修改为如下:

@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    Log.i("yunchong", "around1");
    joinPoint.proceed();// 目标方法执行完毕
    Log.i("yunchong", "around2");
}

打印结果如下:

图片.png

所以,被@Around修饰的切点,是否执行目标方法,joinPoint.proceed()可以控制。

@AfterReturning("pointcut()")和目标方法的返回值有关。

假如test方法加上返回值,如下:

private int test(){
    Log.i("yunchong", "执行了MainActivity中的test方法1");
    Log.i("yunchong", "执行了MainActivity中的test方法2");
    Log.i("yunchong", "执行了MainActivity中的test方法3");
    Log.i("yunchong", "执行了MainActivity中的test方法4");
    Log.i("yunchong", "执行了MainActivity中的test方法4");
    return 1;
}

那么,编译之后,MainActivity.class文件中的test方法如下:

private int test() {
    JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
    Log.i("yunchong", "执行了MainActivity中的test方法1");
    Log.i("yunchong", "执行了MainActivity中的test方法2");
    Log.i("yunchong", "执行了MainActivity中的test方法3");
    Log.i("yunchong", "执行了MainActivity中的test方法4");
    Log.i("yunchong", "执行了MainActivity中的test方法4");
    byte var2 = 1;
    TestAnnoAspect.aspectOf().afterReturning(var1, (Object)null);
    return var2;
}

打印结果如下:

图片.png

@AfterThrowing(value = "pointcut()", throwing = "ex")和异常有关,只有出现异常之后才会生效,修改test方法中的代码如下:

private void test(){

    Log.i("yunchong", "执行了MainActivity中的test方法1");

    int a = 1/0 ;

    Log.i("yunchong", "执行了MainActivity中的test方法2");

}

编译之后,MainActivity.class文件中的test方法如下:

private void test() {
    try {
        Log.i("yunchong", "执行了MainActivity中的test方法1");
        int a = 1 / 0;
        Log.i("yunchong", "执行了MainActivity中的test方法2");
    } catch (Throwable var3) {
        TestAnnoAspect.aspectOf().afterThrowing(var3);
        throw var3;
    }
}

也就是说,代码块被try...catch包裹,自带捕获异常的功能。

打印结果如下:

图片.png
总结:

Android AOP其实就是在某方法的开头或者结尾处埋下一个点,在指定点插入想要动态注入的代码。在原方法里,方法里面的功能是单一的,没有任何其它逻辑,符合单一性原则,编译之后,在字节码文件中会生成与原方法不一样的代码,字节码中的代码比原代码多了被动态注入的代码。

在Android中,很多场景都可以使用AOP编程的思想,如:

登录判断
网络判断
权限获取
数据校验
日志输出
性能监控 
按钮防抖

[本章完...]

你可能感兴趣的:(Android之AOP架构<第一篇>:入门)