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编程的思想,如:

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

[本章完...]

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355