Android AOP

本文是对网络中相关文章的总结,再加上自己的相关见解得出的,若有侵权请及时联系

参考的大佬博客:

白话 Android AOP ---> 这个说的是 ASM + Transform 的方式实现AOP(难度比较大,维护起来比较难)
深入理解Android之AOP ---> 这个说的是 AspectJ 实现 AOP(相当于别人的三方库,使用起来比较简单,容易上手)
Android AOP方案(一)——AspectJ ---> 基本语法加实现
Android AspectJ详解 ---> 更为详细的基本语法加实现 主要可以看这篇博客
谈谈Android AOP技术方案

AOP简介

AOP(Aspect Oriented Programming 的缩写),意为:面向切面编程,和OOP(Object Oriented Programming,面向对象编程)以对象为核心不同,AOP 则是针对业务处理过程中的相同或者相似的代码逻辑(切面)进行提取,然后统一处理,它所面对的是处理过程中的某个步骤或阶段。这两种设计思想在目标上有着本质的差异,但是 AOP 和 OOP 并不是对立的,相反,巧妙的结合这两种思想来指导代码编写,会让我们的代码保持可重用性的同时,显著降低各个部分之间的耦合度。

OOP 和 AOP 都是方法论,是我个人认为对这两种思想最准确的描述和总结。

自我总结:AOP是一种思想,其目的是为了完成解耦,将同一功能从代码中抽离,然后使用轻量级的方式注入到代码中,以实现某种功能(相同的逻辑在同一个地方处理);从这种方面来想,在 BaseActivity 和 在Application 中添加 ActivityLifecycle 监听的也是一种AOP的实现方式(可以理解为,这种是有现成的切面的AOP)

学习之前需要理解一些AOP术语

AOP术语:

  • Cross-cutting concerns(横切关注点):监管面向对象模型中大多数类会实现单一特定的功能,但通常也会开放一些通用的附属功能给其他类。例如,我们喜欢在数据访问层中的类添加日志,同时也希望当UI层中一个县城进入或者退出调用一个方法时添加日志。监管每个类都有一个区别于其他类的主要功能,但在代码里,仍然经常需要添加一些相同的附属功能。
  • Advice(通知):注入到class文件中的代码。典型的Advice类型有before、after和around,分别表示在目标方法执行之前、执行后和完全代替目标方法执行的代码。除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
  • Joint Point(连接点):程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
  • Pointcut(切入点,切点):告诉代码注入工具,在任何注入一段特定代码的表达式。例如,在哪些joint points应用一个特定的Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebugLog的自定义注解的所有方法。
  • Aspect(切面):Pointcut和Advice的组合看做切面。例如,我们在应用中通过定义一个pointcut和给定恰当的advice,添加一个日志切面。
  • Weaving(织入):注入代码(advices)到目标位置(joint points)的过程

AOP的Android实现

实现AOP的技术,主要分为两大类:

  1. 采用动态代理技术,利用截取消息的方式,对该信息进行装饰,以取代原有对象行为的执行
  2. 采用静态织入的方式,引入特定的语句创建“方面”,从而使得编译器可以在编译期间织入有关“方面的代码

AOP是一种思想,要使用这种思想就需要拥有这种思想的工具,下面列举我现在知道的AOP的实现方式:

  • ASM+Transformer库:ASM是一个通用的Java字节码操作和分析框架, Transfrom允许第三方插件在经过编译的 .class 文件转换为 .dex 文件之前对其进行操纵。ASM的class的字节码过于复杂,总是出错,开发成本非常高,上手难度很大,但是ASM库非常强大,更加灵活
  • AsepcJ:它是一种几乎和Java完全一样的语言,而且完全兼容Java(AspectJ应该就是一种扩展Java,但它不是像Groovy那样的拓展)。当然,除了使用AspectJ特殊的语言外,AspectJ还支持原生的Java,只要加上对应的AspectJ注解就好。所以,使用AspectJ有两种方法:
    1. 完全使用AspectJ的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
    2. 或者使用纯Java语言开发,然后使用AspectJ注解,简称 @AspectJ。
  • Hugo:另一个是Jake大神实现的Hugo库
  • Lancet:一个轻量级Android AOP框架

以下以AsepcJ来讲解AOP

