AspectJ在Android中的应用

AOP是Aspect Oriented Programming的缩写,即『面向切面编程』。
不同于我们平时接触到的OOP编程思想,即Object Oriented Programming『面向对象编程』。
OOP的思想:功能模块化,对象化。
而AOP的思想,则不一样:
它提倡针对同一类问题的统一处理。
若要分析谁优谁劣,这倒没有了意思。
编程的时候,肯定是谁更适合,采用谁;取百家之长,才是正道。


AOP有何优势

AOP可以无侵入的在宿主中插入一些代码,特别适用于:

  • 日志埋点
  • 性能监控
  • 动态权限控制
  • 甚至是代码调试等等。

AspectJ简介

AspectJ实际上是对AOP编程思想中的一个实践。
除了它以外,还有很多其它的AOP实现,例如ASMDex,但目前最好、最方便的,依然是AspectJ。
AspectJ官方网站: http://www.eclipse.org/aspectj

在Android中,AspectJ是略阉割的版本,不过对于一般客户端,完全够用。
在Android上集成AspectJ比较复杂,不过那些奋斗在开源路上的前辈,已经帮大家解决了这个问题。
以下两个开源库目前比较成功:

gradle_plugin_android_aspectjx 实践

注意事项

  • aspectjx基于 gradle android插件1.5及以上版本。
  • aspectjx是使用在application module的插件, 虽然用在library module上也不会出错,但是不生效。
  • 需要在AOP代码进行hook的类及方法名不能被混淆,需要在混淆配置里keep住。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    package com.hujiang.test;
    public class A {
    public boolean funcA(String args) {
    ....
    }
    }
    # 如果你在AOP代码里对A#funcA(String)进行hook, 那么在混淆配置文件里加上这样的配置
    -keep class com.hujiang.test.A {*;}
  • Android Studio的Instant Run功能有时会对你的编译有影响,当出现这种情况时,关闭Instant Run功能,参考Issue
  • 由于IntelliJ现在并没有提供AspectJ相关的工具和插件,所以,在Android Studio,IDEA上不支持*.aj文件的编译。目前仅支持AspectJ annotation的方式。
  • aspectj代码编译时会报一些如下的错,找到对应的库,把这个库过滤掉就可以了。

使用

导入依赖

  • 在根目录的build.gradle添加
    1
    2
    3
    dependencies {
    classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.10'
    }
  • 主项目的build.gradle里应用插件:
    1
    apply plugin: 'android-aspectjx'
  • 主项目的build.gradle中添加AspectJ的依赖:
    1
    compile 'org.aspectj:aspectjrt:1.8.9'

    aspectjx配置

  • aspectjx默认会遍历项目编译后所有的.class文件和依赖的第三方库去查找符合织入条件的切点,为了提升编译效率,可以加入过滤条件指定遍历某些库或者不遍历某些库。
    例如:忽略所有依赖的库 其他配置可参考该开源项目说明文档
    1
    2
    3
    aspectjx {
    excludeJarFilter '.jar'
    }

    AspectJ入门

    以下代码会在调用所有满足android.app.Activity.on**(..)匹配的方法执行之前执行一遍
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Aspect
    public class AspectJTest {
    @Before("execution(* android.app.Activity.on**(..))")
    public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Object[] args = joinPoint.getArgs();
    Log.i("Log", "onActivityMethodBefore: key=" + key + (args != null ? ("参数" + args.length + "个") : "无参数"));
    }
    }
    在class上方使用@Aspect注解来定义这样一个AspectJ文件,编译器会在编译的时候自动去解析。

AspectJ 相关组成介绍

1
2
3
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
}

这里会分成几个部分,我们依次来看:

  • @Before:Advice,也就是具体的插入点
  • execution:处理Join Point的类型,例如call、execution
  • (* android.app.Activity.on*(..)):这个是最重要的表达式,第一个『』表示返回值,『』表示返回值为任意类型,后面这个就是典型的包名路径,其中可以包含『』来进行通配,几个『*』没区别。同时,这里可以通过『&&、||、!』来进行条件组合。()代表这个方法的参数,你可以指定类型,例如android.os.Bundle,或者(..)这样来代表任意类型、任意个数的参数。
  • public void onActivityMethodBefore:实际切入的代码。
    这里还有一些匹配规则,可以作为示例来进行讲解:
表达式 含义
java.lang.String 匹配String类型
java.*.String 匹配java包下的任何“一级子包”下的String类型,如匹配java.lang.String,但不匹配java.lang.ss.String
java..* 匹配java包及任何子包下的任何类型,如匹配java.lang.String、java.lang.annotation.Annotation
java.lang.*ing 匹配任何java.lang包下的以ing结尾的类型
java.lang.Number+ 匹配java.lang包下的任何Number的自类型,如匹配java.lang.Integer,也匹配java.math.BigInteger
参数 含义
() 表示方法没有任何参数
(..) 表示匹配接受任意个参数的方法
(..,java.lang.String) 表示匹配接受java.lang.String类型的参数结束,且其前边可以接受有任意个参数的方法
(java.lang.String,..) 表示匹配接受java.lang.String类型的参数开始,且其后边可以接受任意个参数的方法
(*,java.lang.String) 表示匹配接受java.lang.String类型的参数结束,且其前边接受有一个任意类型参数的方法
Advice

