朋友介绍了一个开源项目Hugo,国庆期间,我对它进行了一些学习和探究。
Hugo介绍
我们写代码时,常会打日志输出某个函数执行耗时,传入的参数以及返回值。那么我们能否把这件事情做的更加优雅呢?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"
log只会在debug版本中输出(这需要写编译插件,目前只支持gradle编译),并且添加的注解也不会被VM读取,所以可以认为它对release版本没有任何影响,因此我们也可以放心地把加了注解的代码提交到代码仓库中。
Hugo的实现原理
要理清Hugo的实现原理,我觉得要回答两个问题:
1. 如何只通过加一个注解,就实现输出日志的功能?
2. 如何做到对release版毫无影响?
我们通过分析源码来逐个解决问题。
1.如何输出日志?
核心代码在hugo.weaving.internal.Hugo.java中。
@Aspect
public class Hugo {
@Pointcut("within(@hugo.weaving.DebugLog *)")
public void withinAnnotatedClass() {}
@Pointcut("execution(* *(..)) && withinAnnotatedClass()")
public void methodInsideAnnotatedType() {}
@Pointcut("execution(*.new(..)) && withinAnnotatedClass()")
public void constructorInsideAnnotatedType() {}
@Pointcut("execution(@hugo.weaving.DebugLog * *(..)) || methodInsideAnnotatedType()")
public void method() {}
@Pointcut("execution(@hugo.weaving.DebugLog *.new(..)) || constructorInsideAnnotatedType()")
public void constructor() {}
@Around("method() || constructor()")
public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {
//ProceedingJoinPoint有参数信息,输出参数的值
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) {
...
}
private static void exitMethod(JoinPoint joinPoint, Object result, long lengthMillis) {
...
}
private static String asTag(Class<?> cls) {
...
}
}
Hugo.java这个类有一个注解@Aspect。这里用到了AspectJ,AspectJ是基于Java的AOP框架,关于AOP和AspectJ这里有一篇不错的中文介绍。
到这里我们可以理解了,借助AspectJ,Hugo在编译期对有@DebugLog注解的函数加上log逻辑。
例如:
@DebugLog
private void printArgs(String... args) {
for (String arg : args) {
Log.i("Args", arg);
}
}
编译后,再反编译,看到的结果如下:
@DebugLog
private void printArgs(String... args) {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, (Object) this,
(Object) this, (Object) args);
Hugo.aspectOf().logAndExecute(new AjcClosure1(new Object[]{this,
args, makeJP}).linkClosureAndJoinPoint(69648));
}
static final void printArgs_aroundBody0(HugoActivity ajc$this, String[] args,
JoinPoint joinPoint) {
for (String arg : args) {
Log.i("Args", arg);
}
}
public class AjcClosure1 extends AroundClosure {
public AjcClosure1(Object[] objArr) {
super(objArr);
}
public Object run(Object[] objArr) {
Object[] objArr2 = this.state;
HugoActivity.printArgs_aroundBody0((HugoActivity) objArr2[0],
(String[]) objArr2[1], (JoinPoint) objArr2[2]);
return null;
}
}
printArgs()函数已经被代理了。
2.如何做到对release版本无影响
根据文档,使用Hugo时,除了引入aar,还需要在gradle文件中引入Hugo插件。
apply plugin: 'com.jakewharton.hugo'
插件的核心代码在hugo.weaving.plugin.HugoPlugin.groovy,核心代码如下:
project.dependencies {
debugCompile 'com.jakewharton.hugo:hugo-runtime:1.2.2-SNAPSHOT'
debugCompile 'org.aspectj:aspectjrt:1.8.6'
compile 'com.jakewharton.hugo:hugo-annotations:1.2.2-SNAPSHOT'
}
这里就明白了,因为这里的依赖声明是debugCompile,故release不会输出log。
再来看Annotation的声明
@Target({TYPE, METHOD, CONSTRUCTOR}) @Retention(CLASS)
public @interface DebugLog {
}
由于注解DebugLog的RetentionPolicy是CLASS,所以它虽然会被写入class文件中,但是不会被VM读取到,对运行时没有影响。(注:项目的README中介绍说“the annotation itself is never present in the compiled class file for any build type”我觉得是不太妥,根据源码,他应该是会存在于编译后的class文件的。)
Hugo的价值
毫无疑问,Hugo可以用极小的代价帮我们实现优雅的函数监控。当然如果像我们这样使用maven打包的,我们需要自己开发maven的插件。
但我觉得Hugo的价值不止于此。Hugo给我们提供一种思路,在Android中,利用AOP的思路实现优雅的变成。我想类似的思想我们还可以做别的很多事情,例如统计打点,例如可以用在我们的common模块中实现模块解耦等等。