Android 开发中使用 AOP

原创 2017年03月19日 14:19:44

大家对AOP应该都不陌生, 就算没有用过也肯定听说过. 用过或了解过Java AOP的同学应该都知道AspectJ的大名. 因为AspectJ与java程序完全兼容,几乎是无缝关联, 所以只需要做一些简单的AJC适配就可以用在Android开发中. AspectJ用法简单容易上手, 不像之前说过的ASM那样有陡峭的学习曲线, 需要了解Java的字节码指令才能做代码注入. 同时AspectJ也很强大, 在AOP方面AspectJ完全可以满足开发需求. 这篇博客会介绍AspectJ的基本术语和用法, 在Android中适配AJC和简单AOP应用, 以及AJC编织之后的反向分析. 之后有机会的话会写一些结合Android和AOP实际用法例如日志管理, 权限管理, 性能检测, 异常管理或统计埋点的库.

代码地址 https://github.com/HiJesse/Android-AOP

AspectJ基础概念

  • Aspect 切面

    切面是切入点和通知的集合.

  • PointCut 切入点

    切入点是指那些通过使用一些特定的表达式过滤出来的想要切入Advice的连接点.

    表达式类型 描述
    execution 过滤出方法执行时的连接点
    within 过滤出制定类型内方法
    this 过滤当前AOP对象的执行时方法
    target 过滤目标对象的执行时方法
    args 过滤出方法执行时参数匹配args的方法
    @within 过滤出持有指定注解类型内的方法
    @target 过滤目标对象持有指定注解类型的方法
    @args 过滤当前执行的传入的参数持有指定注解的方法
    @annotation 过滤当前执行的持有指定注解的方法
    匹配语法 描述
    * 匹配任何数量字符
    .. 匹配任何数量字符
    匹配任何数量子包
    匹配任何数量参数
    + 匹配指定类型的子类型
  • Advice 通知

    通知是向切点中注入的代码实现方法, 即下面例子中的两个Triggered方法. 根据注入的位置和运行注入代码的时机分为五种Advice.

    通知类型 描述
    Before 前置通知, 在目标执行之前执行通知
    After 后置通知, 目标执行后执行通知
    Around 环绕通知, 在目标执行中执行通知,
    控制目标执行时机
    AfterReturning 后置返回通知, 目标返回时执行通知
    AfterThrowing 异常通知, 目标抛出异常时执行通知
  • Joint Point 连接点

    所有的目标方法都是连接点.

  • Weaving 编织

    主要是在编译期使用AJC将切面的代码注入到目标中, 并生成出代码混合过的.class的过程.

Android 适配AJC

AJC是可以编译AspectJ代码的编译器, 不确定全称是AspectJ compiler还是 AOP Java Compiler. 前面提到的Weaving, 就是使用AJC编译实现的. 想要在Android编译期间使用AJC还需要一些Gradle配置. 为了方便大家使用, 我就实现了一个基于AspectJ 1.8.9版本的Gradle插件. Gradle插件的实现之前有讲过-Gradle插件实现传送门. 之后将写好的插件部署到mavenCentral上了, 可以直接引用.

在根项目的build.gradle引入插件依赖.

buildscript {
    repositories {
        ...
        mavenCentral()
        ...
    }
    dependencies {
        ...
        classpath 'com.github.hijesse:android-aop:1.0.0'
        ...
    }
}

在需要使用AOP编译的Application或Library模块build.gradle文件中引入AOP插件

apply plugin: 'android-aop'

插件中实现接入AJC时首先判断一下当前apply插件的模块是否是Application或Library, 否则没有使用AJC的意义.

def hasApp = project.plugins.hasPlugin('com.android.application')
def hasLib = project.plugins.hasPlugin('com.android.library')
if (!hasApp && !hasLib) {
    throw new IllegalStateException("'android' or 'android-library' plugin required.")
}

根据不同的module获取对应的variants, 然后遍历.

final def log = project.logger
final def variants
if (hasApp) {
    variants = project.android.applicationVariants
} else {
    variants = project.android.libraryVariants
}

variants.all { variant ->
    ...
}

遍历Variant的过程中, 获取当前variant的Java编译环境. 获取到编译器的各个属性放在一个数组中.

