Android AOP编程实践

简介

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编译器已经将我们抽取出来的代码,重新拷贝到方法中了。

你可能感兴趣的:(Android AOP编程实践)