Android编译期插桩,让程序自己写代码(二)

4,013 阅读5分钟

前言

在上篇文章Android编译期插桩,让程序自己写代码(一)的前言部分我放了一张图,用来说明编译期插桩的位置和相应的技术。这里,我还打算这张图来开篇。

AspectJ

在上图中,我们可以清楚的看到AspectJ的插桩位置是.java与.class之间。这很容易使人联想到编译器。事实上,AspectJ就是一种编译器,它在Java编译器的基础上增加了关键字识别和编译方法。因此,AspectJ可以编译Java代码。它还提供了Aspect程序。在编译期间,将开发者编写的Aspect程序织入到目标程序中,扩展目标程序的功能。

AspectJ可以应用于Android和后端开发中。在后端,AspectJ 应用更为广泛一些,著名的Spring框架就对AspectJ提供了支持。不过,近些年,AspectJ技术在Android领域也开始崭露头角,比较知名的有JakeWharton的hugo。另外,一些企业也开始探索AspectJ在埋点、权限管理等方面的应用。

关于AspectJ更为详细的介绍,请大家移步邓平凡大神的博客深入理解Android之AOP。这篇文章对于初次接触AspectJ的人来说十分友好,笔者最初就是通过它进入AspectJ殿堂的。珠玉在前,本文就不再介绍AspectJ的基础知识了。那本文要说些什么呢?

  • 一个简单的Hugo框架。
  • 从字节码分析AspectJ。

Hugo

Hugo是JakeWharton基于AspectJ开源的一个调试框架,其功能是通过注解的方式可以打印出方法的运行时间,方便开发者性能调优。今天,我们就来看一下它的庐山真面目。

配置AspectJ

Hugo是基于AspectJ的,那首先我们就要支持AspectJ。这里向大家推荐沪江的AspectJX,它不仅使用简单,而且还支持过滤一些aar或jar包。

首先我们需要在根build.gradle中依赖AspectJX

dependencies {
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
        }

在app项目的build.gradle里应用插件,并添加aspectj的依赖库

apply plugin: 'android-aspectjx'

api 'org.aspectj:aspectjrt:1.8.9'

这样就配置完成了,是不是很简单啊。

注意:笔者这里采用的gradle版本是3.0.1,如果没有编译通过,检查一下gradle版本。

定义DebugLog注解

十分简单,直接上代码


@Target({METHOD, CONSTRUCTOR})
@Retention(CLASS)
public @interface DebugLog {
}

编写Aspect


@Aspect
public class Hugo {

  @Pointcut("execution(@com.hugo.example.lib.DebugLog * *(..))")
  public void method() {}

  @Pointcut("execution(@com.hugo.example.lib.DebugLog *.new(..))")
  public void constructor() {}

  @Around("method() || constructor()")
  public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {

    CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();
    Class<?> cls = codeSignature.getDeclaringType();
    String methodName = codeSignature.getName();
    long startNanos = System.nanoTime();
    
    Object result = joinPoint.proceed();
    
    long stopNanos = System.nanoTime();
    long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);
    StringBuilder builder = new StringBuilder();
    builder.append("methodName:")
            .append(methodName)
            .append("  ----  executeTime:")
            .append(lengthMillis);

    Log.e(asTag(cls), builder.toString());

    return result;
  }

  private static String asTag(Class<?> cls) {
    if (cls.isAnonymousClass()) {
      return asTag(cls.getEnclosingClass());
    }
    return cls.getSimpleName();
  }
}

如果你学习了深入理解Android之AOP,那么这段代码应该很容易能看懂。这里我简单解释一下。

  1. 选取注解了DebugLog的method和constructor作为pointcut。
  2. 在原方法执行前织入开始时间,在原方法执行后织入结束时间,并计算出运行时间。通过Log.v打印出来。

到此,这个简化版的Hugo基本就介绍完了。你可以做个Demo试一下了。

  1. 笔者DebugLog的包名是com.hugo.example.lib。因此在method()和constractor()中的注解内容是com.hugo.example.lib.DebugLog。
  2. 其实,真正的Hugo框架核心也只有一个类。它只是在日志打印时,输出了更多的方法信息。本文为了方便读者理解作了简化。

测试

我们定义一个Test类,然后在Activity启动的时候调用myThread()方法。Test类如下:

public class Test {

    @DebugLog
    public void myMethod1() throws Exception{
        Thread.sleep(1000);
    }
}

我们看一下日志,方法的运行时间被完美的打印出来了。

从字节码分析AspectJ

我们仍然以Test为例,看一下Test反编之后的字节码。

反编译的内容看起来不太方便,我在这里把它转换成了如下代码:

为了观看方便,上图将代码分为4部分。

我们先看第一部分,这是一个静态代码块,也就是说在类加载的时候,程序会AspectJ提供的Factory类,创建一个类型为JoinPoint.StaticPart静态实例STATIC_PART。深入理解Android之AOP中对JoinPoint.StaticPart介绍如下:

thisJoinPointStaticPart对象:在advice代码中可直接使用,代表JPoint中那些不变的东西。比如这个JPoint的类型,JPoint所处的代码位置等。这里thisJoinPointStaticPart就是代码中的JoinPoint.StaticPart。

第二部分是我们之前定义的myThread方法,它在编译期间被替换了。在运行时,它首先通过Factory的静态方法makeJP创建一个JoinPoint对象。makeJp是一个重载方法,我们看一下。

public static JoinPoint makeJP(JoinPoint.StaticPart staticPart, Object _this, Object target) {
    return new JoinPointImpl(staticPart, _this, target, NO_ARGS);
}
public static JoinPoint makeJP(JoinPoint.StaticPart staticPart, Object _this, Object target, Object[] args) {
    return new JoinPointImpl(staticPart, _this, target, args);
}

通过我列出来了两个,可以看到JoinPoint除了包含了我们第一步中提到了STATIC_PART对象,还包括了this,target对象,以及方法参数。这和深入理解Android之AOP中对thisJoinpoint描述也是一致的:

thisJoinpoint对象:在advice代码中可直接使用。代表JPoint每次被触发时的一些动态信息,比如参数啊之类的。

创建完JoinPoint对象后,随后调用了第三部分中的advice()方法。advice()方法大部分都是我们在Hugo中编写的织入代码,这里只有一个不同,那就是joinPoint.proceed()不见了,替换成了源代码中具体的处理逻辑。

总结

通过上述分析,我们可以清楚的感知到AspectJ提供了非常强大的功能。但同时,由于其为每个切入点生成一个JoinPoint.StaticPar静态实例和在运行过程中生成的JoinPoint以及一些其它的封装,这必然会导致程序在内存和处理速度等方面受影响。因此,在小范围内使用AspectJ是可以的,但是如果涉及范围较大就要慎重考虑了。