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

栏目: 编程工具 · 发布时间: 7年前

内容简介:在上篇文章在上图中,我们可以清楚的看到AspectJ的插桩位置是.java与.class之间。这很容易使人联想到编译器。事实上,AspectJ就是一种编译器,它在Java编译器的基础上增加了关键字识别和编译方法。因此,AspectJ可以编译Java代码。它还提供了Aspect程序。在编译期间,将开发者编写的Aspect程序织入到目标程序中,扩展目标程序的功能。AspectJ可以应用于Android和后端开发中。在后端,AspectJ 应用更为广泛一些,著名的Spring框架就对AspectJ提供了支持。不过

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

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);
    }
}
复制代码

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

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

从字节码分析AspectJ

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

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

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

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

为了观看方便,上图将代码分为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是可以的,但是如果涉及范围较大就要慎重考虑了。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spark SQL内核剖析

Spark SQL内核剖析

朱锋、张韶全、黄明 / 电子工业出版社 / 2018-8 / 69.00元

Spark SQL 是 Spark 技术体系中较有影响力的应用(Killer application),也是 SQL-on-Hadoop 解决方案 中举足轻重的产品。《Spark SQL内核剖析》由 11 章构成,从源码层面深入介绍 Spark SQL 内部实现机制,以及在实际业务场 景中的开发实践,其中包括 SQL 编译实现、逻辑计划的生成与优化、物理计划的生成与优化、Aggregation 算......一起来看看 《Spark SQL内核剖析》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具