JavaCompile javaCompile = variant.javaCompile
            javaCompile.doLast {
                String[] args = ["-showWeaveInfo",
                                 "-source", javaCompile.sourceCompatibility,
                                 "-target", javaCompile.targetCompatibility,
                                 "-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)

初始化AJC的MessageHandler用来处理所有的消息, 并结合之前拿到的JavaCompile信息启动AJC.

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;
    }
}

Android 中AOP 实现

Android中其实有很多可以使用AOP的场景, 例如日志管理, 权限管理, 性能检测, 异常管理和统计埋点等. 这篇文章只是简单得介绍AspectJ在Android中的用法, 在Activity的onCreate方法前后以及onResume方法之前打印日志, 之后可以发挥想象力玩出各种花样.

  1. 声明Aspect

    首先新建一个类, 在头部使用AspectJ注解声明这个类为切面.

    @Aspect
    public class ActivityAOP {
        ...
    }
  2. 声明PointCut

    在切面内部使用PointCut注解和上面章节讲到的语法声明两个切点.分别是Activity.onCreate切点.

    /**
     * activity onCreate point cut
     */
    @Pointcut("execution(* android.app.Activity.onCreate(..))")
    public void activityOnCreate() {
        //empty method body
    }

    和Activity.onResume切点.

    /**
     * activity onResume point cut
     */
    @Pointcut("execution(* android.app.Activity.onResume())")
    public void activityOnResume() {
        //empty method body
    }
  3. 实现Advice

    根据AspectJ基础概念中对Advice的介绍, 根据自己不同的需求选择不同的时机去调用对应的advice.像要在onCreate方法前后都打印日志, 那么我们就选择使用Around注解.

    根据参数中的ProceedingJointPoint获取到当前被切入类的类名和被切入方法的方法签名信息, 加上before注释打印. 调用JointPoint对象的proceed方法执行被切入方法. 执行完业务方法之后再打印带有after注释的日志.

    @Around("activityOnCreate()")
    public void activityOnCreateTriggered(ProceedingJoinPoint joinPoint) throws Throwable{
        String targetClassName = joinPoint.getTarget().getClass().getName();
        String signatureName = joinPoint.getSignature().getName();
        Log.d(TAG, targetClassName + " " + signatureName + " before");
        joinPoint.proceed();
        Log.d(TAG, targetClassName + " " + signatureName + " after");
    }

    而对onResume方法的处理只需要使用Before注解, advice的实现同上根据JoinPoint 获取到当前被切入类的类名和被切入方法的方法名, 加上before注释并打印出来.

    @Before("activityOnResume()")
    public void activityOnResumeTriggered(JoinPoint joinPoint) {
        String targetClassName = joinPoint.getTarget().getClass().getName();
        String signatureName = joinPoint.getSignature().getName();
        Log.d(TAG, targetClassName + " " + signatureName + " before");
    }

    根据logcat的日志输出可以看到AOP实现了我们想要的结果, 在onCreate方法前后和onResume方法之前加入了日志.

AOP Weaving

经过上面一节的三个步骤我们已经在Android中实现了AOP. 但是AJC将切面的代码织入到目标中之后是如何修改目标行为的? 猜测应该是AspectJ在编译的过程中根据切面切点和Advice增加目标的行为, 简单理解就是根据声明插入一些AspectJ生成的和Advice代码到目标当中, 以达到新增目标行为的目的. 具体是什么样子的我们还是反向一下代码.

先反编译一下声明的切面类看看有什么变化. 这里为了简洁就只贴出跟Weaving相关的并且是AspectJ自己新增的代码. 整理之后发现新增了一个类的静态实例, 通过静态块在类加载的时候调用postClinit方法对类的静态实例进行初始化. 同时提供了一个对外暴露的aspectOf方法, 该方法会返回切面对象的实例, 如果对象实例为null 则抛出异常.

根据这些新增的代码不难猜出AspectJ应该是要让外部能够通过aspectOf方法获取当前切面的实例, 然后就可以调用我们编写的advice.

public class ActivityAOP {

    public static final ActivityAOP ajc$perSingletonInstance;

