架构之路

Android面向切面编程AOP详解

2018-01-09  本文已影响36人  apkcore

为了吸引大家看下去,先举一个使用场景

App 发版之前,都要跟数据分析师一起过一下看看哪些地方需要进行埋点。发版在即,添加代码会非常仓促,还需要安排人手进行测试。而且埋点的代码都很通用,使用@HookTrace 这个注解。它可以在调用某个方法之前、以及之后进行hook。可以单独使用也可以跟任何自定义注解配合使用。


安掌门面向切面编程.jpg
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "AOP";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @HookTrace(beforeMethod = "method1",afterMethod = "method2")
    public void doClick(View view){
        Log.d(TAG,"doClick()");
    }

    private void method1() {
        Log.d(TAG,"method1() is called before initData()");
    }

    private void method2() {
        Log.d(TAG,"method2() is called after initData()");
    }
  }

点击事件触发后,

01-08 17:12:54.394 2764-2764/com.apkcore.aopdemo D/AOP: method1() is called before initData()
01-08 17:12:54.394 2764-2764/com.apkcore.aopdemo D/AOP: doClick()
01-08 17:12:54.402 2764-2764/com.apkcore.aopdemo D/AOP: method2() is called after initData()

是不是很神奇?你也可以做到。

AOP的出现

大家都知道,我们现在软件编程普通采用的是OOP思想。
OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。.

最简单的例子,对于一个客户这样一个业务实体封装,自然是OOP的任务,面向对象的特点是继承、多态和封装。为了符合单一职责的原则,OOP将功能分散到不同的对象中去。让不同的类设计不同的方法,这样代码就分散到一个个的类中。可以降低代码的复杂程度,提高类的复用性。

那么假设我们要给不同的客户不同的权限,不同的权限能看到不同有页面,比如,一般用户能看到A,高级会员看到A,B,管理员能看到A,B,C三个页面,如果以OOP的写法,封装一个判断权限的工具类,在每个都要加上判断,这样会造成一个问题,如果哪天权限判断的方法有了改变,所有有使用的模块都要跟着变,而且很明显的违反了OOP的单一使用原则。

这个时候就能看到AOP的好处了,AOP可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能的一种技术,把散落在程序中的公共部分提取出来,做成切面类,这样的好处在于,代码的可重用,一旦涉及到该功能的需求发生变化,只要修改该代码就行,否则,你要到处修改,如果只要修改1、2处那还可以接受,万一有1000处呢。

AOP一般使用场景

AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理,拦截分发,Hook等等。

AOP的使用

BB了一大堆理论知识,其实我也绕晕了,上正菜~

AOP在android中的使用早已有前人种树,我们只要站在巨人的肩膀上,AOP在eclipse中的使用比较的复杂,因为现在大部分android程序员都已经迁移到了android studio中了,就不讨论eclipse使用aop了,如果有要使用eclipse的童鞋,请参考(https://www.jianshu.com/p/f7a587f88dff)

现在使用as来开发aop吧,as的配置非常简单

这里我们使用的是aspectjrt

gradle(app)做如下配置

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

apply plugin: 'com.android.application'

android {
    ...
}
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;
            }
        }
    }
}

dependencies {
    ...
    implementation 'org.aspectj:aspectjrt:1.8.13'
}

目前最新的版本是1.8.13,gradle已经配置完毕,环境就已经搭好了(前人栽树~)。

先抛出问题:如何使用AOP实现一个用户行为统计,
比如用户在微信摇一摇的功能中用了多少时间

OOP的写法:

public void doClick(View view) {
    long begin = System.currentTimeMillis();
    SystemClock.sleep(3000);
    Log.d(TAG, "doClick()");
    Log.d(TAG, (System.currentTimeMillis() - begin) + "ms");
}

如果我要在漂流瓶也要做这样的统计,那么几乎一样的代码我还是要写一遍,这时就看出了AOP的好处了,下面我们使用AOP来实现这个简单的功能。

首先自定义一个注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BehaviorTrace {
    String value();

    int type();
}

