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的源码是学习这些东西的一个非常好的方式。
参考:
Spring AOP,AspectJ, CGLIB 有点晕:http://www.jianshu.com/p/fe8d1e8bd63e
Spring AOP 实现原理与 CGLIB 应用:https://www.ibm.com/developerworks/cn/java/j-lo-springaopcglib/
深入理解Android之AOP:http://blog.csdn.net/innost/article/details/49387395
深入理解Android(一):Gradle详解:http://www.infoq.com/cn/articles/android-in-depth-gradle
以上是关于Hugo源码分析的主要内容,如果未能解决你的问题,请参考以下文章