本文是对网络中相关文章的总结,再加上自己的相关见解得出的,若有侵权请及时联系
参考的大佬博客:
白话 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的技术,主要分为两大类:
- 采用动态代理技术,利用截取消息的方式,对该信息进行装饰,以取代原有对象行为的执行
- 采用静态织入的方式,引入特定的语句创建“方面”,从而使得编译器可以在编译期间织入有关“方面的代码
AOP是一种思想,要使用这种思想就需要拥有这种思想的工具,下面列举我现在知道的AOP的实现方式:
- ASM+Transformer库:ASM是一个通用的Java字节码操作和分析框架, Transfrom允许第三方插件在经过编译的 .class 文件转换为 .dex 文件之前对其进行操纵。ASM的class的字节码过于复杂,总是出错,开发成本非常高,上手难度很大,但是ASM库非常强大,更加灵活
- AsepcJ:它是一种几乎和Java完全一样的语言,而且完全兼容Java(AspectJ应该就是一种扩展Java,但它不是像Groovy那样的拓展)。当然,除了使用AspectJ特殊的语言外,AspectJ还支持原生的Java,只要加上对应的AspectJ注解就好。所以,使用AspectJ有两种方法:
- 完全使用AspectJ的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
- 或者使用纯Java语言开发,然后使用AspectJ注解,简称 @AspectJ。
- Hugo:另一个是Jake大神实现的Hugo库
- Lancet:一个轻量级Android AOP框架
以下以AsepcJ来讲解AOP
AsepcJ的使用实例
环境配置
通过插件的形式来配置AspectJ环境。 具体可见AspectJX Github地址
- 在项目根目录的build.gradle里依赖AspectJX
buildscript {
dependencies {
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
}
- 在需要支持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 涉及到的类型规则也是一样,可以使用 '!'、''、'..'、'+','!' 表示取反,'' 匹配除 . 外的所有字符串,'*' 单独使用事表示匹配任意类型,'..' 匹配任意字符串,'..' 单独使用时表示匹配任意长度任意类型,'+' 匹配其自身及子类,还有一个 '...'表示不定个数 |
说明:
@注解 访问权限 返回值的类型 包名.函数名(参数)
- @注解和访问权限(public/private/protect,以及static/final)属于可选项。如果不设置它们,则默认都会选择。以访问权限为例,如果没有设置访问权限作为条件,那么public,private,protect及static、final的函数都会进行搜索。
- 返回值类型就是普通的函数的返回值类型。如果不限定类型的话,就用*通配符表示
- 包名.函数名用于查找匹配的函数。可以使用通配符,包括和..以及+号。其中号用于匹配除.号之外的任意字符,而..则表示任意子package,+号表示子类。
比如:
java..Date:可以表示java.sql.Date,也可以表示java.util.Date
Test:可以表示TestBase,也可以表示TestDervied
java..:表示java任意子类
java..Model+:表示Java任意package中名字以Model结尾的子类,比如TabelModel,TreeModel 等- 最后来看函数的参数。参数匹配比较简单,主要是参数类型,比如:
- (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注解修饰的方法有一些约束:
- 方法必须为public。
- Before、After、AfterReturning、AfterThrowing 四种类型方法返回值必须为void。
- Around的目标是替代原切入点,它一般会有返回值,这就要求声明的返回值类型必须与切入点方法的返回值保持一致;不能和其他 Advice 一起使用,如果在对一个 Pointcut 声明 Around 之后还声明 Before 或者 After 则会失效。
- 方法签名可以额外声明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 //传入的新的参数去执行目标方法
实例(快速点击)
AspectJ 缺点
- 如果相应的class没有实现相应的切点方法将无法织入,如上文中的没有BlankFragment实现onResume方法的话,将无法织入代码。
- 无法处理Lambda语法会有一系列兼容性问题,如R8、gradle版本不同等性能较差
- APP项目比较大时编译时间明显加长。
- 兼容性:如果使用的三方库也使用了AspectJ,可能导致未知的风险。