Advice就是我们具体插入的代码,它有三种方式 Before、After、Around。
分别指示在我们是具体插入的代码(之前、之后、前后的时候)执行注入代码。
例如:以下注释表示在满足匹配条件的方法之前执行注入代码:

1
@Before("execution(* android.app.Activity.on**(..))")
  • Before 注入代码执行时间在调用方法之前
  • After 注入代码执行时间在调用方法之后
  • Around 注入代码执行时间在调用方法前后都可以
    1
    2
    3
    //这是之前
    proceedingJoinPoint.proceed();
    //这是之后
    注意:,Around和After是不能同时作用在同一个方法上的,会产生重复切入的问题
Join Points

Join Points,简称JPoints。
是AspectJ的核心思想之一,它就像一把刀,把程序的整个执行过程切成了一段段不同的部分。
例如,构造方法调用、调用方法、方法执行、异常等等,这些都是Join Points。
实际上,也就是你想把新的代码插在程序的哪个地方,是插在构造方法中,还是插在某个方法调用前,或者是插在某个方法中,这个地方就是Join Points,当然,不是所有地方都能给你插的,只有能插的地方,才叫Join Points。

Pointcuts

Pointcuts是带有过滤器的Join Points,以帮助我们确定调用正确的代码作为切入点。

1
@Pointcut("withincode(* com.xys.aspectjxdemo.MainActivity.testAOP2(..))")
Pointcuts中的call 和 execution介绍
1
2
3
4
5
6
7
8
9
10
11
12
13
对于Call来说:
Call(Before)
Pointcut{
Pointcut Method
}
Call(After)

对于Execution来说:
Pointcut{
execution(Before)
Pointcut Method
execution(After)
}
  • call 指的是需要执行的注入代码加在调用(指定的方法)的方法体内中。(前后都是,即哪里调用,就在哪里实现注入代码)
  • execution 指的是需要执行的注入代码加指定的方法体内(前后都是,即调用某某方法,就在某某方法实现注入代码)
Pointcuts中的切入点过滤与withincode

看下面这个例子就可以明白:加入withincode可以做到只在testAOP2()方法中被插入代码,这就做到了精确条件的插入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package top.goluck.aspectj_2017_6_11;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testAOP1();
testAOP2();
}

public void testAOP() {
Log.d("Log", "testAOP");
}

public void testAOP1() {
testAOP();
}

public void testAOP2() {
testAOP();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 在testAOP2()方法内
@Pointcut("withincode(* com.xys.aspectjxdemo.MainActivity.testAOP2(..))")
public void invokeAOP2() {
}

// 调用testAOP()方法的时候
@Pointcut("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
public void invokeAOP() {
}

// 同时满足前面的条件,即在testAOP2()方法内调用testAOP()方法的时候才切入
@Pointcut("invokeAOP() && invokeAOP2()")
public void invokeAOPOnlyInAOP2() {
}

@Before("invokeAOPOnlyInAOP2()")
public void beforeInvokeAOPOnlyInAOP2(JoinPoint joinPoint) {
String key = joinPoint.getSignature().toString();
Log.d(TAG, "onDebugToolMethodBefore: " + key);
}
自定义Pointcuts

自定义Pointcuts可以让我们更加精确的切入一个或多个指定的切入点。

  • 首先,我们需要自定义一个注解类,例如——DebugTool.java:
    1
    2
    3
    4
    5
    6
    7
    /**
    * 自定义AOP注解
    */
    @Retention(RetentionPolicy.CLASS)
    @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
    public @interface DebugTool {
    }
  • 然后在需要插入代码的地方使用这个注解:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class MainActivity extends AppCompatActivity {

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

    @DebugTool
    public void testAOP() {
    Log.d("Log", "testAOP");
    }

    }
  • 最后,我们来创建自己的切入文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Pointcut("execution(@com.xys.aspectjxdemo.DebugTool * *(..))")
    public void DebugToolMethod() {
    }

    @Before("DebugToolMethod()")
    public void onDebugToolMethodBefore(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d("Log", "onDebugToolMethodBefore: " + key);
    }
    总结步骤:先定义Pointcut,并申明要监控的方法名,最后,在Before或者其它Advice里面添加切入代码,即可完成切入。
    总结优势:通过这种方式,我们可以非常方便的监控指定的Pointcut,从而增加监控的粒度。即:我们能够一目了然的了解那些方法我们有加入注入。
异常处理AfterThrowing

AfterThrowing是一个比较少见的Advice,它用于处理程序中未处理的异常。
我们随手写一个异常,代码如下:

1
2
3
4
5
6
7
8
package top.goluck.aspectj_2017_6_11;
public class AspectJTest{
//其他代码略
public void testAOP() {
View view = null;
view.animate();
}
}

然后使用AfterThrowing来进行AOP代码的编写:

1
2
3
4
5
@AfterThrowing(pointcut = "execution(* top.goluck.aspectj_2017_6_11.AspectJTest.testAOP(..))", throwing = "exception")
public void catchExceptionMethod(Exception exception) {
String message = exception.toString();
Log.d("Log", "catchExceptionMethod: " + message);
}

注意:

  • @AfterThrowing虽然可以捕捉异常,但是catch之后又重新抛出了该异常。
    故:此处可用于异常统计、监听等之类的处理,而不可用于解决处理异常之用。
  • @AfterThrowing 不能用于原生代码也包含try catch捕捉了所有异常的方法。因为都没有抛异常的行为了,也就无法切入了。

相关参考