AOP,即面向切面编程,之前我们在编程的时候,采用的是面向对象的思想编程,例如我们想要统计一个方法执行的耗时,通常需要记录一个开始时间,然后记录一个结束时间。
public void run(){
long startTime = System.currentTimeMillis();
System.out.println("do something");
long endTime = System.currentTimeMillis();
System.out.println("run 方法执行时间"+(endTime - startTime));
}
没问题,这个是正确的方法,但是如果当前场景下需要对10000个方法统计耗时,如果采用OOP的思想,那么需要对10000个方法去写这个样板代码,真的就写死了,那么针对这种场景,使用AOP能够起到事半功倍的效果。
面向切面编程,不需要侵入我们的代码,它是在预编译节点,通过某种方式获取方法,然后在方法之前之前记录一个时间,方法结束之后记录一个时间,两个时间相减就能得到方法的耗时,但是不需要每个方法植入OOP时写入的代码,而且更灵活更具备扩展性。
编译,通常就是通过javac,将java文件转换为JVM能够识别的class文件;那么如果使用AOP的编译方式,即ajc,同样能把java文件编译为class文件,而且能将一些业务逻辑 织入 class文件。
那么AspectJ框架就提供了面向切面的能力,它扩展了Java语言,并定义了AOP语法,在编译的过程中,通过编译器将字节码织入我们定义的切面代码,需要使用AspectJ的ajc编译方式。
1 切入点
如果想要在原有的代码基础上,织入自己的代码,需要定义一个切入点
@PointCut
public void run(){
System.out.println("do something");
}
例如我们之前的场景中,需要统计run方法的执行时间,那么run方法就是切入点,AspectJ获取切入点之后,拿到run方法,织入代码
2 Advice 通知
常见的Advice有before、after、round;在切入点织入代码,需要确定在方法执行之前(before)、方法执行之后(after)还是完全替代方法中的代码(round);
当然除了在方法中注入代码,也可以对其他代码做修改,例如对一个类修改,增加属性或者接口
3 连接点 Joint Point
通常代表一个方法调用,或者方法入口,例如run方法;
怎么这么一看,切入点和连接点怎么有点儿一样?其实不然,切入点只是告诉AspectJ我要从这个方法开始入手,就像切一块蛋糕,我要从哪里开始切;而连接点则是真正拿到了这个方法,就是切下的那块蛋糕,至于我要怎么处理这块蛋糕,就是Advice要做的
首先,项目中集成AspectJ
## 工程 build.gradle
classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10"
//这里注意,需要在仓库中配置jcenter(),否则编译不过
//在使用AspectJ的模块下配置依赖
## aop # build.gradle
api 'org.aspectj:aspectjrt:1.9.5'
如果某个模块需要AspectJ织入代码,就需要依赖AspectJ这个模块,并引入android-aspectjx这个插件
## app # build.gradle
plugins {
id 'com.android.application'
id 'android-aspectjx'
}
dependencies{
implementation project(':aop')
}
接下来,简单实现一个功能,就是监测方法的执行时间
public void getDuration(){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
正常统计耗时,就像文章开头那样,利用结束时间 - 开始时间得到耗时,那么这是一个手段,如果在其他页面中,存在成百上千的方法需要统计,问题就来了,重复的样板代码会令人抓狂的。
那么AspectJ怎么做的,怎么解放我们的双手?
既然我们想通过AspectJ织入一部分代码,那么就需要一个织入类,通过@Aspect注解来实现
/**
* 织入类
*/
@Aspect
public class MethodDurationAspectJ {
}
我们知道,我们写的所有.java类,都会通过javac编译成.class文件;但是如果使用了@Aspect注解,就不会通过javac编译,而是ajc编译期会遍历所有的java类,判断是否标注了@Aspect注解
在声明了织入类之后,那么接下来就是要找到这个切入点,以便织入代码;那么AspectJ提供了切入点注解@Pointcut
@Pointcut("execution(public void com.take.demo02.MainActivity.getDuration(..))")
public void cutMethod(){
}
@Pointcut可以传参数,以便获取方法或者执行方法,对于AOP的语法,大家可以去看附录,这里先来简单的精准匹配,我就拿MainActivity下的getDuration方法
Advice的作用就是在切入点织入代码,我们之前已经定义了切入点,而且拿到了待织入代码的方法
@Around("cutMethod()")
public void invoke(ProceedingJoinPoint joinPoint){
}
通过@Around注解,把切点交给了它,由@Around注解来决定怎么处理这个方法,同样也是需要放在一个方法上,这个方法可以传入一个参数ProceedingJoinPoint,这个参数其实就是切入点的方法(MainActivity中的getDuration方法)
@Around("cutMethod()")
public void invoke(ProceedingJoinPoint joinPoint){
Log.e("TAG","around 切入");
long startTime = System.currentTimeMillis();
//执行切入点的方法
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
long endTime = System.currentTimeMillis();
Log.e("TAG","耗时 -- "+(endTime - startTime));
}
这是一种方式,通过自己手动配置切入点,当调用这个方法的时候,就会注入我们自己写的代码
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//调用方法,代码织入
getDuration();
}
耗时 -- 3081
首先声明一个注解,当前注解放在方法上,然后方法在执行时织入代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS) //要在编译期
public @interface ExecuteDuration {
}
相对应的切入点就方便好多了,如下
@Pointcut("execution(@com.take.aop.annatation.ExecuteDuration * * (..))")
public void cutMethod(){
}
就是被@ExecuteDuration标注的任意返回值、任意参数的所有方法(这里需要注意,如果注解不是跟织入类在同一级目录下,就要使用全类名)
@ExecuteDuration
public void getDuration(){
int a = 10;
int b = 9;
int c = a + b;
Log.e("TAG","c ="+c);
}
E/TAG: around 切入
E/TAG: c =19
E/TAG: 耗时 -- 0
在之前我们使用到的是Around注解,Around是拿到方法内容,然后织入自己的代码,而且这里需要注意的是,必须要要调用JoinPoint的proceed(),否则即使在代码中调用了这个方法,这个方法也不会执行
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//如果没有调用proceed方法,即便这个调用也不会执行
getDuration();
}
@Before通常是放在方法之前之前的逻辑处理,例如判断当前是否登录,或者权限校验,判断是否获取某个权限,这个方法不能传入ProceedingJoinPoint会报错,通常做一些前置处理
@Before("cutMethod()")
public void before(){
Log.e("TAG","权限获取判断");
}
@Around("cutMethod()")
public void around(ProceedingJoinPoint joinPoint){
Log.e("TAG","执行权限处理");
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
E/TAG: 权限获取判断
E/TAG: 执行权限处理
E/TAG: c =19
@After就是在方法执行完成之后,跟@Before是一样
如果使用AspectJ,那么对于AOP的语法是需要了解的
在获取切入点方法时,需要声明切入点方法的返回值以及参数,这里是有一套匹配规则的
execution(<注解> <修饰符> <返回值类型> <类型声明>.<方法名>(参数列表) <异常列表>)
call(<注解> <修饰符> <返回值类型> <类型声明>.<方法名>(参数列表) <异常列表>)
这里execution和call都能拿到方法,区别在于execution是方法被执行,call是调用方法
"execution(public void com.take.demo02.MainActivity.getDuration(Context))"
这种方式属于精准匹配,明确了方法getDuration是在MainActivity方法下,入参为Context,返回值是public void,那么这种情况下,切入点就变得唯一了
"execution(* com.take.demo02.MainActivity.getDuration(Context))"
如果返回值变为*,这个是AOP语法中的通配符,代表是任意返回值
"execution(* com.take.demo02.MainActivity.getDuration(..))"
如果入参变为 … ,那么就代表任意类型的入参,这种情况下,MainActivity下所有的getDuration方法都会被拿到
"execution(* com.take.demo02.MainActivity.*(..))"
如果没有声明方法名称,那么在MainActivity下的所有方法,都会被拿到
"execution(* *..Activity+.getDuration(..)) && within("com.take.aop.*")"
这里没有声明Activity的确切类型,也就是说任意Activity,这里还有一个通配符+,代表其子类,getDuration方法都会被拿到,但是这里有个约束条件,就是得在com.take.aop包下