#AspectJ 在 Android 中的使用
在介绍 AspectJ 之前,我们先看看常见的几种编程架构思想。
- 面向对象编程 Object Oriented Programming
- 面向过程编程 Procedure Oriented Programming
- 面向切面编程 Aspect Oriented Programming
面向对象、面向过程、面向切面, 这三种是我们常见的三种编程架构思想,在日常的编程中, OOP 是 Android 开发中最常见的,其他的两种比较少见。
一、AOP
AOP 是面向切面编程,它在我们的日志系统、权限管理方面有着比较好的应用。
在项目中,我们的很多功能都是分散到各个模块,例如日志打印,AOP 的目标就是要把这些功能集中起来,放到一个统一的地方来控制和管理。
二、AspectJ
AOP 是一种编程的思想,在具体的编程中需要实际的工具去实现这套思想。 AspectJ 就是这样的一个工具。使用 AspectJ 有两种方式
- 完全使用 AspectJ 的语言开发;
- 使用 AspectJ 注解,完全的使用纯 Java 开发
我们后续讲的,基本上都是以 AspectJ 注解的方法,同时在最后也会附上 AspectJ 和 AspectJ 注解的等价。
1.AspectJ 语法
这里只是介绍简单的一些概念,如果想要去了解深入的用法,可参考文后的链接,去官网查看。
1. JoinPoint
JoinPoint: A particular point in a program that might be the target of code injection.
JoinPoint 简单一点说就是程序运行时要执行一些动作的点。
AspectJ 中可以选择的 JoinPoint
JoinPoint | 说明 | 示例 |
---|---|---|
method call | 函数调用 | 例如调用 Log.e( ) |
method execution | 函数执行 | 例如 Log.e( ) 的执行内部。 method call 是调用某个函数的地方 execution 是某个函数执行的内部 |
constructor call | 构造函数调用 | 和 method call 类似 |
constructor execution | 构造函数执行 | 和 method execution 类似 |
field get | 获取某个变量 | 例如读取 MainActivity.mTest 成员 |
field set | 设置某个变量 | 例如设置 MainActivity.mTest 成员 |
pre-initialization | Object 在构造函数中做的一些工作 | |
initialization | Object 在构造函数中做的工作 | |
static initialization | 类初始化 | 例如类的 static{} |
handler | 异常处理 | 例如 try catch(xxx) 中,对应 catch 内的执行 |
advice execution | AspectJ 的内容 |
JoinPoint 的选择要结合下面的 Pointcuts 表达式来看
2. Pointcut
Pointcut: An expression which tell a code injection tool where to inject a particular piece of code
Pointcut 简单的说就是从一堆的 JoinPoint 中挑选感兴趣的 JoinPoint 的表达式。
例如
pointcut anyCall(): call(* *.println(..)) && !within(TestAspect);
在 AspectJ 的语言中定义一个 Pointcout 需要用关键词 pointcut .
上面的这里是
- pointcut: 是定一个 Pointcut 的关键词
- anyCall(): 是 Pointcut 的名称
- call : 表示 JoinPoint 的类型为 call
- 第一个 ‘*‘ 号是返回值, ‘*’ 代表是任意返回值; 第二个 ‘*’ 号代表是包名,‘*’ 代表是任意包名,这边表明我们是选择任意包名下的 println 函数;在 (..) 中指定参数类型,‘..’ 通配符表示任意类型;
- &&! 表示组合条件,有 &&, || 以及 !
- within(TestAspect): within 是 JoinPoint 间接选择过滤的一个方法,后面会讲到。 !within(TestAspect) 表示调用者的类型不是 TestAspect.
3. JointPoint 的选择
JointPoint 的选择有分成直接选择和间接选择两种方式
- JointPoint 的直接选择就是通过和 Pointcut 的语法一一对应关系中选择;
- JointPoint 的间接选择就是通过一些通配符进行筛选过滤的选择,上面例子中的 within 就是间接选择的一种。
1.JointPoint 直接选择
JoinPoint 的选择策略和 Pointcut 的语法对应关系
JoinPoint Category | Pointcut Syntax |
---|---|
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() |
JoinPoint 的策略的选择对应着不同 Pointcut,特别是 Pointcut 里面有着不同的 Signature。
以下有详细的说明:
Method Signature 表达式
语法
@注解 访问权限 返回值的类型 包名.函数名(参数)
例子:
@before(“execution(* android.app.Activity.on**(..))”);
- 注解: 是可选项; 这里是 @before,关于注解的在后面 Adivce 中有更详细的说明
- 访问权限: 可选项; 有 public, private, protected 类型;例子没有设置
- 返回值的类型: 与普通函数的返回值类型是一样的,如果不限定类型,用通配符 表示。例子中是
包名.函数名:用于查找匹配的函数,可以使用通配符
通配符的类型- ‘ * ‘表示用于匹配处 . 号之外的任意字符;
- ‘ .. ‘ 表示任意子 package
‘ + ‘号表示子类
例子:- java.*.Data: 可以表示 java.sql.Data ,也可以表示 java.util.Date;
- Test* : 表示Test开头的函数,可以表示 TestBase, 也可以表示 TestDervied
- java..* : 表示 java 任意子类
- java..*Model+: 表示 Java 任意 package 中名字以 Model 结尾的子类,比如 TabelModel, TreeModel 等
函数参数
参数有不同的型式- (int, char): 表示参数只有两个, 并且第一个参数是 int, 第二个参数是 char;
- (String, ..): 表示参数至少有一个。并且第一个参数是 String, 后面参数类型不限。在参数匹配中, .. 代表任意参数个数和类型;
- (Oject …): 表示不定个数的参数,并且类型都是 Object, 这里的 … 不是通配符,而是 java 中不定参数的意思;
Constructor Signature 表达式
和 Method Signature 类似
不同点:
构造函数没有返回值,并且函数名必须叫 new
例子:
public *..TestDeived.new(..)
- public: 表示选择 public 访问权限的
- *.. : 代表任意包名
- TestDeived.new: 代表 TestDerived 的构造函数
- (..): 代表参数个数和类型都是任意的
Field Signature表达式
语法
@注解 访问权限 类型 类名.成员变量名
- @注解和访问权限是可选的
- 类型:成员变量类型, * 表示任意类型
- 类名.成员变量名: 成员变量名可以是*, 代表任意成员变量
例子, 用 AspectJ 打印成员变量赋值前后的值
1 | // TraceAspect.java set field 的切面 |
打印结果
onFiled value = 100
fieldSignature =int org.android10.viewgroupperformance.activity.MainActivity.mTest
field = private int org.android10.viewgroupperformance.activity.MainActivity.mTest +
FileName = mTest
clazzName = int
oldValue = -1
TypeSignature表达式
例子:
staticinitlization(test..TestBase): 表示 TestBase 类的 static block
handler(NullPointException): 表示 catch 到 NullPointerException 的 JPoin
2.JointPoint 间接选择
JointPoint 的直接选择是通过 Signature 信息匹配的,除此之外还有其他的方式,这些方式都可以归类到间接选择
关键词 | 说明 | 实例 | |
---|---|---|---|
within(TypePattern) | TypePattern 表示 package 或者类 TypePattern 可以使用通配符 |
表示某个 Package 或者类中的 Point within(Test): Test 类中(包括内部类)所有的 JointPoint |
|
withcode(Constructor Signature\ | Method Signature) | 表示某个构造函数或其他函数执行过程涉及到的 JointPoint | withinCode( Test.testMethod(..)) 表示 testMethod 涉及的 JointPoint withinCode(.Test.new(..)) 表示 Test 的构造函数涉及的 JointPoint |
cflow(pointcuts) | cflow 表示 call flow cflow 的条件是一个 pointcut |
cflow(call Test.testMethod) 表示调用 Test.testMethod 函数是所包含的 JointPoint,包含 testMethod 的 call 这个 JointPoint 本身 |
|
cflowbelow(pointcuts) | cflowbelow 表示不包含自身的 cflow | cflowbelow(call Test.testMethod) 表示调用 Test.testMethod 函数是所包含的 JointPoint, 不包含 testMethod 的 call 这个 JointPoint 本身 |
|
this(Type) | JointPoint 的 this 对象是 Type 类型 | JPoint是代码段(不论是函数,异常处理,static block),从语法上说,它都属于一个类。如果这个类的类型是Type标示的类型,则和它相关的JPoint将全部被选中。 | |
target(Type) | JoinPoint 的 target 对象是 Type 类型 | 和this相对的是target。不过target一般用在call的情况。call一个函数,这个函数可能定义在其他类。比如testMethod是TestDerived类定义的。那么target(TestDerived)就会搜索到调用testMethod的地方。但是不包括testMethod的execution JointPoint | |
args(TypeSignature) | 用来对 JointPoint 的参数进行条件搜索 | 例如 arg(int, ..) 表示第一个参数是 int, 后面参数个数和类型不限的 JointPoint |
3.call 与 execution 区别
当 call 捕获 joinPoint 时,捕获的签名方法的调用点;execution 捕获 joinPoint 时,捕获的则是执行点。
两个的区别在于一个是 ”调用点“, 一个是 ”执行点“
对于 call 来讲
1 | call(Before) |
对于 execution 来说
1 | Pointcut { |
3.AspectJ 注解的等价
AspectJ 提供了相应的注解,注解的方式和 AspectJ 语言编写是等效的。我们在 Android 中一般也是采用注解的方式
Aspect
public aspect Foo{}
等效
@Aspect
public class Foo{}
call
@Pointcut(“call( .(..))”)
void anyCall(){}
等效
pointcut anyCall(): call( .(..))
要绑定参数的时候,只需要将参数作为备注解的方法的参数即可
@Pointcut(“call( .(int)) && arg(i) && target(callee)”)
void anyCall(int i, Fool callee){}
等效
pointcut anyCall(int i, Foo callee): call( .(int)) && arg(i) && target(callee){};
说明要先定义参数 int i, Foo callee
before
@Before(“call( org.android10.viewgroupperformance.activity..(..)) && this(foo)”)
public void callFromFoo(Foo foo){}
等效
before(Foo foo): call( org.android10.viewgroupperformance.activity..(..)) && this(foo){}
returning
@AfterReturning(pointcut=”call(Foo+.new(..))”, returning=”f”)
public void itsAFoo(foo f){}
等效
after returning(Foo f): call(Foo+.new(..)){}
其他的表达式也是类似的
4. Advice
Advice: Advice defines pieces of aspect implementation that execute at well-defined points in the execution of the program.
通过前面的 Pointcuts 找到了相应的 JointPoint, 需要对这些 JointPoint 最一些事情,相当于对 JointPoint 进行 hook, 这就是 advice 要做的事。
advice 的分类
关键词 | 说明 | 示例 |
---|---|---|
before() | before advice | 表示在 JointPoint 执行之前要干 的事 |
after() | after advice | 表示在 JointPoint 执行之后要干的事 |
after(): returning(返回值类型) after():throwing(异常类型) |
returning 和 throwing 后面都可以指定具体的类型,如果不指定则匹配类型不限制 | 假设 JointPoint 是一个函数调用 那么函数调用执行完有两种方式退出 一个是正常的 return, 另一个是抛异常 after() 默认包括 returing 和 throwing 两种情况 |
返回值类型 around() |
around 替代原来的 JointPoint | around 替代了原来的 JointPoint,如果要执行原 JointPoint 的话,需要调用 procced |
例子:
我们需要在 Activity 中的 onResume 方法调用前后输出
1 | // MainActivity.java |
查看输出
改成 Around 的方式
1 | // TraceAspect.java |
输出
从上面例子的输出我们可以看到 around 等价于 before + after, 另外 JointPoint#proceed 是原来的 JointPoint,在这里是 onResume 方法, 输出中的 “— onResume—” 就是在 onResume 中打印的。
5. 参数传递和 JointPoint 信息
经过前面的几个步骤,我们已经拿到了 JointPoint,但是我们经常需要对一些 advice 传入参数,然后进行处理的。例如如果传入的参数不合法,就不用调 JointPoint#proceed 方法处理了。
参数传递
advice 参数的方法由 this, target(), args()
- this(Type or Id): 捕获当前对象(被绑定 this)实例执行的连接点 – 实例由 Type 或者 Id 描述
- target(Type or Id): 捕获目标对象(被应用与对象上的调用和属性操作)实例的连接点 – 实例由 Type 和 Id 描述(必须绑定和封装后放入通知或者切点定义)。它不匹配任何静态的调用、应用和设置成员。
- args(Type or Id): 捕获具有适当类型样式的实例连接点
下面是例子说明
我们在 MainActivity 定义一个成员变量 mTest, 初始值为 -1,在 OnResume() 方法对它进行赋值,用 target 和 args 对 mTest 赋值前后的值进行监听
1 | // MainActivity.java |
我们看看输出
定义切面表达式使用 args(newValue) && target(t) 它们的参数值 newValue, t,必须要和方法中的定义的对的上
public void onFiled(JoinPoint joinPoint, Object newValue, Object t)
JointPoint 信息
在 advice 中我们可以拿到 JointPoint 的信息,一般包含
JointPoint 对象信息:例如参数、前面之类的
JoinPoint.getSignature() 包含有
- MethodSignature 方法的签名
- FieldSignature 成员变量的签名
- ConstructorSignature 构造函数的签名
- InitializerSignature 初始化的签名
不同的签名对应不同的场景
- JointPoint 源代码部分的信息,例如类型、所处的位置
- JointPoint 静态部分信息
例子:
1 | "methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace()") ( |
更详细的信息参考文档
https://www.eclipse.org/aspectj/doc/released/runtime-api/index.html
总结一下,使用 AspectJ 的步骤:
- 设置 Pointcut 的表达式
- 选择相应的 advice
- 对 JointPoint 或参数进行相应的处理
三、AspectJ 集成在 Android studio 中
前面已经介绍完了 AspectJ, 那接下来看看在 Android 中的实际使用;
实例代码是在这个例子上进行修改的Android-AOPExample。
1. Library 库依赖方式使用
项目的结构如下
在 library 项目 gintoinc 的 build.gradle 文件要添加 aspectj 的依赖
1 | buildscript { |
在引用库工程的工程 build.gradle 也要进行相应的配置
1 | import org.aspectj.bridge.IMessage |
文章Aspect Oriented Programming in Android 是通过注解去查看方法执行的时间,我们在这个基础上进行修改,去监听一个成员变量赋值变化的监听。
我们需要监听 MainActivity 中 mTest
1 | private int mTest = -1; |
第一步. 设置 Pointcut 的表达式
在 TraceAspect.java中
1 | // set field 的切面 |
根据 JoinPoint 的选择策略和 Pointcut 的语法对应关系,成员变量选择的 set, 参数传递的监听使用 args(newValue) && target(t)
第二步. 选择相应的 advice
1 | (POINTCUT_FILEED) |
这里我们选择的是 Before, 注意在第一步 args(newValue) && target(t) 中的 newValue 和 t 是要在 advice 函数中定义的 Object newValue, Object t.
第三步. 对 JointPoint 或参数进行相应的处理
1 | (POINTCUT_FILEED) |
通过 JoinPoint 获取相应的信息。
在 build 之后,在 app/intermediates/classes/debug 目录下的
MainActivity.class 文件中
1 | protected void onResume() { |
我们发现上面生成了一下代码,这些生成的代码就是 AspectJ 根据我们前面设置的 Pointcut 和 adive 生成的。
输出
在 build 之后,在 app/intermediates/classes/debug 目录下的
2. Plugin 插件方式使用
如果是多个 Module 都依赖 AspectJ, 可以写成 plugin 插件的型式
关于如果使用 Android studio 的 Plugin 插件,可以去查看相关资料
项目的 build.gradle
1 | buildscript { |
gintonic 的 build.gradle 文件
1 | buildscript { |
app module 的 build.gradle 文件
1 | apply plugin: 'com.android.application' |
具体的代码可以去 github AndroidAopDemo 选择 tag v2 即可。
四、参考
- 1.深入理解Android之AOP
- 2.Aspect Oriented Programming in Android
- 3.《Android 全埋点解决方案》第 8 章,AppClick 全埋点方案5:AspectJ
- 4.极客时间专栏《Android 开发高手课》第 27 讲, 编译插桩的三种方法: AspectJ, ASM, ReDex
- 5.AspectJ 官方手册