AOP(面向切面编程)在Andorid开发中的应用

AOP概念

在百度百科中对AOP的介绍如下:

  在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。这种在运行时,动态的将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

  面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。

  但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

  也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

  AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充,AOP的主要好处是对原有代码毫无入侵性,把和主业务无关的事情放到代码以外去做 ,AOP像OOP一样,只是一种编程范式,AOP并没有规定说,实现AOP协议的代码,要用什么方式去实现。这里举个基于代理模式的方式(基于动态代理的SpringAOP):
这里写图片描述

  AOP不一定都像Spring AOP那样,是在运行时生成代理对象来织入的,还可以在编译期、类加载期织入,比如AspectJ。
这里写图片描述

  这里列出了一些SpringAOP和AspectJ的区别,想了解更多请点击 此处

AOP在Android客户端(如网易新闻客户端)中的应用

  AOP有哪些能够解决我们痛点的使用场景的,下面简单列举一下在Android客户端中对AOP技术的应用

网易新闻热补丁技术

  网易新闻的Android客户端热更新技术使用的是AspectJ, AspectJ就是AOP技术的一种框架。详情网易新闻热补丁技术实践

检测方法耗时

  新闻客户端开发了一套能够根据指定的sdk进行排查方法耗时的工具,原理就是使用的AspectJ处理字节码包装方法。
方法耗时,这个其实Android上已经有一些现成的工具,比如trace view等等,这些工具都可以进行方法耗时的检测。但是痛点是这些工具使用起来都比较麻烦,效率低下, 而且无法针对某一个块代码或者某个指定的sdk进行查看方法耗时。

  我们为了能够提高客户端的FPS,其中有一个思路就是希望降低主线程方法耗时。 最初的思路就是使用trace View等工具进行排查。不用不知道一用你就会发现有多么的繁琐。于是我们希望能有一种方式能够快速打印出我们的方法耗时。
于是我们采用了AOP的技术,对每个方法做一个切点,在执行之后打印方法耗时。

  具体的实现原理和网易新闻热更新技术原理差不多,都是对方法做切点,注入一段自己逻辑,只不过注入的是计算方法耗时的逻辑而已。

  AOP不一定都像Spring AOP那样,是在运行时生成代理对象来织入的,还可以在编译期、类加载期织入,比如AspectJ。

编译完成之后使用AspectJ编译器处理字节码,两种方案

1.hook java compiler的方案
  直接hook java Compiler的Task,在java源码编译完成之后执行AspectJ的编译器,进行字节码插桩操作。

 project.android.applicationVariants.all { variant ->
            if (variant.buildType.name != "release") {
                log.debug("Skipping non-release build type '${variant.buildType.name}'.")
                return;
            }
            JavaCompile javaCompile = variant.javaCompile
            javaCompile.doLast {
                            String[] args = ["-showWeaveInfo",
                                             "-1.5",
                                             "-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);                    
                    }
                }
            }
        }

  此中方案的缺陷就是对参与编译过程的代码处理很简单,但是对于一些不参与编译过程的jar/aar等处理比较困难。

  1. gradle Transform API方案
      对于上述方法存在一些问题,对于一些jar包,其实已经是字节码了不会走这个过程,因此对一些jar的插桩操作不是很好处理。因此我们采用了Transform API的方案。 transform api是Android gradle plugin 1.5之后新api, 作用就是在生成dex之前,给开发者一个机会能够统一进行修改字节码, 这个过程中你能拿到所有的源码生成的class文件和jar/aar中的class文件。因此,此时处理能够满足我们对所有的class文件进行处理。 为了方便我们实现了一个gradle plugin 专门进行AspectJ处理class文件,完成字节码插桩操作。
      并且为了处理对第三方jar包的方法耗时最终,我们可以配置对指定的jar或者aar进行字节码插桩,查看方法耗时.
    如: 插件的配置如下,再buildType为debug的时,对AMap_Location jar包(高德地图sdk)进行字节码插桩
@Aspect
public class AspectJSpectControler {
    @Around(value = "execution(* com.netease.newsreader..*.*(..)")
    public Object weavePatchLogic(ProceedingJoinPoint joinPoint) throws Throwable {
        if (BuildConfig.DEBUG) { //debug    状态下计算方法耗时
            long startT = System.currentTimeMillis();
            Object proceed = joinPoint.proceed();
            long consume = System.currentTimeMillis() - startT;
            if (consume > 40 && Thread.currentThread().getId() == BaseApplication.getInstance().getMainThreadId()) { // 方法耗时大于40毫秒,并且当前线程是主线程,则直接打印当前方法的签名
                NeteaseLog.d(METHOD_TIME_TAG, consume + " ms " + joinPoint.getSignature() + " main thread method");
            }
            return proceed;
        }
        return joinPoint.proceed();
    }    
}
运行app,过滤log查看方法耗时,打印log过滤关键字:adb logcat | grep method

这里写图片描述

猜你喜欢

转载自blog.csdn.net/Q9859/article/details/80663369