Android AOP

概念

切面(Aspect)

通俗来说就是“何时何地发生何事”,其组成如下:

Aspect = Advice (what & when) + Pointcut (where)

通知(Advice)

通知(Advice)定义了何时(when)发生何事(what)。

Spring AOP 的切面(Aspect)可以搭配下面五种通知(Adive)注解使用:

切点(Pointcut)

切点(Pointcut)定义了切面在何处(where)执行。

pointcut可以控制你把哪些advice应用于jointpoint上去,通常你使用pointcuts通过正则表达式来把明显的名字和模式进行匹配应用。决定了那个jointpoint会获得通知。分为call、execution、target、this、within等关键字。

Spring AOP 的切点(Pointcut)使用 AspectJ 的“切点表达式语言(Pointcut Expression Language)”进行定义。但要注意的是,Spring 仅支持其中一个子集:

切点表达式的语法如下: 这里还有一些匹配规则,可以作为示例来进行讲解:

连接点(jointpoint)

连接点是切面插入应用程序的地方,该点能被方法调用,而且也会被抛出意外。连接点是应用程序提供给切面插入的地方,在插入地建立AspectJ程序与源程序的连接。

AOP的实现原理

在编译期对目标对象、方法做标记,对目标类、方法进行重构,将PointCut插入目标中,截获该目标的信息以及上下文环境,以达到非侵入代码监控的目的。

  1. 编写Aspect:声明Aspect、PointCut和Advise。
  2. ajc编织: AspectJ编译器在编译期间对所切点所在的目标类进行了重构,在编译层将AspectJ程序与目标程序进行双向关联,生成新的目标字节码,即将AspectJ的切点和其余辅助的信息类段插入目标方法和目标类中,同时也传回了目标类以及其实例引用。这样便能够在AspectJ程序里对目标程序进行监听甚至操控。
  3. execution & call
    • execution:它截获的是方法真正执行的代码区。Around方法就是专门为它存在的,调用Around可以控制原方法的执行与否,可以选择执行也可以选择替换。
      Pointcut{
      execution(Before)
      Pointcut Method
      execution(After)
      }
      
    • call:它截获的是方法的调用区,并不截获代码真正的执行区域,它截获的是方法调用之前与调用之后(与before、after配合使用),在调用方法的前后插入JoinPoint和before、after通知。它截获的信息并没有execution那么多,它无法控制原来方法的执行与否,只是在方法调用前后插入切点,因此它比较适合做一些轻量的监控(方法调用耗时,方法的返回值等)。
      Call(Before)
      Pointcut{
      Pointcut Method
      }
      Call(After)
      
  4. Around替代原理:目标方法体被Around方法替换,原方法重新生成,名为XXX_aroundBody(),如果要调用原方法需要在AspectJ程序的Around方法体内调用joinPoint.proceed()还原方法执行,是这样达到替换原方法的目的。达到这个目的需要双方互相引用,桥梁便是Aspect类,目标程序插入了Aspect类所在的包获取引用。AspectJ通过在目标类里面加入Closure(闭包)类,该类构造函数包含了目标类实例、目标方法参数、JoinPoint对象等信息,同时该类作为切点原方法的执行代理,该闭包通过Aspect类调用Around方法传入Aspect程序。这样便达到了关联的目的,便可以在Aspect程序中监控和修改目标程序。
  5. Before与After:Before与After只是在方法被调用前和调用之后添加JoinPoint和通知方法(直接插入原程序方法体中),调用AspectJ程序定义的Advise方法,它并不替代原方法,是在方法call之前和之后做一个插入操作。After分为returnning和throwing两类,前者是在正常returning之后调用,后者是在throwing发生之后调用。默认的After是在finally处调用,因此它包含了前面的两种情况。
  6. within 和 withincode
    • within:针对的是类
    • withincode: 针对的是方法

例如,假设方法functionA, functionB都调用了dummy,但只想在functionB调用dummy时织入代码。

使用的一个例子:

//--------  MainActivity.java
public class MainActivity extends Activity {

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

