Hugo源码分析

Posted _houzhi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Hugo源码分析相关的知识,希望对你有一定的参考价值。

同时发表在: http://blog.houzhi.me/2016/11/05/hugo-sourcecode-analysis

Hugo是JakeWharton大神开发的一个通过注解触发的Debug日志库。它是一个非常好的AOP框架,在Debug模式下,Hugo利用aspectj库来进行切面编程,插入日志代码。通过分析Hugo的代码能够对gradle以及aspectj有一个非常好的了解。

使用示例

通过使用来看Hugo具体的功能,这样也能够更好的明白Hugo的实现方式。

首先把下面的编译配置文件添加到项目当中:


buildscript 
  repositories 
    mavenCentral()
  

  dependencies 
    classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
  


apply plugin: 'com.android.application'
apply plugin: 'com.jakewharton.hugo'

使用的时候直接使用@DebugLog注解给想要调试的方法就好了,它会打印函数的参数,执行时间,以及返回值:


@DebugLog
public String getName(String first, String last) 
  SystemClock.sleep(15); // Don't ever really do this!
  return first + " " + last;

输出:

V/Example: ⇢ getName(first="Jake", last="Wharton")
V/Example: ⇠ getName [16ms] = "Jake Wharton"

需要指出的,Hugo只会在Debug模式下打印log。DebugLog的源码如下:


package hugo.weaving;



import java.lang.annotation.Retention;

import java.lang.annotation.Target;



import static java.lang.annotation.ElementType.CONSTRUCTOR;

import static java.lang.annotation.ElementType.METHOD;

import static java.lang.annotation.ElementType.TYPE;

import static java.lang.annotation.RetentionPolicy.CLASS;



@Target(TYPE, METHOD, CONSTRUCTOR) @Retention(CLASS)

public @interface DebugLog 


就是注解,Target也包含了TYPE,DebugLog也可以添加到类上面:


  @DebugLog

  static class Greeter 

    private final String name;



    Greeter(String name) 

      this.name = name;

    



    private String sayHello() 

      return "Hello, " + name;

    

  

AspectJ

AspectJ是一个面向切面的框架,它有一个专门的编译器用来生成遵守Java字节编码规范的class文件。在这里下载安装,实际上它有自己的语法。看个简单的例子:


// 我们有一个TestService类,TestService.java

public class TestService

    public void test()

        System.out.println("test");

    

    public static void main(String[]args)

            new TestService().test();

    


现在我们想要给test()方法增加一些东西,比如打印test方法进入的时间。如果用aspectj则可以增加一个文件:


public aspect LogAspect 



        pointcut logPointcut():execution(void TestService.test());

        void around():logPointcut()

                System.out.println("start time: " + System.currentTimeMillis());

                proceed();

                System.out.println("end time: " + System.currentTimeMillis());

        

通过执行命令 ajc -d . TestService.java LogAspect.java生成TestService.class,然后执行命令java TestService,输出为:




start time: 1478071231659

test

end time: 1478071231659


实际上将ajc理解为类似于javac的编译工具就好了,它编译的目标跟javac一样的都是java class文件,只是源文件的语法是符合aspect语法的。可以看看TestService.class反编译后的源码:


org.aspectj.runtime.internal.AroundClosureTestService 
    TestService() 
    

    test() 
        test_aroundBody1$advice(LogAspect.aspectOf()(AroundClosure))

    main(String[] args) 
        (TestService()).test()

将aspect理解编译时期对源文件按照指定的描述(aspect语法文件)进行编译,得到进行切入后的字节码文件。详细的介绍可以参看这篇文章spring aop

我们看看Hugo项目中是如何使用aspect,实际的aspect部分代码是在hugo-runtime子模块当中。


package hugo.weaving.internal;



import android.os.Build;

import android.os.Looper;

import android.os.Trace;

import android.util.Log;



import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.Signature;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import org.aspectj.lang.reflect.CodeSignature;

import org.aspectj.lang.reflect.MethodSignature;



import java.util.concurrent.TimeUnit;



@Aspect

