Android 开发中使用 AOP

大家对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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值