前言
话说我们做程序员的,都应该多少是个懒人,我们总是想办法驱使我们的电脑帮我们干活,所以我们学会了各式各样的语言来告诉电脑该做什么——尽管,他们有时候也会误会我们的意思。
突然有一天,我觉得有些代码其实,可以按照某种规则生成,但你又不能不写——不是所有的重复代码都可以通过重构并采用高端技术比如泛型来消除的——比如我最痛恨的代码:
TextView textView = (TextView) findViewById(R.id.text_view);
Button button = (Button) findViewById(R.id.button);
这样的代码,你总不能不写吧,真是让人沮丧。突然想到以前背单词的故事:正着背背不过 C,倒着背背不过 V。嗯,也许写 Android App,也是写不过 findViewById
的吧。。
我们今天要介绍的 ButterKnife 其实就是一个依托 Java 的注解机制来实现辅助代码生成的框架,读完本文,你将能够了解到 Java 的注解处理器的强大之处,你也会对 Dagger2 和 AndroidAnnotations 这样类似的框架有一定的认识。
1、初识 ButterKnife
1.1 ButterKnife 简介
说真的,我一直对于 findViewById
这个的东西有意见,后来见到了 Afinal 这个框架,于是我们就可以直接通过注解的方式来注入,哇塞,终于可以跟 findViewById
说『Byte Byte』了,真是好开心。
什么?寨见不是介么写么?
不过,毕竟是移动端,对于用反射实现注入的 Afinal 之类的框架,我们总是难免有一种发自内心的抵触,于是。。。
别哭哈,不用反射也可以的~~
这个世界有家神奇的公司叫做 Square,里面有个大神叫 Jake Wharton,开源了一个神奇的框架叫做 ButterKnife,这个框架虽然也采用了注解进行注入,不过人家可是编译期生成代码的方式,对运行时没有任何副作用,果真见效快,疗效好,只是编译期有一点点时间成本而已。
说句题外话,现如今做 Android 如果不知道 Jake Wharton,我觉得面试可以直接 Pass 掉了。。。哈哈,开玩笑啦
1.2 ButterKnife 怎么用?
怎么介绍一个东西,那真是一个折学问题。别老说我没文化,我的意思是比较曲折嘛。
我们还是要先简单介绍一些 ButterKnife 的基本用法,这些知识你在 ButterKnife 这里也可以看到。
简单来说,使用 ButterKnife 需要三步走:
- 配置编译环境,由于 Butterknife 用到了注解处理器,所以,比起一般的框架,配置稍微多了些,不过也很简单啦:
- 用注解标注需要注解的对象,比如 View,比如一些事件方法(用作 onClick 之类的),例:
- 在初始化布局之后,调用
bind
方法:
setContentView(R.layout.activity_main); ButterKnife.bind(this);//一定要在 setContentView 之后哈,不然你就等着玩空指针吧
瞧,这时候你要是编译一下,你的代码就能欢快的跑起来啦,什么 findViewById
,什么 setOnClickListener
,我从来没听说过~
哈,不过你还是要小心一点儿,你要是有本事写成这样,ButterKnife 就说『信不信我报个错给你看啊!』
这又是为神马嘞?如果你知道 ButterKnife 的机制,那么这个问题就很清晰了,前面我们已经提到,ButterKnife 是通过注解处理器来生成辅助代码进而达到自己的注入目的的,那么我们就有必要瞅瞅它究竟生成了什么鬼。
话说,生成的代码就在 build/generated/source/apt
下面,我们就以 ButterKnife 的官方 sample 为例,它生成的代码如下:
让我们看一下 SimpleActivity$$ViewBinder
:
我们看到这里面有个叫 bind
的方法,这个方法跟我们之前调用的 ButterKnife.bind
的关系可想而知——其实,后者只是个引子,调用它就是为了调用生成的代码。什么,不信?好吧,我就喜欢你们这些充满好奇的娃。我们在调用 ButterKnife.bind
之后,会进入下面的方法:
我们知道参数 target
和 source
在这里都是咱们的 Activity
的实例,那么找到的 viewBinder
又是什么鬼呢?
简单看下注释就很容易理解了,如果我们的 Activity 名为 SimpleActivity
,那么找到的 ViewBinder 应该就是 SimpleActivity$$ViewBinder
。
还是回到我们前面的问题,如果需要注入的成员是 private
,ButterKnife 会报错,显然,如果 title
是 private
,生成的代码中又写到 target.title
,这不就是在搞笑么?小样儿,你以为你是生成的代码, Java 虚拟机就会让你看见不该看的东西么?
当然,对需要注入的成员的要求不止这些啦,我们稍后就会知道,其实对于静态成员和某些特定包下的类的成员也是不支持注入的。
1.3 小结
这个框架给我们的感觉就是,用起来炒鸡简单有木有。说话想当年,@ 给了我们上网冲浪的感觉,现在,我们仍然只需要在代码里面 @ 几下,就可以在后面各种浪了。
等等,这么简单的表象后面,究竟隐藏着怎样的秘密?它那光鲜的外表下面又有那些不可告人的故事?请看下回分解。
2、ButterKnife,给我上一盘蛋炒饭
Jake 大神,我赌一个月好莱坞会员,你一定是一个吃货。。
我们把生成代码这个过程比作一次蛋炒饭,在炒的时候你要先准备炊具,接着准备用料,然后开炒,出锅。
2.1 准备炊具
蛋炒饭是在锅里面炒出来的,那么我们的 ButterKnife 的”锅”又是什么鬼?
闲话少叙,且说从我们配置好的注解,到最终生成的代码,这是个怎样的过程呢?
上图很清晰嘛,虽然什么都没说。额。。别动手。。
你看图里面 ButterKnife 很厉害的样子,其实丫是仗势欺人。仗谁的势呢?我们千呼万唤始出来滴注解处理器,这时候就要登上历史舞台啦!
话说 Java 编译器编译代码之前要先来个预处理,这时候编译器会对 classpath 下面有下图所示配置的注解处理器进行调用,那么这时候我们就可以干坏事儿了(怎么每到这个时候都会很兴奋呢。。)
所以,如果你要自己写注解处理器的话,首先要继承 AbstractProcessor
,然后写下类似的配置。不过稍等一下,让我们看下 Butterknife 是怎么做的:
AutoService
是干什么的呢?看看刚才的图,有没有注意到那个文件夹是红色?是的,它是自动生成的,而负责生成这个配置的家伙就是 AutoService,这是 google 的一个开源组件,非常简单,我就不多说了。
简而言之:注解处理器为我们打开了一扇门,让我们可以在 Java 编译器编译代码之前,执行一段我们的代码。当然这代码也不一定就是要生成别的代码了,你可以去检查那些被注解标注的代码的命名是否规范(周志明大神的 《深入理解 Java 虚拟机》一书当中有这个例子)。啊,你说你要去输出一个 “Hello World”,~~(╯﹏╰)b 也可以。。吧。。
2.2 嘿蛋炒饭,最简单又最困难
既然知道了程序的入口,那么我们就要来看看 ButterKnife 究竟干了什么见不得人的事儿。在这里,所有的输入就是我们在自己的代码中配置的注解,所有的输出,就是生成的用于注入对象的辅助代码。
关于注解处理器的更多细节请大家参考相应的资料哈,我这里直接给出 ButterKnife 的核心代码,在 ButterKnifeProcessor.process
当中:
我们知道,ButterKnife 对于需要注入对象的成员有要求的,在解析注解配置时,首先要对被标注的成员进行检查,如果检查失败,直接抛异常。
在分析解析过程时,我们以 @Bind
为例,注解处理器找到用 @Bind
标注的成员,检验这些成员是否符合注入的条件(比如不能是 private
,不能是 static
之类),之后将注解当中的值取出来,创建或者更新对应的 BindingClass
。
现在以前面提到的 title 为例,解析的时候拿到的 element 其实对应的就是 title 这个变量。
在注入 title 时,对应的要接着执行 parseBindOne
方法:
其实每一个注解的解析流程都是类似的,解析的最终目标就是在这个 bindingClass
里 addField
,这意味着什么呢?
通过前面的分析,其实我们已经知道解析注解的最终目标是生成那些用于注入的代码,这就好比我们让注解管理器写代码。这似乎是一个很有意思的话题,如果你的程序足够聪明,它就可以自己写代码~~
那么这么说 addField
就是要给生成的代码添加一个属性咯?不不不,是添加一组注入关系,后面生成代码时,注解管理器就需要根据这些解析来的关系来组织生成的代码。所以,要不要再看一下生成的代码,看看还有没有新的发现?
2.3、出锅咯
话说,注解配置已经解析完毕,我们已经知道我们要生成的代码长啥样了,那么下一个问题就是如何真正的生成代码。这里用到了一个工具 JavaPoet,同样出自 Square 的大神之手。JavaPoet 提供了非常强大的代码生成功能,比如我们下面将给出生成输出 HelloWorld 的 JavaDemo 的代码:
这样就可以生成下面的代码了:
其实我们自己写个程序生成一些代码并不难,不过导包这个事情却非常的令人焦灼,别担心,JavaPoet 可以把这些统统搞定。
有了个简单的认识之后,我们要看下 ButterKnife 都用 JavaPoet 干了什么。还记得下面的代码么:
bindingClass.brewJava().writeTo(filer);
这句代码将 brew 出来的什么鬼东西写到了 filer
当中,Filer
嘛发挥想象力就知道是类似于文件的东西,换句话说,这句代码就是完成代码生成到指定文件的过程。
- Brew Java !!
- ~ ~ heating ~ ~ ~
=> => pumping => =>
[ ]P coffee! [ ]P
现在我们需要继续看下 createBindMethod
方法,这个方法是生成代码的关键~
private MethodSpec createBindMethod() { /*创建了一个叫做 bind 的方法,添加了 @Override 注解,方法可见性为 public
以及一些参数类型 */
MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(FINDER, "finder", FINAL)
.addParameter(TypeVariableName.get("T"), "target", FINAL)
.addParameter(Object.class, "source"); if (hasResourceBindings()) { // Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
result.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());
} // Emit a call to the superclass binder, if any.
if (parentViewBinder != null) {
result.addStatement("super.bind(finder, target, source)");
} /* 关于 unbinder,我们一直都没有提到过,如果我们有下面的注入配置:
@Unbinder
ButterKnife.Unbinder unbinder;
* 那么这时候就会在生成的代码中添加下面的代码,这实际上就是构造 unbinder
*/
// If the caller requested an unbinder, we need to create an instance of it.
if (hasUnbinder()) {
result.addStatement("$T unbinder = new $T($N)", unbinderBinding.getUnbinderClassName(),
unbinderBinding.getUnbinderClassName(), "target");
} /*
* 这里就是注入 view了,addViewBindings 这个方法其实就生成功能上类似
TextView textView = (TextView) findViewById(...) 的代码
*/
if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) { // Local variable in which all views will be temporarily stored.
result.addStatement("$T view", VIEW); // Loop over each view bindings and emit it.
for (ViewBindings bindings : viewIdMap.values()) {
addViewBindings(result, bindings);
} // Loop over each collection binding and emit it.
for (Map.Entry<FieldCollectionViewBinding, int[]> entry : collectionBindings.entrySet()) {
emitCollectionBinding(result, entry.getKey(), entry.getValue());
}
} /*
* 注入 unbinder
*/
// Bind unbinder if was requested.
if (hasUnbinder()) {
result.addStatement("target.$L = unbinder", unbinderBinding.getUnbinderFieldName());
} /* ButterKnife 其实不止支持注入 View, 还支持注入 字符串,主题,图片。。
* 所有资源里面你能想象到的东西
*/
if (hasResourceBindings()) { //篇幅有限,我还是省略掉他们吧
...
} return result.build();
}
不知道为什么,这段代码让我想起了我写代码的样子。。那分明就是 ButterKnife 在替我们写代码嘛。
当然,这只是生成的代码中最重要的最核心的部分,为了方便理解,我把 demo 里面生成的这个方法列出来方便查看:
@Override
public void bind(final Finder finder, final T target, Object source) { //构造 unbinder
Unbinder unbinder = new Unbinder(target); //下面开始 注入 view
View view;
view = finder.findRequiredView(source, 2130968576, "field 'title'");
target.title = finder.castView(view, 2130968576, "field 'title'"); //... 省略掉其他成员的注入 ...
//注入 unbinder
target.unbinder = unbinder;
}
3、Hack 一下,定义我们自己的注解 BindLayout
我一直觉得,既然 View 都能注入了,咱能不能把 layout 也注入了呢?显然这没什么难度嘛,可为啥 Jake 大神没有做这个功能呢?我觉得主要是因为。。。你想哈,你注入个 layout,大概要这么写
@BindLayout(R.layout.main)public class AnyActivity extends Activity{...}
可我们平时怎么写呢?
public class AnyActivity extends Activity{ @Override
protected void onCreate(Bundle savedInstances){ super.onCreate(savedInstances);
setContentView(R.layout.main);
}
}
你别说你不继承 onCreate
方法啊,所以好像始终要写一句,性价比不高?谁知道呢。。。
不过呢,咱们接下来就运用我们的神功,给 ButterKnife 添砖加瓦(这怎么感觉像校长说的呢。。嗯,他说的是社河会蟹主@义),让 ButterKnife 可以 @BindLayout
。先看效果:
//注入 layout@BindLayout(R.layout.simple_activity)public class SimpleActivity extends Activity {
...
}
生成的代码:
public class SimpleActivity$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> { @Override
public void bind(final Finder finder, final T target, Object source) { //生成了这句代码来注入 layout
target.setContentView(2130837504); //下面省略掉的代码我们已经见过啦,就是注入 unbinder,注入 view
...
}
...
}
那么我们要怎么做呢?一个字,顺藤摸瓜~
第一步,当然是要定义注解 BindLayout
@Retention(CLASS) @Target(TYPE)public @interface BindLayout { @LayoutRes int value();
}
第二步,我们要去注解处理器里面添加对这个注解的支持:
@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
...
types.add(BindLayout.class.getCanonicalName());
... return types;
}
第三步,注解处理器的解析环节要添加支持:
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
Set<String> erasedTargetNames = new LinkedHashSet<>(); // Process each @Bind element.
for (Element element : env.getElementsAnnotatedWith(BindLayout.class)) { if (!SuperficialValidation.validateElement(element)) continue; try {
parseBindLayout(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindLayout.class, e);
}
}
...
}
下面是 parseBindLayout
方法:
private void parseBindLayout(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) { /*与其他注解解析不同,BindLayout 标注的类型就是 TYPE,所以这里直接强转为
TypeElement,其实就是对应于 Activity 的类型*/
TypeElement typeElement = (TypeElement) element;
Set<Modifier> modifiers = element.getModifiers(); // 只有 private 不可以访问到,static 类型不影响,这也是与其他注解不同的地方
if (modifiers.contains(PRIVATE)) {
error(element, "@%s %s must not be private. (%s.%s)",
BindLayout.class.getSimpleName(), "types", typeElement.getQualifiedName(),
element.getSimpleName()); return;
} // 同样的,对于 android 开头的包内的类不予支持
String qualifiedName = typeElement.getQualifiedName().toString(); if (qualifiedName.startsWith("android.")) {
error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
BindLayout.class.getSimpleName(), qualifiedName); return;
} // 同样的,对于 java 开头的包内的类不予支持
if (qualifiedName.startsWith("java.")) {
error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
BindLayout.class.getSimpleName(), qualifiedName); return;
} /* 我们暂时只支持 Activity,如果你想支持 Fragment,需要区别对待哈,
因为二者初始化 View 的代码不一样 */
if(!isSubtypeOfType(typeElement.asType(), ACTIVITY_TYPE)){
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindLayout.class.getSimpleName(), typeElement.getQualifiedName(), element.getSimpleName()); return;
} // 拿到注解传入的值,比如 R.layout.main
int layoutId = typeElement.getAnnotation(BindLayout.class).value(); if(layoutId == 0){
error(element, "@%s for a Activity must specify one layout ID. Found: %s. (%s.%s)",
BindLayout.class.getSimpleName(), layoutId, typeElement.getQualifiedName(),
element.getSimpleName()); return;
}
BindingClass bindingClass = targetClassMap.get(typeElement); if (bindingClass == null) {
bindingClass = getOrCreateTargetClass(targetClassMap, typeElement);
} // 把这个布局的值塞给 bindingClass,这里我只是简单的存了下这个值
bindingClass.setContentLayoutId(layoutId);
log(element, "element:" + element + "; targetMap:" + targetClassMap + "; erasedNames: " + erasedTargetNames);
}
第四步,添加相应的生成代码的支持,这个在 BindingClass.createBindMethod
当中:
private MethodSpec createBindMethod() {
MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(FINDER, "finder", FINAL)
.addParameter(TypeVariableName.get("T"), "target", FINAL)
.addParameter(Object.class, "source"); if (hasResourceBindings()) {
... 省略之 ...
} //如果 layoutId 不为 0 ,那说明有绑定,添加一句 setContentView 完事儿~~
//要注意的是,这句要比 view 注入在前面。。。你懂的,不然自己去玩空指针
if(layoutId != 0){
result.addStatement("target.setContentView($L)", layoutId);
}
...
}
这样,我们就可以告别 setContentView
了,写个注解,非常清爽,随意打开个 Activity
一眼就看到了布局在哪里,哈哈哈哈哈
其实是说你胖。。
4、AndroidAnnotations 和 Dagger2
4.1 AndroidAnnotations
AndroidAnnotations 同样是一个注入工具,如果你稍微接触一下它,你就会发现它的原理与 ButterKnife 如出一辙。下面我们给出其中非常核心的代码:
private void processThrowing(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws Exception { if (nothingToDo(annotations, roundEnv)) { return;
}
AnnotationElementsHolder extractedModel = extractAnnotations(annotations, roundEnv);
AnnotationElementsHolder validatingHolder = extractedModel.validatingHolder();
androidAnnotationsEnv.setValidatedElements(validatingHolder); try {
AndroidManifest androidManifest = extractAndroidManifest();
LOGGER.info("AndroidManifest.xml found: {}", androidManifest);
IRClass rClass = findRClasses(androidManifest);
androidAnnotationsEnv.setAndroidEnvironment(rClass, androidManifest);
} catch (Exception e) { return;
}
AnnotationElements validatedModel = validateAnnotations(extractedModel, validatingHolder);
ModelProcessor.ProcessResult processResult = processAnnotations(validatedModel);
generateSources(processResult);
}
我们就简单看下,其实也是注解解析和代码生成几个步骤,当然,由于 AndroidAnnotations 支持的功能要复杂的多,不仅仅包含 UI 注入,还包含线程切换,网络请求等等,因此它的注解解析逻辑也要复杂得多,阅读它的源码时,建议多多关注一下它的代码结构设计,非常不错。
从使用的角度来说,ButterKnife 只是针对 UI 进行注入,功能比较单一,而 AndroidAnnotations 真是有些庞大和强大,究竟使用哪一个框架,那要看具体需求了。
4.2 Dagger 2
Dagger 2 算是超级富二代了,妈是 Square,爹是 Google—— Dagger 2 源自于 Square 的开源项目,目前已经由 Google 接管(怎么感觉 Google 喜当爹的节奏 →_→)。
Dagger 本是一把利刃,它也是用来注入成员的一个框架,不过相对于前面的两个框架,它
- 显得更基础,因为它不针对具体业务
- 显得更通用,因为它不依赖运行平台
- 显得更复杂,因为它更关注于对象间的依赖关系
用它的开发者说的一句话就是(大意):有一天,我们发现我们的构造方法居然需要 3000 行,这时候我们意识到是时候写一个框架帮我们完成构造方法了。
换句话说,如果你的构造方法没有那么长,其实也没必要引入 Dagger 2,因为那样会让你的代码显得。。。不是那么的好懂。
当然,我们放到这里提一下 Dagger 2,是因为它 完全去反射,实现的思想与前面提到的两个框架也是一毛一样啊。所以你可以不假思索的说,Dagger 2 肯定至少有两个模块,一个是 compiler,里面有个注解处理器;还有一个是运行时需要依赖的模块,主要提供 Dagger 2 的注解支持等等。
5、小结
本文通过对 ButterKnife 的源码的分析,我们了解到了 ButterKnife 这样的注入框架的实现原理,同时我们也对 Java 的注解处理机制有了一定的认识;接着我们还对 ButterKnife 进行了扩充的简单尝试——总而言之,使用起来非常简单的 ButterKnife 框架的实现实际上涉及了较多的知识点,这些知识点相对生僻,却又非常的强大,我们可以利用这些特性来实现各种各样个性化的需求,让我们的工作效率进一步提高。
来吧,解放我们的双手!
本文系腾讯Bugly独家内容,转载请在文章开头显眼处注明作者和出处“腾讯Bugly(http://bugly.qq.com)”