public class Hugo 

  private static volatile boolean enabled = true;



  @Pointcut("within(@hugo.weaving.DebugLog *)")

  public void withinAnnotatedClass() 



  @Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")

  public void methodInsideAnnotatedType() 



  @Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")

  public void constructorInsideAnnotatedType() 



  @Pointcut("execution(@hugo.weaving.DebugLog * *(..)) || methodInsideAnnotatedType()")

  public void method() 



  @Pointcut("execution(@hugo.weaving.DebugLog *.new(..)) || constructorInsideAnnotatedType()")

  public void constructor() 



  public static void setEnabled(boolean enabled) 

    Hugo.enabled = enabled;

  



  @Around("method() || constructor()")

  public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable 

    enterMethod(joinPoint);





    long startNanos = System.nanoTime();

    Object result = joinPoint.proceed();

    long stopNanos = System.nanoTime();

    long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);

    exitMethod(joinPoint, result, lengthMillis);

    return result;

  



  private static void enterMethod(JoinPoint joinPoint) 

    if (!enabled) return;



     // ...

    //组织相关信息到builder当中



    if (Looper.myLooper() != Looper.getMainLooper()) 

      builder.append(" [Thread:\\"").append(Thread.currentThread().getName()).append("\\"]");

    



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



    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) 

      final String section = builder.toString().substring(2);

      Trace.beginSection(section);

    

  



  private static void exitMethod(JoinPoint joinPoint, Object result, long lengthMillis) 

    if (!enabled) return;



    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) 

      Trace.endSection();

    



    // ...

    //组织相关信息为builder



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

  

  private static String asTag(Class<?> cls) 

    if (cls.isAnonymousClass()) 

      return asTag(cls.getEnclosingClass());

    

    return cls.getSimpleName();

  


上面只是给出的代码去除了相关详细内容,具体代码可以直接看源码Hugo.java

上面是使用了aspect注解来描述相关插入代码的:

@Aspect 表示这个类由AspectJ处理

@Pointcut 描述切面内容,可以理解为针对哪些方法,类,进行拦截,插入代码。

@Around 实际上拦截方法,这个注解可以同时拦截方法的执行前后,另外有@Before, @After,顾名思义,表示方法执行前跟方法执行后拦截。

关于aspectj,邓凡平的这篇文章深入理解Android之AOP介绍的挺详细的。aspectj编译器会根据这些描述信息对项目中的源码进行插入。另外还有cglib能够有类似的功能,CGlib是在运行期对类进行动态代理(Proxy.newProxyInstance只能对接口进行动态代理),具体可以Google一下。

gradle代码

Hugo源码中除了aspect的使用,我觉得另外就是项目的编译控制了,因为Hugo只会在Debug模式下打印日志,而控制只在Debug模式下打印日志是在编译脚本中实现的。Hugo源码中目录树主要是:


|- hugo-plugin

|- hugo-runtime

|- hugo-annotations

|- hugo-example

我们使用过程的方式是:


buildscript 
  repositories 
    mavenCentral()
  

  dependencies 
    classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
  


apply plugin: 'com.android.application'
apply plugin: 'com.jakewharton.hugo'

Gradle 实现Debug插入代码

所以先看com.jakewharton.hugo插件,这个插件的实现是在hugo-plugin当中,hugo-plugin模块的hugo-plugin/src/main/resources/META-INF/gradle-plugins/目录下面有com.jakewharton.hugo.properties,这就表示插件的声明。该文件的内容是:


implementation-class=hugo.weaving.plugin.HugoPlugin

指定了插件实现的代码。然后看hugo.weaving.plugin.HugoPlugin的内容(对应hugo-plugin/src/main/groovy/hugo/weaving/plugin/HugoPlugin.groovy文件),这是groovy源文件,groovy也是一种编程语言,跟Java差不多。关于gradle插件声明使用可以参看gradle源码目录下面的samples/customPlugin项目(比如~/.gradle/wrapper/dists/gradle-2.10-all/a4w5fzrkeut1ox71xslb49gst/gradle-2.10/samples)。HugoPlugin.groovy的文件内容如下:


class HugoPlugin implements Plugin<Project> 

  @Override void apply(Project project) 

    def hasApp = project.plugins.withType(AppPlugin)

    def hasLib = project.plugins.withType(LibraryPlugin)

    if (!hasApp && !hasLib) 

      throw new IllegalStateException("'android' or 'android-library' plugin required.")

    



    final def log = project.logger

    final def variants // variants是构造变种版本,为同一个应用创建不同的版本。

    if (hasApp) 

      variants = project.android.applicationVariants

     else 

      variants = project.android.libraryVariants

    



    project.dependencies  // 声明的项目依赖

      debugCompile 'com.jakewharton.hugo:hugo-runtime:1.2.2-SNAPSHOT'

      // TODO this should come transitively

      debugCompile 'org.aspectj:aspectjrt:1.8.6'

      compile 'com.jakewharton.hugo:hugo-annotations:1.2.2-SNAPSHOT'

    



    project.extensions.create('hugo', HugoExtension)



    variants.all  variant ->

      if (!variant.buildType.isDebuggable())  // 非Debug情况下,直接返回

        log.debug("Skipping non-debuggable build type '$variant.buildType.name'.")

        return;

       else if (!project.hugo.enabled)  //关闭Hugo

        log.debug("Hugo is not disabled.")

        return;

      

      // 使用Hugo的情况下,调用aspect编译,args指定了aspect相关参数。

      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", project.android.bootClasspath.join(File.pathSeparator)

        ]

        log.debug "ajc args: " + Arrays.toString(args)



        MessageHandler handler = new MessageHandler(true);

        new Main().run(args, handler);// 运行aspect

        // ... 省略了log运行结果的代码

      

    

  