对注解概念不了解的可以先看这个:Java注解基础概念总结
前面有提到注解按生命周期来划分可分为3类:

  1. RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
  2. RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
  3. RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
    这3个生命周期分别对应于:Java源文件(.java文件) ---> .class文件 ---> 内存中的字节码。

那怎么来选择合适的注解生命周期呢?
首先要明确生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解(运行时代码中需要用到);如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife)(只生成辅助代码),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解(标识)。
然后新建BehaviorAspect类来实现业务逻辑

@Aspect
public class BehaviorAspect {
    private static final String TAG = "AOP";
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 切点
     */
    @Pointcut("execution(@com.apkcore.aopdemo.BehaviorTrace * *(..))")
    public void annoBehavior() {

    }

    @Around("annoBehavior()")
    public Object dealPoint(ProceedingJoinPoint point) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        BehaviorTrace trace = methodSignature.getMethod().getAnnotation(BehaviorTrace.class);
        String content = trace.value();
        int type = trace.type();
        long begin = System.currentTimeMillis();
        Log.d(TAG, content + " dealPoint begin: " + simpleDateFormat.format(new Date()));
        Object object = point.proceed();
        Log.d(TAG, content + " dealPoint end: " + (System.currentTimeMillis() - begin) + "ms");
        return object;
    }
}

稍微解释一下,@Aspect修饰类的, '@Pointcut("execution(@com.apkcore.aopdemo.BehaviorTrace * (..))")`表示一个切点,这个切点就是我们自定义的注解标识,其中 *(..)表示BehaviorTrace这个接口的任意方法的执行。这个方法只表示切点,无实现,方法名随便定义。

'@Around("annoBehavior()")'这个方法的参数是固定的,ProceedingJoinPoint已经为我们封装好了。
通过point.getSignature();获取到方法签名,通过这个签名我们能得到注解类BehaviorTrace,可以得到他里面我们定义的value与type;point.proceed();就是执行用@BehaviorTrace修饰的方法。

doClick的调用是这样的

@BehaviorTrace(value = "测试", type = 1)
public void doClick(View view) {
    SystemClock.sleep(3000);
    Log.d(TAG, "doClick: ");
}

测试一下

01-08 18:20:16.849 14350-14350/com.apkcore.aopdemo D/AOP: 测试 dealPoint begin: 2018-01-08 18:20:16
01-08 18:20:19.850 14350-14350/com.apkcore.aopdemo D/AOP: doClick:
01-08 18:20:19.850 14350-14350/com.apkcore.aopdemo D/AOP: 测试 dealPoint end: 3002ms

可以看到我们成功实现了aop切面编程.
如果有兴趣的童鞋,可以看一下它编译出来的class文件

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "AOP";

    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(2131296283);
    }

    @BehaviorTrace(
        value = "测试",
        type = 1
    )
    public void doClick(View view) {
        JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, view);
        BehaviorAspect var10000 = BehaviorAspect.aspectOf();
        Object[] var4 = new Object[]{this, view, var3};
        var10000.dealPoint((new MainActivity$AjcClosure1(var4)).linkClosureAndJoinPoint(69648));
    }

    static {
        ajc$preClinit();
    }
}

可以看到已经自动帮我们把doClick中的方法链接到了Aspect中,其实是aspectjrt已经取代了默认的javac编译出class文件,不过是我们使用了大佬们的gradle配置,已经配置好了,这也是eclipse为什么配置要稍微麻烦一些的原因.

发散思维

通过上面的学习,我们基本会使用AOP编程了,就是这么so easy,那么它能给我们带来什么便利呢?
其实从BehaviorAspectpublic Object dealPoint(ProceedingJoinPoint point)方法中Object obj = point.getThis();我们就能得到这个对象,对象都得到了,还有什么是不能解决的。

登录拦截

