Hugo源码分析

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

使用示例

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

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

1
2
3
4
5
6
7
8
9
10
11
12
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
}
}
apply plugin: 'com.android.application'
apply plugin: 'com.jakewharton.hugo'

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

1
2
3
4
5
@DebugLog
public String getName(String first, String last) {
SystemClock.sleep(15); // Don't ever really do this!
return first + " " + last;
}

输出:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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也可以添加到类上面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@DebugLog
static class Greeter {
private final String name;
Greeter(String name) {
this.name = name;
}
private String sayHello() {
return "Hello, " + name;
}
}

AspectJ

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 我们有一个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则可以增加一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,输出为:

1
2
3
4
5
6
start time: 1478071231659
test
end time: 1478071231659

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

1
2
3
4
5
6
7
8
9
10
11
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子模块当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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源码中目录树主要是:

1
2
3
4
5
6
7
8
|- hugo-plugin
|- hugo-runtime
|- hugo-annotations
|- hugo-example

我们使用过程的方式是:

1
2
3
4
5
6
7
8
9
10
11
12
13
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,这就表示插件的声明。该文件的内容是:

1
2
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的文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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:

1
2
3
4
5
6
7
8
9
10
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的话,可以这样来弄:

1
2
3
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