  private void functionA() { dummy(); }
  private void functionB() { dummy(); }
  private void dummy() { }
}

//-------- MethodTracer.java
@Aspect
public class MethodTracer {
  // withincode: 在functionB方法内
  @Pointcut("withincode(void org.sdet.aspectj.MainActivity.functionB(..))")
  public void invokeFunctionB() {}

  // call: 调用dummy方法
  @Pointcut("call(void org.sdet.aspectj.MainActivity.dummy(..))")
  public void invokeDummy() {}

  // 在functionB内调用dummy方法
  @Pointcut("invokeDummy() && invokeFunctionB()")
  public void invokeDummyInsideFunctionB() {}

  @Before("invokeDummyInsideFunctionB()")
  public void beforeInvokeDummyInsideFunctionB(JoinPoint joinPoint) {
    System.out.printf("Before.InvokeDummyInsideFunctionB.advice() called on '%s'", joinPoint);
  }
}

//-------- 编译后的MainActivity.java

public class MainActivity extends Activity {

  private static final JoinPoint.StaticPart ajc$tjp_0;

  private static void ajc$preClinit() {
    Factory localFactory = new Factory("MainActivity.java", MainActivity.class);
    ajc$tjp_0 = localFactory.makeSJP("method-call", localFactory.makeMethodSig("2", "dummy", "org.sdet.aspectj.MainActivity", "", "", "", "void"), 56);
  }

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(2130903040);    
    functionA();
    functionB();
  }

  private void functionA() {
    dummy();
  }

  // 只有functionB调用dummy时才会织入代码
  private void functionB() {
    MainActivity localMainActivity = this;
    JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_0, this, localMainActivity);
    MethodTracer.aspectOf().beforeInvokeDummyInsideFunctionB(localJoinPoint);
    localMainActivity.dummy();
  }

使用方法

  1. 编写切面代码

    @Aspect
    public class AspectTest {
     private static final String POINTCUT_METHOD = "execution(* android.app.Activity.on**(..))";
    
     @Pointcut(POINTCUT_METHOD)
     public void methodAnnotatedWithDebugTrace() {
     }
    
     @Before("methodAnnotatedWithDebugTrace()")
     public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
         String key = joinPoint.getSignature().toString();
         Log.d(TAG, "onActivityMethodBefore: " + key);
     }
    }
    
  2. 业务代码

    public class MainActivity extends AppCompatActivity {
    
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
     }
    }
    
  3. 配置gradle

    主工程和Module都需要对构建脚本添加一些任务,目的就是为了建立两者的通信,使得IDE使用ajc编译代码。

final def log = project.logger
final def variants = project.android.applicationVariants

android.libraryVariants.all { variant ->
  LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
  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", plugin.project.android.bootClasspath.join(
        File.pathSeparator)]

    MessageHandler handler = new MessageHandler(true);
    new Main().run(args, handler)

    def log = project.logger
    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:
        case IMessage.INFO:
          log.info message.message, message.thrown
          break;
        case IMessage.DEBUG:
          log.debug message.message, message.thrown
          break;
      }
    }
  }

常见问题

(1)问题:AspectJ中Signature提供的getDeclareType返回的是声明类型,无法获取运行时类型,因此无法准确获取接口运行时类别。

方案:使用target关键字约束pointCut,获取目标对象,通过反射获取其运行时类别。

(2)问题:使用target关键字约束pointcut获取目标对象Object之后,无法获取静态方法(不属于对象)

方案:单独将静态方法提出来,再与前面的target关键字约束的集合取并集。

(3)问题:使用Before、After通知,测试方法耗时的精确度误差较大

方案:改用execution+around。两点,第一:由于Before、After是在原方法调用前后插入通知(会影响本来所在方法快的执行速率);第二:同时Before、After两个操作无法保证是原子操作,多线程情况下会有误差。因此该用execution关键字,截获方法体的真正执行处,使用Around通知,替代原方法(原方法被更名,但结构不变),在Around通知体内调用原方法计时,这样能够真正还原方法执行耗时;

results matching ""

    No results matching ""