这里不得不引用一下 android 关于先登录成功后再进入目标界面的思考(https://juejin.im/post/5a2d23ed51882531ea653578)这篇文章,有时间的朋友可以一看,里面的设计思路还是挺不错的,不过实现方法,我们可以用上一篇文章讲的和今天的结合起来,用一个不可见的fragment+AspectJ,最后使用的方法简单的不能在简单了,只是在方法上加上注解,就能处理像权限申请、登录状态。。类似的前置条件功能

简单的登录验证

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginTrace {
}
@Aspect
public class LoginAspect {

    @Pointcut("execution(@com.apkcore.aopdemo.login.LoginTrace * *(..))")
    public void onLoginMethod(){
    }

    public Object doLoginMethod(ProceedingJoinPoint joinPoint)throws  Throwable{
//        if (!已经登录){
//            跳转登录页面
//            return null;
//        }

        return joinPoint.proceed();
    }

如果我们如上一篇无界面Fragment绑定数据所讲的一样,当这个@LoginTrace放在一个fragment的方法中,并在其中处理好逻辑,在需要的时候activity/fragment add这个fragment,远比该作用使用得更为简单,有兴趣的小伙伴可以自己实现一下。

安全控制

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface SafeTrace {
}

@Aspect
public class SafeAspect {
    private static final String TAG = "AOP";

    @Pointcut("execution(@com.apkcore.aopdemo.save.SafeTrace * *(..))")
    public void onSafe() {
    }

    @Around("onSafe()")
    public Object doSafeMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        return safeMethod(joinPoint);
    }

    private Object safeMethod(ProceedingJoinPoint joinPoint) {
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            StringWriter stringWriter = new StringWriter();
            throwable.printStackTrace(new PrintWriter(stringWriter));
            Log.d(TAG, "safeMethod: " + stringWriter.toString());
        }
        return result;
    }
}

上述方法中,直接在Aspect中catch掉异常,保证程序不会崩溃

Hook埋点

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
public @interface HookTrace {
    String beforeMethod() default "";

    String afterMethod() default "";
}
@Aspect
public class HookAspect {
    private static final String POINTCUT_METHOD = "execution(@com.apkcore.aopdemo.hook.HookTrace * *(..))";
    private static final String POINTCUT_CONSTRUCTOR = "execution(@com.apkcore.aopdemo.hook.HookTrace *.new(..))";

    @Pointcut(POINTCUT_METHOD)
    public void methodOnHook() {
    }

    @Pointcut(POINTCUT_CONSTRUCTOR)
    public void constructorOnHook() {
    }

    @Around("methodOnHook() || constructorOnHook()")
    public Object doHook(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        HookTrace hookTrace = method.getAnnotation(HookTrace.class);

        String beforeMethod = hookTrace.beforeMethod();
        String afterMethod = hookTrace.afterMethod();
        if (!TextUtils.isEmpty(beforeMethod)) {
            //反射调用
            Class cls = joinPoint.getTarget().getClass();
            //getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。
            //getMethod*()获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。
            Method m = cls.getDeclaredMethod(beforeMethod);
            m.invoke(joinPoint.getTarget());
        }
        Object object = joinPoint.proceed();
        if (!TextUtils.isEmpty(afterMethod)) {
//            Class cls = joinPoint.getTarget().getClass();
//            Method m = cls.getDeclaredMethod(beforeMethod);
//            m.invoke(joinPoint.getTarget());
            Reflect.on(joinPoint.getTarget()).call(afterMethod);
        }
        return object;
    }
}

这就是文章最开头所使用的了,其实就是在doHook中根据hooktrace的两个方法名,决定前后执行顺序,使用反射的方法调用。

小结

Github
AOP让我们在代码使用的时候拥有了充分的自由性,是OOP非常好的补充,希望大家都能掌握并使用起来。

参考

无界面Fragment绑定数据
Android AOP 面向切面编程(一) AspectJ 处理网络错误
当RxJava遇到AOP
归纳AOP在Android开发中的几种常见用法
android 关于先登录成功后再进入目标界面的思考

End

下面的是我的公众号,欢迎大家关注我。

Apkcore.jpg
上一篇下一篇

猜你喜欢

热点阅读