AsepcJ的使用实例

环境配置

通过插件的形式来配置AspectJ环境。 具体可见AspectJX Github地址

  1. 在项目根目录的build.gradle里依赖AspectJX
buildscript {
    dependencies {
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
    }
}
  1. 在需要支持AspectJ的module的build.gradle文件中声明插件。
apply plugin: 'android-aspectjx'

在编译阶段AspectJ会遍历工程中所有class文件(包括第三方类库的class)寻找符合条件的切入点,为加快这个过程或缩小代码织入范围,我们可以使用exclude排除掉指定包名的class。

# app/build.gradle
aspectjx {
    //排除所有package路径中包含`android.support`的class文件及库(jar文件)
    exclude 'android.support'
}

在debug阶段我们更注重编译速度,可以关闭代码织入。

# app/build.gradle
aspectjx {
    //关闭AspectJX功能
    enabled false
}

基本使用

  • @Aspect 用它声明一个类,表示一个需要执行的切面。
  • @Pointcut 声明一个切点。
  • @Before/@After/@Around/...(统称为Advice类型) 声明在切点前、后、中执行切面代码。

举个例子:

@Aspect  //声明一个切面类,此处是固定的
public class MethodAspect {

    // 此处指定一个切点,后面括号中的是切点表达式(个人理解:其表达的就是一个join point),详细见Aspect基本语法
    @Pointcut("call(* com.wandering.sample.aspectj.Animal.fly(..))")
    public void callMethod() {
    }

    //表示一个通知,类型为Before并指定切点为上面callMethod方法所表示的那个切点
    @Before("callMethod()")
    public void beforeMethodCall(JoinPoint joinPoint) {
        Log.e(TAG, "before->" + joinPoint.getTarget().toString());  //织入的代码
}}

AsepcJ的基本语法

官网文档
本处基本摘于: Android AspectJ详解

Join Point

Joint Point 含义
Method call 方法被调用
Method execution 方法执行
Constructor call 构造函数被调用
Constructor execution 构造函数执行
Static initialization static块初始化
Field get 读取属性
Field set 写入属性
Handler 异常处理

Method call 和 Method execution的区别常拿来比较,其实就是调用与执行的区别

就拿上面Animal的fly方法举例。demo代码如下:

Animal a = Animal();
a.fly();

如果我们声明的织入点为call,再假设Advice类型是before,则织入后代码结构是这样的。

Animal a = new Animal();
//...我是织入代码
a.fly();

如果我们声明的织入点为execution,则织入后代码结构就成这样了。

public class Animal {
    public void fly() {
        //...我是织入代码
        Log.e(TAG, "animal fly method:" + this.toString() + "#fly");
    }}

本质上的区别就是织入对象不同,call被织入在指定方法被调用的位置上,而execution被织入到指定的方法内部。

Pointcut

Pointcuts是具体的切入点,基本上Pointcuts 是和 Join Point 相对应的。

Joint Point Pointcuts 表达式
Method call call(MethodPattern)
Method execution execution(MethodPattern)
Constructor call call(ConstructorPattern)
Constructor execution execution(ConstructorPattern)
Static initialization staticinitialization(TypePattern)
Field get get(FieldPattern)
Field set set(FieldPattern)
Handler handler(TypePattern)

除了上面与 Join Point 对应的选择外,Pointcuts 还有其他选择方法。

Pointcuts表达式 说明
within(TypePattern) 符合 TypePattern 的代码中的 Join Point
withincode(MethodPattern) 在某些方法中的 Join Point
withincode(ConstructorPattern) 在某些构造函数中的 Join Point
cflow(Pointcut) Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,包括 P 本身
cflowbelow(Pointcut) Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,不包括 P 本身
this(Type or Id) Join Point 所属的 this 对象是否 instanceOf Type 或者 Id 的类型
target(Type or Id) Join Point 所在的对象(例如 call 或 execution 操作符应用的对象)是否 instanceOf Type 或者 Id 的类型
args(Type or Id, ...) 方法或构造函数参数的类型
if(BooleanExpression) 满足表达式的 Join Point,表达式只能使用静态属性、Pointcuts 或 Advice 暴露的参数、thisJoinPoint 对象

Pattern

