Android编程思想和框架Android开发Android知识

Welcome to Android AOP

2017-02-20  本文已影响465人  MasterNeo

What?

As we all know,在进行项目构建时,追求各模块高内聚,模块间低耦合。然而现实并不总是如此美好,某些通用功能是横跨并嵌入到其他各模块中的。比如日志打印、方法的执行时间统计、参数校验等。这些功能星罗棋布得分散在项目工程中,既不方便统一管理,也不利于后期维护。

如何对这些零散却通用的功能做一些优化处理呢? AOP(Aspect-Oriented Programming),为我们提供了一种新的思路。AOP翻译成中文是面向切面编程,其与OOP一样,是一种编程范式。如果说OOP是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。AOP可通过预编译和运行时动态代理的方式实现,从而更好地组织工程和代码结构,AOP的实现原理如图1所示。

图1 AOP实现原理

AOP已广泛应用于服务端编程,并取得了巨大的成功,如大名鼎鼎的Spring框架。而在本文中,我们将AOP引入Android工程,用以解决开篇提到的编程痛点。阅读完本文后便会发现,AOP在Android项目中同样可以做一些有趣的事。

Where?

AOP在Android工程中能用在哪?或者说能用AOP来做什么?下面列举了一些使用场景:

当然,用AOP还可以实现一些具体的业务需求。

How?

介绍了这么多AOP的概念,那么如何进行具体应用呢?下面介绍几种现有的工具和类库,可以很方便地实现AOP编程。

琳琅满目的兵器库中,当然要选择一件最趁手的,就是AspectJ了。选择AspectJ的具体原因主要有三点:1、功能强大,能满足大部分需求;2、支持编译期和加载时代码注入,无侵入实现;3、易于使用。

AspectJ是一种几乎和Java完全一样的语言而且完全兼容Java。使用AspectJ有两种方法,除了使用AspectJ的语法之外,还支持原生Java。然而,无论使用哪种方法,最后都需要AspectJ的编译工具ajc来实现编译,由于AspectJ实际上脱胎于Java,所以ajc工具也能编译Java源码。

AspectJ支持编译期和加载时代码注入,可以理解为是一种Hook。在开始之前,我们先看几个关键词:Join Point、Pointcut、Advice、Aspect、Weaving。这几个关键词在AspectJ中的抽象关系如图2所示。

图2 AspectJ中各组件的抽象关系

接下来详细阐述各个关键词的含义和作用。

Join Point 描述 示例
method call 方法调用 如调用a.method(),此处为Join Point
method execution 方法执行 如a.method()执行内部,此处为Join Point
constructor call 构造方法调用 同method call类似
constructor execution 构造方法执行 同method execution类似
field get get 变量 如读取a.field成员变量,此处为Join Point
field set set 变量 如设置a.field成员变量,此处为Join Point
static initialization 类初始化 如class A中的static{},此处为Join Point
handler 异常处理 如try-catch(xxx)中对应catch内部的执行

public pointcut test call(public * *.method(…)) && !within(A);

Join Point类型 Pointcut类型
method call call(MethodSignature)
method execution execution(MethodSignature)
constructor call call(ConstructorSignature)
constructor execution execution(ConstructorSignature)
Field read access get(FieldSignature)
field write access set(FieldSignature)
static initialzation staticinitialzation(TypeSignature)
handler handler(TypeSignature)

Pointcut的过滤规则语法内容较多,规则如下:

@注解(可选) 访问权限(可选) 返回值的类型 包名.函数名(参数)

com.*.util:可以表示com.netease.util,也可以表示com.google.util;
com.netease.*AOP,可以表示com.netease.AndroidAOP,也可以表示com.neteast.JavaAOP;
com..*表示com包中任意子包下的任意子类;
com..*AOP+表示com包下任意子包下以AOP结尾的子类,如AndroidAop的子类;

方法参数也有相应的匹配规则:

(int, String):表示第一个参数为int,第二个参数为String;
(int, ..):表示第一个参数为int,后面参数任意数量和类型;
(String …):表示不定参数,且类型都为String;
(..):表示任意数量和类型的参数;

附加筛选 描述 示例
within(TypeSignature) 参数表示package或class,可使用通配符 如within(A),表示class A中的所有Join Point
withincode(Constructor&Method) 与within类似,匹配精确到方法 如within(A.method())表示class的method()涉及到的所有Join Point
this(Type) 判断Join Point是否是Type类型 Join Point属于某个类,this用以判断该Join Point所在的类是否是Type类型
args(TypeSignature) 对参数进行条件搜索 args(String...),表示参数为String的不定参数

所以,总结起来,上文的Pointcut的栗子的语义就是:

选择那些任意包名和类名的&&访问权限为public的&&方法名为method的&&参数任意的方法作为目标Join Point。但需要排除class A中的所有Join Point。

Advice类型 描述
before() 表示在Join Points之前前Hook并执行
after() 表示在Join Points执行完之后Hook并执行
after():returning(返回值类型) 方法执行完正常返回处Hook并执行
after():throwing(异常类型) 方法异常退出点Hook并执行
around() around作用是取代原Join Points

引入AOP的栗子

在项目中引入AOP的方式共有三种,分别是原生AspectJ语言,在Java代码中使用AspectJ,结合自定义注解的方式在Java代码中使用AspectJ。考虑到易用性和非侵入性,本栗子的方式是在Java代码中使用AspectJ,在不修改业务代码的前提下,实现onCreate方法性能的监控。

首先考虑未引入AOP的情形,我们如何完成对业务代码的性能监控。

public class MainActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener {
    private static final String TAG = MainActivity.class.getSimpleName();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TimeWatcher watcher = new TimeWatcher();
        watcher.start();

