AOP
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
Android开发中使用AspectJ来实现AOP编程。AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器(ajc编译器)用来生成遵守Java字节编码规范的Class文件。AspectJ是对AOP编程思想的一个实践,当然,除了AspectJ以外,还有很多其它的AOP实现,例如ASMDex,但目前最好、最方便的,依然是AspectJ。
基础概念
- Aspect 切面:切面是切入点和通知的集合。
- PointCut 切入点:切入点是指那些通过使用一些特定的表达式过滤出来的想要切入Advice的连接点。
- Advice 通知:通知是向切点中注入的代码实现方法。
- Joint Point 连接点:所有的目标方法都是连接点.
- Weaving 织入:在编译期使用AJC编译器将切面的代码注入到目标中, 并生成出代码混合过的.class的过程.
需求描述
平时开发时候有时候需要打印出某个方法执行时长,一般都是在方法开始时记录一下时间,在方法结束时候记录时间,然后两者相减计算运行时长。但是如果有很多方法都需要打印时间,这种方法就需要在每个方法中添加代码,我们就得重复很多次这样的体力劳动,同样的情景也出现在权限判断、数据收集、登录判断等等。那么有没有什么方法能够优雅的实现我们的需求呢,当然是有的,那就是使用AOP思想。下面我们就针对打印方法时长来写一个demo,初步认识AOP。
配置
第一步肯定是配置环境了。
1.先在项目的build.gradle的dependencies中添加:
dependencies {
classpath "com.android.tools.build:gradle:4.1.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//添加引用
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'
}
hujiang对AspectJ的二次封装,原生的需要自己去配置,太过麻烦容易出错,所以建议直接使用hujiang。注意如果你的Gradle是3.6.1以上的hujiang需要使用2.0.10,网上大部分都是用的2.0.08版本,会报下面的错误:
Cannot cast object ‘com.android.build.gradle.internal.pipeline.TransformTask$2$1@f074159’ with class ‘com.android.build.gradle.internal.pipeline.TransformTask$2$1’ to class ‘com.android.build.gradle.internal.pipeline.TransformTask’
2.在module的build.gradle开头加入插件
apply plugin: 'com.hujiang.android-aspectjx'
3.然后还需要添加依赖
implementation 'org.aspectj:aspectjrt:1.8.+'
然后就可以Sync Now了。
实现
首先需要考虑怎么让代码知道我要在哪个方法中打印时间,也就是怎么确定切入点。这里我使用的是注解,当然直接指定报名类名加方法也可以找到切入点,但是不够灵活。所以首先定义一个注解RunTime:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RunTime {
}
因为需要加到方法上面所以Target很明显是METHOD,它要在运行时起作用所以Retention是RUNTIME。
然后创建一个Activity,布局就不说了只有一个button,点击会调用startRun方法
public class AOPActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a_o_p);
}
public void onClick(View view) {
startRun();
}
@RunTime
public void startRun(){
Log.d("gsy","click startRun");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然后创建一个处理切点的类,RunTimeInfo :
import android.util.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.reflect.Method;
/**
* 定义一个织入类 把这个类交给AspectJ的编译器来编译
*/
@Aspect
public class RunTimeInfo {
//定义切点 然后来进行匹配 类似于一种正则规则
@Pointcut("execution(@honeywell.com.androidstudy.aop.RunTime * *(..))")
public void logRunTime(){}
@Around("logRunTime()")
public void executioonLogRunTime(ProceedingJoinPoint proceedingJoinPoint){
//记录开始时间
long startTime = System.currentTimeMillis();
//获取到方法的反射对象
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
//获取到方法
Method method = signature.getMethod();
try {
//执行原来的方法
proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//记录结束时间
long endTime = System.currentTimeMillis();
Log.d("gsy",method.getName()+" runtime :"+(endTime - startTime));
}
}
使用Aspect的编译器编译必须给类打上标注,@Aspect。
@Pointcut注解,就是切点,即触发该类的条件。取值如下:
execution,也就是以方法执行时为切点,触发Aspect类。而execution里面的字符串是触发条件,也是具体的切点。@honeywell.com.androidstudy.aop.RunTime * *(…)的意思就是带有RunTime注解的 任意类型 任意返回类型 任意参数 的方法,这里怎么写就看你的需求了。
@Around是指环绕通知,另外还有@Before、@After等:
类型 | 描述 |
---|---|
@Before | 前置通知, 在目标执行之前执行通知 |
@After | 后置通知, 目标执行后执行通知 |
@Around | 环绕通知, 在目标执行中执行通知, 控制目标执行时机 |
@AfterReturning | 后置返回通知, 目标返回时执行通知 |
@AfterThrowing | 异常通知, 目标抛出异常时执行通知 |
不同的类型对应不同的需求,比如如果要记录按钮点击次数,那可以用Before或者After,而记录时间需要在方法前后都要执行,所以Around更适合。使用Around时要记得调用ProceedingJoinPoint的proceed()方法,否则原来的方法就不会执行了。
运行结果
当点击按钮后通过logcat看到如下结果
12-29 16:54:08.327 14968 14968 D gsy : click startRun
12-29 16:54:11.327 14968 14968 D gsy : startRun runtime :3000
打印了方法名和运行时间,如果有多个方法需要打印,通过AOP我们只需要在方法上面加一个@RunTime注解就行了,是不是非常方便。鉴于代码比较简单,demo就不上传了。我曾经自己通过AspectJ封装了一个Android权限请求的工具,找时间上传上来请大家指点指点。
参考如下文章,感谢作者:
Android AOP面向切面编程详解
Android AOP之Aspect简单使用