Pattern类型 语法
MethodPattern [!] [@Annotation] [public,protected,private] [static] [final] 返回值类型 [类名.]方法名(参数类型列表) [throws 异常类型]
ConstructorPattern [!] [@Annotation] [public,protected,private] [final] [类名.]new(参数类型列表) [throws 异常类型]
FieldPattern [!] [@Annotation] [public,protected,private] [static] [final] 属性类型 [类名.]属性名
TypePattern 其他 Pattern 涉及到的类型规则也是一样,可以使用 '!'、''、'..'、'+','!' 表示取反,'' 匹配除 . 外的所有字符串,'*' 单独使用事表示匹配任意类型,'..' 匹配任意字符串,'..' 单独使用时表示匹配任意长度任意类型,'+' 匹配其自身及子类,还有一个 '...'表示不定个数

说明:

@注解 访问权限 返回值的类型 包名.函数名(参数)

  1. @注解和访问权限(public/private/protect,以及static/final)属于可选项。如果不设置它们,则默认都会选择。以访问权限为例,如果没有设置访问权限作为条件,那么public,private,protect及static、final的函数都会进行搜索。
  2. 返回值类型就是普通的函数的返回值类型。如果不限定类型的话,就用*通配符表示
  3. 包名.函数名用于查找匹配的函数。可以使用通配符,包括和..以及+号。其中号用于匹配除.号之外的任意字符,而..则表示任意子package,+号表示子类。
    比如:
    java..Date:可以表示java.sql.Date,也可以表示java.util.Date
    Test
    :可以表示TestBase,也可以表示TestDervied
    java..:表示java任意子类
    java..
    Model+:表示Java任意package中名字以Model结尾的子类,比如TabelModel,TreeModel 等
  4. 最后来看函数的参数。参数匹配比较简单,主要是参数类型,比如:
    • (int, char):表示参数只有两个,并且第一个参数类型是int,第二个参数类型是char
    • (String, ..):表示至少有一个参数。并且第一个参数类型是String,后面参数类型不限。在参数匹配中,..代表任意参数个数和类型
    • (Object ...):表示不定个数的参数,且类型都是Object,这里的...不是通配符,而是Java中代表不定参数的意思

Advice

直译过来是通知,实际上表示一类代码织入位置,在AspectJ中有五种类型的注解:Before、After、AfterReturning、AfterThrowing、Around,我们将它们统称为Advice注解。

Advice 说明
@Before 切入点前织入
@After 切入点后织入,无论连接点执行如何,包括正常的 return 和 throw 异常
@AfterReturning 只有在切入点正常返回之后才会执行,不指定返回类型时匹配所有类型
@AfterThrowing 只有在切入点抛出异常后才执行,不指定异常类型时匹配所有类型
@Around 替代原有切点,如果要执行原来代码的话,调用 ProceedingJoinPoint.proceed()

Advice注解修饰的方法有一些约束:

  1. 方法必须为public。
  2. Before、After、AfterReturning、AfterThrowing 四种类型方法返回值必须为void。
  3. Around的目标是替代原切入点,它一般会有返回值,这就要求声明的返回值类型必须与切入点方法的返回值保持一致;不能和其他 Advice 一起使用,如果在对一个 Pointcut 声明 Around 之后还声明 Before 或者 After 则会失效。
  4. 方法签名可以额外声明JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart。

常见方法

@Aspect 定义类为切入类
@Pointcut 声明一个切入策略供
@Before @After @ Around @ AfterReturning选择
@Before 被切入方法执行前执行
@After 被切入方法执行后执行
@Around 被切入方法前后都可以加入一些逻辑
@AfterReturning 被切入方法返回时执行
JoinPoint 加入这个参数可以获取被切入方法的名称和参数

JoinPoint 对象
Signature getSignature();//获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 (修饰符+包名+类名+方法名)
Object[] getArgs();//获取传入目标方法的参数对象
Object getTarget();//获取传入目标方法的参数对象
Object getThis();//获取代理对象
getSignature().getName();//获取方法名

ProceedingJoinPoint对象
只用在@Around的切面方法中,是JoinPoint的子接口
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法

实例(快速点击)

避免快速点击.png

AspectJ 缺点

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

推荐阅读更多精彩内容