上面这个HogoPlugin.groovy指定了编译的时候区分Debug和release版本编译。非Debug并且没有disable Hugo的时候,使用aspect给应用中的代码插入。aspect会找到有@Aspect注解的类,然后解析这个类,处理代码。这样整个过程就完了。

Gradle与maven

在hugo目录下面有个build.gradle,先看一下dependencies:


buildscript 

  repositories 

    mavenCentral()

    jcenter()

  



  dependencies 

    classpath 'org.gradle.api.plugins:gradle-nexus-plugin:0.7'

    classpath 'com.android.tools.build:gradle:1.3.1'

    classpath 'org.aspectj:aspectjtools:1.8.6'

    classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3'

  


mavenCentral()和jcenter是指定了repositories,也就是远程仓库,gradle编译的时候,可以从这些仓库里面获取引用库的包。

  • org.aspectj:aspectjtools : 是aspectj的库

  • com.github.dcendents:android-maven-gradle-plugin: 修改自maven插件,是一个让maven与android库(arr)相兼容的库。

  • gradle-nexus-plugin: 是配置和上传组件的gradle插件

build.gradle里面定义了几个Task,我们看一下cleanExample的定义来简单了解一下Task:


task cleanExample(type: Exec) 

  executable = '../gradlew'

  workingDir = project.file('hugo-example')

  args = [ 'clean' ]


Task cleanExample 是清理example项目的task,上述代码使用gradlew脚本,将工作目录设置为hugo-example目录下面,设置gradle的参数为clean,这样就清理hugo-example项目了。

这里只是简单介绍一下gradle,如果不了解gradle,建议先看看gradle的介绍文档,比如Gradle for Android中文,另外就是邓凡平的深入理解Android(一):Gradle详解

我觉得对于gradle,正确的理解方式是它是基于groovy脚本的一种构建框架,它提供了Android编译的框架及其API。另外groovy种充满了闭包,理解好闭包的概念,然后查看API,这样入手和理解Gradle会很容易明白。

Hugo 改进

因为Hugo当中DebugLog使用的Retention是CLASS类型,所以打包之后,注解还是会存在,这样release的apk包就会增加一些大小。就拿Hugo的example来说,如果DebugLog的Retention是CLASS,release包大概是3476bytes,如果DebugLog的retention是Source的时候,release包是3444bytes。还是能够减少一点包大小的。目前我还没有完全地弄好这个问题,不过如果是导入library的方式使用Hugo的话,可以这样来弄:


      debugCompile 'com.jakewharton.hugo:hugo-annotations:1.2.2-SNAPSHOT'
      releaseCompile 'com.jakewharton.hugo:hugo-annotations-release:1.2.2-SNAPSHOT'

hugo-annotations-release里面使用SOURCE retention的DebugLog。这样就能够在debug版本使用CLASS retention的DebugLog,而release版本使用SOURCE retention的DebugLog。不过这种配置只能适合debug和release打包时。如果有更好地idea,我在hugo上面提了个issues:support release build type use DebugLog with SOURCE Retention ,直接评论。

总结

Hugo是一个比较小的项目,但是里面却包含了优秀的思想以及先进的技术。AOP编程,注解的理解,Gradle编译的理解,Aspect的使用,以及gradle-maven在Hugo项目中都用到了,看一下Hugo的源码是学习这些东西的一个非常好的方式。

参考:

  1. Spring AOP,AspectJ, CGLIB 有点晕:http://www.jianshu.com/p/fe8d1e8bd63e

  2. Spring AOP 实现原理与 CGLIB 应用:https://www.ibm.com/developerworks/cn/java/j-lo-springaopcglib/

  3. 深入理解Android之AOP:http://blog.csdn.net/innost/article/details/49387395

  4. Hugo 探究:https://yq.aliyun.com/articles/7104

  5. Gradle for Android中文:https://avatarqing.gitbooks.io/gradlepluginuserguidechineseverision/content/introduction/README.html

  6. 深入理解Android(一):Gradle详解:http://www.infoq.com/cn/articles/android-in-depth-gradle

以上是关于Hugo源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Android 高级进阶(源码剖析篇) 便于性能分析的日志框架 hugo

hugo小玩

HashMap详细解释+全站最硬核手撕源码分析

HashMap详细解释+全站最硬核手撕源码分析

如何在hugo的页面内容中使用模板参数

如何在 Hugo 中注释掉内容