简介
AOP(Aspect Oriented Programming)是面向切面编程,OOP(Object Oriented Programming)是面向对象编程,AOP一般在Java EE编程Spring框架用的比较多,我们平时接触的比较多OOP编程提倡的是将功能模块化,对象化(也就我们学java时讲的万物皆对象),而AOP的思想则是提倡针对同一类问题统一处理。
在Android中AOP用于用户行为统计、性能监控、日志埋点、异常处理、动态权限控制、甚至是代码调试等等
优点:利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。--百度百科
框架
AOP只是一种编程思想,实现它需要有以下几种(工具和库):
AspectJ: 一个 JavaTM 语言的面向切面编程的无缝扩展(适用Android)。
Javassist for Android: 用于字节码操作的知名 java 类库 Javassist 的 Android 平台移植版。
DexMaker: Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。
ASMDEX: 一个类似 ASM 的字节码操作库,运行在Android平台,操作Dex字节码。
本篇就以AspectJ为例编码
引入
eclipse
1、aspectj下载AspectJ(目前发布的最新版为1.9.2),双击下载下来的jar文件,完成AspectJ的安装;然后把AspectJ安装目录下的lib中的aspectjrt.jar复制到JRE安装目录下的lib\ext目录中。
至此,已经可以通过使用命令行的方式编写AspectJ程序了,AspectJ安装目录下的bin中的ajc为编译器(对应Java的javac)。
2、如果要使用Eclipse编写AspectJ程序,则需要为Eclipse安装AJDT插件,eclipse AJDT(根据自己Eclipse版本下载插件)
Android Studio
1.使用插件 gradle-android-aspectj-plugin github地址
2.自己配置 gradle,添加脚本
【注意】AspectJX是基于 gradle android插件1.5及以上版本设计使用的,如果你还在用1.3或者更低版本,请把版本升上去。
第一种方式:
1、项目根目录的build.gradle中增添依赖
repositories {
maven{ url'http://maven.aliyun.com/nexus/content/groups/public/'}
...
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
2、在dependencies下添加依赖
dependencies {
//aspectjrt的依赖
implementation 'org.aspectj:aspectjrt:1.8.9'
}
3、最后一步,在module的build.gradle里添加以下代码
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break;
case IMessage.WARNING:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
第二种方式:
1、项目根目录的build.gradle中增加依赖(阿里国内镜像地址)
repositories {
maven{ url'http://maven.aliyun.com/nexus/content/groups/public/'}
...
}
2、在项目根目录的build.gradle里依赖AspectJX
dependencies {
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
3、在app项目的build.gradle里应用插件
apply plugin: 'android-aspectjx'
4、在app项目的build.gradle的dependencies中添加
dependencies{
...
implementation 'org.aspectj:aspectjrt:1.8.9'
}
【注意】下载'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'半天都下载不来,最后改用国内镜像maven库。。。
实践
测试按钮布局
Activity中点击事件代码如下
/**
* 测试按钮1调用方法
*/
public void testBtn1(View view) {
long startTime = System.currentTimeMillis();
//每个方法内部具体执行内容
SystemClock.sleep(300);
Log.e(TAG, "测试1");
long endTime = System.currentTimeMillis();
Log.d(TAG, "总共耗时: " + (endTime - startTime));
}
/**
* 测试按钮2调用方法
*/
public void testBtn2(View view) {
long startTime = System.currentTimeMillis();
//每个方法内部具体执行内容
SystemClock.sleep(400);
Log.e(TAG, "测试2");
long endTime = System.currentTimeMillis();
Log.d(TAG, "总共耗时: " + (endTime - startTime));
}
/**
* 测试按钮3调用方法
*/
public void testBtn3(View view) {
long startTime = System.currentTimeMillis();
//每个方法内部具体执行内容
SystemClock.sleep(500);
Log.e(TAG, "测试3");
long endTime = System.currentTimeMillis();
Log.d(TAG, "总共耗时: " + (endTime - startTime));
}
【注意】这里为了演示效果,在主线程中做了一定的耗时操作
执行结果:
2019-03-04 00:31:33.410 10329-10329/com.android.aop E/MainActivity: 测试1
2019-03-04 00:31:33.410 10329-10329/com.android.aop D/MainActivity: 总共耗时: 301
2019-03-04 00:31:33.422 10329-10329/com.android.aop W/Looper: Slow Frame: doFrame is 310ms late
2019-03-04 00:31:35.182 10329-10329/com.android.aop E/MainActivity: 测试2
2019-03-04 00:31:35.182 10329-10329/com.android.aop D/MainActivity: 总共耗时: 401
2019-03-04 00:31:35.193 10329-10329/com.android.aop W/Looper: Slow Frame: doFrame is 400ms late
2019-03-04 00:31:37.537 10329-10329/com.android.aop E/MainActivity: 测试3
2019-03-04 00:31:37.537 10329-10329/com.android.aop D/MainActivity: 总共耗时: 501
2019-03-04 00:31:37.547 10329-10329/com.android.aop I/Choreographer: Skipped 30 frames! The application may be doing too much work on its main thread.
2019-03-04 00:31:37.556 10329-10329/com.android.aop W/Looper: Slow Frame: doFrame is 507ms late
如果以后要修改这个耗时统计逻辑,那么程序员的流水线模式就要打开了,由此可以看出代码冗余,修改一下耗时统计逻辑,需要修改三处地方,这种代码结构的问题也很明显。
在实际项目中,可能有多个模块都要添加耗时统计功能,那么我们就要再每个模块中修改代码,而且现实的业务逻辑可能较为复杂,这样的代码可读性极差。
而且在一个独立的代码块中添加了统计的功能,这显然不符合我们的OOP编程思想,所以在这里就可以使用面向切面的编程思想,将需要增加统计功能的代码块单独的切出来,再单独的操作它。
假如有三个方法其中代码逻辑上第二部分都是相同的,我们可以封装成一个工具方法,三个方法中如果需要修改第二部分,我们只要修改工具方法里面的代码就行了。
但是如果三个方法中第一部分和第三部分代码是相同的,而且还存在公用的变量,如果变量少的话我们也可抽出两个方法,如果变量多的话我们就要jj了。
AOP实现
首先我们使用自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeConsumingTrace {
//第一个值
int key();
//第二个值
String value();
}
【补充】
@Target(ElementType.TYPE) 作用于接口、类、枚举、注解
@Target(ElementType.FIELD) 作用于字段、枚举的常量
@Target(ElementType.METHOD) 作用于方法
@Target(ElementType.PARAMETER) 作用于方法参数
@Target(ElementType.CONSTRUCTOR) 作用于构造函数
@Target(ElementType.LOCAL_VARIABLE) 作用于局部变量
@Target(ElementType.ANNOTATION_TYPE)注解
@Target(ElementType.PACKAGE) 作用于包
@Retention(RetentionPolicy.SOURCE) 注解保留在源代码中,但是编译的时候会被编译器所丢弃。比如@Override, @SuppressWarnings
@Retention(RetentionPolicy.CLASS) 这是默认的policy。注解会被保留在class文件中,但是在运行时期间就不会识别这个注解。
@Retention(RetentionPolicy.RUNTIME) 注解会被保留在class文件中,同时运行时期间也会被识别。所以可以使用反射机制获取注解信息。比如@Deprecated
切面
切面类添加@Aspect注解
@Aspect
public class TimeConsumingAspect {
private final String TAG = "TimeConsumingAspect";
// Pointcut的功能 是从众多的 JoinPoint中找到指定的执行点;
@Pointcut("execution(@com.android.aop.TimeConsumingTrace * *(..))")
public void pointcut() {
}
@Before("pointcut()")
public void before(JoinPoint point) {
Log.d(TAG, "@Before");
}
@Around("pointcut()")
public void timeConsumingTrace(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//方法执行前
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
TimeConsumingTrace annotation = methodSignature.getMethod().getAnnotation(TimeConsumingTrace.class);
int key = annotation.key();
Log.d(TAG, "key: " + key);
String value = annotation.value();
Log.d(TAG, "value: " + value);
String name = methodSignature.getName();
Log.d(TAG, "执行方法: " + name);
long startTime = System.currentTimeMillis();
proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
Log.d(TAG, "总共耗时: " + (endTime - startTime));
}
@After("pointcut()")
public void after(JoinPoint point) {
Log.d(TAG, "@After");
}
@AfterReturning("pointcut()")
public void afterReturning(JoinPoint point, Object returnValue) {
Log.d(TAG, "@AfterReturning: " + returnValue);
}
@AfterThrowing(value = "pointcut()", throwing = "ex")
public void afterThrowing(Throwable ex) {
Log.e(TAG, "@afterThrowing");
Log.e(TAG, "ex = " + ex.getMessage());
}
}
【注意】,切点表达式使用注解,execution()是最常用的切点函数,其语法如下所示,整个表达式可以分为五个部分:
1、execution(): 表达式主体。
2、第一个 * 号:表示返回类型,* 号表示所有的类型。
3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.android.aop包、子孙包下所有类的方法。
4、第二个 * 号:表示类名,* 号表示所有的类。
5、* (..):最后这个星号表示方法名,* 号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
AOP基本概念
类型 | 描述 |
---|---|
@Aspect | 声明切面,标记类 |
@Pointcut(切点表达式) | 定义切点,标记方法 |
@before() | 前置通知, 在切点执行之前执行通知 |
@After() | 后置通知, 切点执行后执行通知 |
@Around() | 环绕通知, 在切点执行中执行通知, 控制目标执行时机 |
@AfterReturning() | 后置返回通知, 切点返回时执行通知 |
@AfterThrowing() | 异常通知, 切点抛出异常时执行通知 |
调用代码改造:
/**
* 测试按钮1调用方法
*/
@TimeConsumingTrace(value = "测试1", key = 1)
public void testBtn1(View view) {
//每个方法内部具体执行内容
SystemClock.sleep(300);
int i = 1/0;
Log.e(TAG, "testBtn1测试");
}
/**
* 测试按钮2调用方法
*/
@TimeConsumingTrace(value = "测试2", key = 2)
public void testBtn2(View view) {
//每个方法内部具体执行内容
SystemClock.sleep(400);
Log.e(TAG, "testBtn2测试");
}
@TimeConsumingTrace(value = "测试3", key = 3)
public void testBtn3(View view) {
//每个方法内部具体执行内容
SystemClock.sleep(500);
Log.e(TAG, "testBtn3测试");
}
正常执行结果:
03-06 07:38:04.835 15161-15161/? D/TimeConsumingAspect: @Before
03-06 07:38:04.836 15161-15161/? D/TimeConsumingAspect: key: 2
03-06 07:38:04.837 15161-15161/? D/TimeConsumingAspect: value: 测试2
03-06 07:38:04.837 15161-15161/? D/TimeConsumingAspect: 执行方法: testBtn2
03-06 07:38:05.237 15161-15161/? E/MainActivity: testBtn2测试
03-06 07:38:05.237 15161-15161/? D/TimeConsumingAspect: 总共耗时: 400
03-06 07:38:05.237 15161-15161/? D/TimeConsumingAspect: @After
03-06 07:38:05.237 15161-15161/? D/TimeConsumingAspect: @AfterReturning: null
异常执行结果:
03-06 07:40:50.356 15161-15161/? D/TimeConsumingAspect: @Before
03-06 07:40:50.357 15161-15161/? D/TimeConsumingAspect: key: 1
03-06 07:40:50.357 15161-15161/? D/TimeConsumingAspect: value: 测试1
03-06 07:40:50.357 15161-15161/? D/TimeConsumingAspect: 执行方法: testBtn1
03-06 07:40:50.657 15161-15161/? D/TimeConsumingAspect: @After
03-06 07:40:50.658 15161-15161/? E/TimeConsumingAspect: @afterThrowing
03-06 07:40:50.658 15161-15161/? E/TimeConsumingAspect: ex = divide by zero
其他参数获取:
String name = methodSignature.getName(); // 方法名:testBtn2
Log.d(TAG, "name: " + name);
Method method = methodSignature.getMethod(); // 方法:public void com.lqr.androidaopdemo.MainActivity.testBtn2(android.view.View)
Log.d(TAG, "method: " + method);
Class returnType = methodSignature.getReturnType(); // 返回值类型:void
Log.d(TAG, "returnType: " + returnType);
Class declaringType = methodSignature.getDeclaringType(); // 方法所在类名:MainActivity
Log.d(TAG, "declaringType: " + declaringType);
String[] parameterNames = methodSignature.getParameterNames(); // 参数名:view
Log.d(TAG, "parameterNames: " + parameterNames[0]);
Class[] parameterTypes = methodSignature.getParameterTypes(); // 参数类型:View
Log.d(TAG, "parameterTypes: " + parameterTypes[0]);
03-06 09:03:44.188 17833-17833/? D/TimeConsumingAspect: name: testBtn2
03-06 09:03:44.188 17833-17833/? D/TimeConsumingAspect: method: public void ?.MainActivity.testBtn2(android.view.View)
03-06 09:03:44.188 17833-17833/? D/TimeConsumingAspect: returnType: void
03-06 09:03:44.188 17833-17833/? D/TimeConsumingAspect: declaringType: class ?.MainActivity
03-06 09:03:44.189 17833-17833/? D/TimeConsumingAspect: parameterNames: view
03-06 09:03:44.189 17833-17833/? D/TimeConsumingAspect: parameterTypes: class android.view.View
AOP原理分析
我们用Android Studio打开目录路径如下:app->build->intermediates->classes->debug->com包下即是我们使用ajc编译后的class代码。
@TimeConsumingTrace(
value = "测试1",
key = 1
)
public void testBtn1(View view) {
View var3 = view;
JoinPoint var4 = Factory.makeJP(ajc$tjp_0, this, this, view);
try {
try {
TimeConsumingAspect.aspectOf().before(var4);
TimeConsumingAspect var10000 = TimeConsumingAspect.aspectOf();
Object[] var5 = new Object[]{this, var3, var4};
var10000.timeConsumingTrace((new MainActivity$AjcClosure1(var5)).linkClosureAndJoinPoint(69648));
} catch (Throwable var8) {
TimeConsumingAspect.aspectOf().after(var4);
throw var8;
}
TimeConsumingAspect.aspectOf().after(var4);
TimeConsumingAspect.aspectOf().afterReturning(var4, (Object)null);
} catch (Throwable var9) {
TimeConsumingAspect.aspectOf().afterThrowing(var9);
throw var9;
}
}
MainActivity$AjcClosure1类中的run方法testBtn1_aroundBody0是每个按钮各自需要实现的不同内容。
public class MainActivity$AjcClosure1 extends AroundClosure {
public MainActivity$AjcClosure1(Object[] var1) {
super(var1);
}
public Object run(Object[] var1) {
Object[] var2 = super.state;
MainActivity.testBtn1_aroundBody0((MainActivity)var2[0], (View)var2[1], (JoinPoint)var2[2]);
return null;
}
}
反编译class文件,我们可以看出ajc编译器已经将我们抽取出来的代码,重新拷贝到方法中了。