在上篇文章 Android编译期插桩,让程序自己写代码(一) 的前言部分我放了一张图,用来说明编译期插桩的位置和相应的技术。这里,我还打算这张图来开篇。
在上图中,我们可以清楚的看到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是JakeWharton基于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版本。
十分简单,直接上代码
@Target({METHOD, CONSTRUCTOR}) @Retention(CLASS) public @interface DebugLog { } 复制代码
@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,那么这段代码应该很容易能看懂。这里我简单解释一下。
到此,这个简化版的Hugo基本就介绍完了。你可以做个Demo试一下了。
我们定义一个Test类,然后在Activity启动的时候调用myThread()方法。Test类如下:
public class Test { @DebugLog public void myMethod1() throws Exception{ Thread.sleep(1000); } } 复制代码
我们看一下日志,方法的运行时间被完美的打印出来了。
我们仍然以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是可以的,但是如果涉及范围较大就要慎重考虑了。