        initView();

        watcher.stop();
        Log.i(TAG, "onCreate execute with " + watcher.getTotalTime() + "ms");
    }

很容易可以发现,业务代码和性能监控的功能性代码交织在一起,结构不清晰,耦合度较高。而且更关键的是,每当一处业务代码需要做性能统计,就需要把统计代码重新复制一份。后期如果需要修改性能统计代码的逻辑,比如修改统计日志内容、增加日志上传功能,需要找到每一处引用到的代码进行修改,这样无疑增加了工作量和出错的概率。

引入AOP可以很好地剥离项目中业务代码和性能统计代码。首先创建aspect Module,引入AspectJ的相关类库,并构建主类和辅助类,项目结构如图3所示。

图3 项目结构

在aspect这个module下新建一个Java类。

@Aspect
public class TimeWatchAspect {
    private static final String POINTCUT_TIME_WATCH =
            "execution(* com.netease.aopdemo.MainActivity.onCreate(..))";

    @Pointcut(POINTCUT_TIME_WATCH)
    public void timeWatch() {}

    @Around("timeWatch()")
    public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature ms = (MethodSignature) joinPoint.getSignature();
        String methodName = ms.getName();
        String className = ms.getDeclaringType().getSimpleName();

        TimeWatcher watcher = new TimeWatcher();
        watcher.start();
        Object result = joinPoint.proceed();
        watcher.stop();

        Log.i(className, methodName + " execute with " + watcher.getTotalTime() + "ms");

        return result;
    }
}

打上@Aspect注解,则该类可以被ajc编译器识别为一个Asepct,在工程项目编译时便能非常方便地实现代码织入。图5中可以看到AspectJ的三个要素,Join Point、Advice和Aspect。好像少了Join Point?Join Point早已定义在Pointcut的字符串常量中,即MainActivity的onCreate方法。Pointcut以注解的形式定义,注解了timeWatch方法,从而timeWatch就是这个Pointcut的名称,注解参数则使用定义好的字符串常量,作为Join Point的过滤规则。同样,Advice也是将类型关键字(此处为Around)注解在特定的方法weaveJoinPoint之上,注解的参数为具名的Pointcut,即timeWatch。上文提到Around类型即用该方法替换原Join Point的实现,图5中Object result = joinPoint.proceed()等价于原有的被Hook方法,即MainActivity的onCreate()。在该语句的前后,是性能统计的代码片段。

public class MainActivity extends AppCompatActivity
        implements NavigationView.OnNavigationItemSelectedListener {
    private static final String TAG = MainActivity.class.getSimpleName();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initView();
    }

再看一眼我们重构后的业务代码,仅需要专注于业务逻辑处理,是不是变得非常简洁。

编译并运行项目代码后,结果如图4所示。

图4 AOP运行结果

那么ajc编译器在字节码层面到底做了什么呢?

  protected void onCreate(Bundle paramBundle)
  {
    JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_0, this, this, paramBundle);
    TimeWatchAspect.aspectOf().weaveJoinPoint(new MainActivity.AjcClosure1(new Object[] { this, paramBundle, localJoinPoint }).linkClosureAndJoinPoint(69648));
  }


  public class MainActivity$AjcClosure1 extends AroundClosure
{
  public MainActivity$AjcClosure1(Object[] paramArrayOfObject)
  {
    super(paramArrayOfObject);
  }

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

反编译项目生成的apk后可以看到,ajc在Join Point处织入了代码,用TimeWatchAspect.aspectOf().weaveJoinPoint()实现了替换。

踩过的坑

正所谓人不能两次踏进同一条河流,我踩过的坑希望大家可以避免。
由于AspectJ在字节码层面将功能性代码织入业务代码中,源码层面无法看到变化,且无法在功能性代码中进行断点调试。所以一旦出错,调试成本相对较高。如果项目运行结果与预期不符,首先检查编译问题,能否正常实现代码织入(可以看apk中的class文件树结构),再检查Join Point、Pointcut和Advice是否符合AspectJ语法,Hook是否正确。

如果Android Studio中的Instant Run开启,则在编译时可能会影响代码的正常织入,所以建议关闭Instant Run。

另外,一般初级阶段会选择日志打印的方式验证AspectJ接入的可行性。如果测试机是魅族系列手机,则注意把项目中Log等级提升到D以上,或者在手机的开发者选项中选择显示所有等级的日志,否则默认情况下你看不到D及D以下等级日志的输出(惨痛的教训,浪费了两天时间排查问题)。

总结

本文主要介绍了Android AOP的编程思想和应用场景,AOP这种编程范式已经在服务端取得了成功,我相信在Android客户端编程中,也一样可以有用武之地。通过AspectJ这种工具可以很方便地实现AOP编程,在文中重点介绍了AspectJ的语法和使用方式,并以实例的方式展示了如何通过AspectJ重构代码,降低了耦合度并利于后期维护。最后介绍了我自己踩过的坑,避免重复入坑。AOP是一种编程思想,理解并运用到工程中将会大有裨益,AspectJ同样是非常强大的工具,文中只介绍了一些基本用法,还有很多高级特性待探索和实践。只要脑洞够大,一定能实现一些意想不到的效果。最后,希望我的文章能给大家带来一些启发,在今后的开发过程中,可以用AOP来做一些exciting的事情。

参考文章

  1. http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
  2. https://blog.egorand.me/going-aspect-oriented-with-aspectj-and-google-analytics/
  3. http://blog.csdn.net/innost/article/details/49387395

Demo源码

https://github.com/Master-Neo/AopDemo

上一篇 下一篇

猜你喜欢

热点阅读