    private static void ajc$postClinit() {
        ajc$perSingletonInstance = new ActivityAOP();
    }

    public static ActivityAOP aspectOf() {
        if(ajc$perSingletonInstance == null)
            throw new NoAspectBoundException("cn.jesse.aop.ActivityAOP", ajc$initFailureCause);
        else
            return ajc$perSingletonInstance;
    }

    static {
        try {
            ajc$postClinit();
        }
        catch(Throwable throwable) {
            ajc$initFailureCause = throwable;
        }
    }
}

然后反向切面的目标类MainActivity, 先分析一下共同的东西. 类中新增了两个StaticPart静态对象, 并且在类装载的时候调用preClinit方法, 通过AspectJRT中的工厂方法和一些静态的方法签名信息对着两个StaticPart对象进行初始化. 从方法的静态签名信息中可以看出这两个静态属性对应我们在AOP中使用的到两个JointPoint中的一些信息.

public class MainActivity extends AppCompatActivity {
    ...
    private static final org.aspectj.lang.JoinPoint.StaticPart ajc$tjp_0;
    private static final org.aspectj.lang.JoinPoint.StaticPart ajc$tjp_1;

    private static void ajc$preClinit() {
        Factory factory = new Factory("MainActivity.java", cn/jesse/aop/MainActivity);
        ajc$tjp_0 = factory.makeSJP("method-execution", factory.makeMethodSig("4", "onCreate", "cn.jesse.aop.MainActivity", "android.os.Bundle", "savedInstanceState", "", "void"), 12);
        ajc$tjp_1 = factory.makeSJP("method-execution", factory.makeMethodSig("4", "onResume", "cn.jesse.aop.MainActivity", "", "", "", "void"), 19);
    }

    static {
        ajc$preClinit();
    }
    ...
}

接下来看被切入的onResume方法有什么变化. 从结果上看来这种Before和After类型Advice对目标的侵入还是比较简单的, 只是在原来的目标实现前面或后面加了两行代码. 还是使用工厂方法根据初始化过方法签名的StaticPart对象tjp_1和目标对象的引用创建出连接点. 再通过上面反向过的切面对象中的aspectOf方法将JoinPoint对象传递给onResume切点的Advice实现中. 最终达到先执行Before类型的Advice, 然后再执行onResume代码的效果.

public class MainActivity extends AppCompatActivity {
    ...
    protected void onResume() {
        JoinPoint joinpoint = Factory.makeJP(ajc$tjp_1, this, this);
        ActivityAOP.aspectOf().activityOnResumeTriggered(joinpoint);
        super.onResume();
        Log.d(TAG, "onResume");
    }
    ...
}

而使用Around Advice的侵入就比较复杂了. 修改了onCreate方法后不仅多出了一个静态方法, 还多了一个继承自AroundClosure的内部类. 还是一步一步分析, 先看新增的静态方法. 从代码实现可以看出该方法的实现就是侵入之前onCreate方法中的实现. 看来是把onCreate方法的代码给转移出来, 方便JoinPoint调用.

public class MainActivity extends AppCompatActivity {
    ...
    static final void onCreate_aroundBody0(MainActivity mainactivity, Bundle bundle, JoinPoint joinpoint) {
        mainactivity.AppCompatActivity.onCreate(bundle);
        mainactivity.setContentView(0x7f04001b);
        Log.d(TAG, "onCreate");
    }
    ...
}

看到新增的内部类就明白了, 在内部类的run方法中调用了上面的静态方法. 从AspectJRT的源码中可以了解到, 在调用JointPoint的proceed方法时其实就是调用的这个内部类对象的run方法, 从而执行onCreate方法中原本的代码.

public class MainActivity extends AppCompatActivity {
    ...
    private class AjcClosure1 extends AroundClosure {

        public Object run(Object aobj[]) {
            aobj = super.state;
            MainActivity.onCreate_aroundBody0((MainActivity)aobj[0], (Bundle)aobj[1], (JoinPoint)aobj[2]);
            return null;
        }

        public AjcClosure1(Object aobj[]) {
            super(aobj);
        }
    }
    ...
}

回到onCreate方法, 方法的实现已经被改的面目全非. 原本的实现已经被转移到了对应的静态方法中, 而新的实现就是将AjcClosure1内部类和JoinPoint联系起来构建出ProceedingJoinPoint对象, 并且调用onCreate方法的Advice实现方法. 在Advice中决定何时调用onCreate方法原本的实现.

public class MainActivity extends AppCompatActivity {
    ...
    protected void onCreate(Bundle bundle) {
        JoinPoint joinpoint = Factory.makeJP(ajc$tjp_0, this, this, bundle);
        ActivityAOP.aspectOf().activityOnCreateTriggered((new AjcClosure1(new Object[] {
            this, bundle, joinpoint
        })).linkClosureAndJoinPoint(0x11010));
    }
    ...
}

转载请注明出处:http://blog.csdn.net/l2show/article/details/63684383

深入理解Android之AOP

深入理解Android之AOP格式更加精美的PDF版请到:http://vdisk.weibo.com/s/z68f8l0xTgCLK 下载一、闲谈AOP大家都知道OOP,即ObjectOriente...
  • Innost
  • Innost
  • 2015年10月24日 19:48
  • 47657

使用AspectJ在Android中实现Aop

开题上一篇文章Android Aop预研中介绍了Aop的各种实现方式,并且在最后提到,选择AspectJ作为合适的开发方式。这篇文章通过我自己编写的一个例子,来说明AspectJ的使用。首先要声明,使...
  • kangaroo835127729
  • kangaroo835127729
  • 2016年07月24日 16:36
  • 10148

Android进阶系列之AOP面向切面编程

Android的博大精深,不是一言两语能够说明道清的,它的魅力只有亲身去接触才能体会。就像美女一样 我先在这里申明一下,我这篇AOP只是学习笔记,适合入门级选手,如果阁下是大神想要参考AOP,那么...
  • sw5131899
  • sw5131899
  • 2016年12月26日 17:30
  • 4482

Android 基于AOP监控之——AspectJ使用指南

AspectJ的使用核心就是它的编译器,它就做了一件事,将AspectJ的代码在编译期插入目标程序当中,运行时跟在其它地方没什么两样,因此要使用它最关键的就是使用它的编译器去编译代码ajc。ajc会构...
  • woshimalingyi
  • woshimalingyi
  • 2016年05月27日 22:01
  • 8309

Android AOP实现原理全解析

深入理解Android之AOP
  • sinat_22657459
  • sinat_22657459
  • 2016年12月25日 15:15
  • 534

Android中的AOP

各位同学,目前已经转入至简书,欢迎来搞事情简书地址: http://www.jianshu.com/users/99f514ea81b3/timeline说一件开心的事情,我的这篇文章将被刊登在 鸿洋...
  • sinat_15877283
  • sinat_15877283
  • 2017年03月10日 11:35
  • 396

Android中的AOP编程之AspectJ实战实现数据埋点

文章背景 最近在给某某银行做项目的时,涉及到了数据埋点,性能监控等问题,那我们起先想到的有两种方案,方案之一就是借助第三方,比如友盟、Bugly等,由于项目是部署在银行的网络框架之内的,所以该方案不可...
  • XiNanHeiShao
  • XiNanHeiShao
  • 2017年07月01日 22:55
  • 2479

Android Aop预研

预研目的公司要求,希望我整理一下项目中的log日志,由于当前项目已经很庞大,包含多个自主开发的library,并且由多个开发人员共同完成。不同的同事,打log的方式都不一样,没有同一个的格式,因此我探...
  • kangaroo835127729
  • kangaroo835127729
  • 2016年07月24日 15:26
  • 3513

Android AOP 总结

AndroidAOP 总结 一、AOP 1.1 什么是AOP AOP,AspectOriented Programming 面向切面编程 OOP,Object-orientedprogramming面...
  • chenzhiqin20
  • chenzhiqin20
  • 2017年03月16日 21:24
  • 614

AOP在Android中的应用

官网地址:http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/ OOP (面向对象编程)针对业务...
  • huohacker
  • huohacker
  • 2017年02月23日 14:45
  • 324
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Android 开发中使用 AOP
举报原因:
原因补充:

(最多只允许输入30个字)