Butter Knife 相信很多朋友都用过,然而它究竟是如何工作的,大多数人估计了解得并不清楚,今天的投稿嘉宾姚家艺同学非常详细地为大家讲解这背后的原理。
其实之前拒绝过一些源码解析类的文章投稿,主要还是担心在公众号上大段代码阅读效果糟糕,然而也有一些朋友反馈希望看到这类文章,因此还是尝试推一下这篇,大家也可以选择在 PC 上阅读,效果更佳。因为微信公众号正文限制了外链,所以链接都是不可点击的,部分概念可 Google 了解。
姚家艺同学目前在爱奇艺任职,是一位热爱开源的小伙伴,曾对主流下拉刷新开源库的性能,进行过全面的对比分析,相当精彩;也解析过热门开源项目 Fresco 的源码,文章均发布在其 GitHub 上,深受大家欢迎,有兴趣的朋友可以前往关注:https://github.com/desmond1121,也可以点击文末阅读原文直接访问。
Jake Wharton的黄油刀ButterKnife,用过的人都说好。如果没有用过,请看这段代码:
public DemoActivity extends Activity{
@Bind(R.id.text_view) TextView tv;
@Bind(R.id.some_view) SomeView sv;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.demo_activity);
// 自动帮你生成所有被@Bind注解
// 的视图的findViewById(id)代码
ButterKnife.bind(this);
// 现在可以随意使用视图变量。
}
// 自动绑定OnClickListener
@OnClick(R.id.submit)
public void sayHi(Button button) {
button.setText("Hello!");
}
}
更多炫酷的使用可以看Butter Knife官网!心动了吗?在复杂的页面中用上它不知道能为你省去多少样板代码,代码质量看起来就提高了一个档次! 这么厉害的手段不研究一下怎么行,赶紧来看看。
首先介绍一些预备知识。
通过元注解@Retention
来标识,有三种:
RetentionPolicy.SOURCE
: 源码级别解析,例如@Override
, @SupportWarnings
。这类注解在编译成功后就不会再起作用,并且不会出现在.class文件中。
RetentionPolicy.CLASS
: 编译时解析,默认的解析方式,会保留在最终的.class中,但是无法在运行时获取。
RetentionPolicy.RUNTIME
: 运行时解析,会保留在最终的.class文件中。这类注解可以用反射API中的getAnnotations()
获取到。
更多注解基础知识可以参考codekk公共技术点。
http://a.codekk.com/detail/Android/Trinea/公共技术点之%20Java%20注解%20Annotation
编译时解析是注解强大的地方之一。你可以利用它在编译时帮你生成一些代码逻辑,避免了运行时利用反射解析所带来的性能开销。那么问题来了,注解是怎么在编译的时候被解析的呢?
Java 5带有Annotation Processing Tool(APT)。它能够提供一个编译时的注解处理,并且能够产生新的代码与文件,同时能够让java编译器将生成的代码和原来的代码一起编译!与之配套的还有Mirror API,它提供在编译时对程序结构的静态、只读的分析。这个功能很强大,但是处理起来略显麻烦。Java 6开始将这个功能整合进编译器中,你只要继承AbstractProcessor,并在javac中通过参数-processor指定注解处理就好了。
当然你也可以不指定具体的类,在META-INF/service/下创建文件javax.annotation.processing.Processor
,在里面指定类名(全名)。Java的ServiceLoader会自己去找到这个类并编译。
说到这里,不得不说的两个库:
google-auto-service: 只要在你的Processor
类上加上注解@AutoService(Processor.class)
,它自动帮你将这个类添加到META-INF/service/javax.naaotation.processing.Processor
下,非常好用。
android-apt, 在gradle插件中整合了-processor
与-processorPath
命令,还可以指定project,轻轻松松实现APT。
看完这段心里大概对实现原理有一个初步的猜测,那么开始挖掘代码吧!
ButterKnife能够提供的注解类型太多了,本文就以解析@Bind
注解为例。所有注解绑定都是通过一句ButterKnife.bind()
函数开始的,我们先来看看它做了些什么
static void bind(Object target, Object source, Finder finder) {
Class<?> targetClass = target.getClass();
try {
if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
if (viewBinder != null) {
viewBinder.bind(finder, target, source);
}
} catch (Exception e) {
throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
}
}
这个findViewBinderForClass()
,负责找到ViewBinder
,我们可以看看ViewBinder
是干嘛的:
public interface ViewBinder<T> {
void bind(Finder finder, T target, Object source)
}
Finder
是一个枚举,它的作用就是适配各类findViewById
至Finder.findView()
函数上,比如你用ButterKnife.bind(Activity activity)
时调用的就是bind(target, target, Finder.ACTIVITY)
。
可以看看Finder.ACTIVITY
的声明:
ACTIVITY {
@Override
protected View findView(Object source, int id) {
return ((Activity) source).findViewById(id);
}
@Override
public Context getContext(Object source) {
return (Activity) source;
}
}
很清楚吧。那么我们话题回到ViewBinder
上,你会发现全局实现它的只有一个啥也没干的类NOP_VIEW_BINDER
。纳尼?好吧,我们先看看findViewBinderForClass
是怎么去找的,希望从这寻求到突破点。
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
throws IllegalAccessException, InstantiationException {
// (省略代码)缓存中有就从缓存中取
// (省略代码)"java."与"android."开头的包中的类,返回NOP_VIEW_BINDER
try {
Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
} catch (ClassNotFoundException e) {
viewBinder = findViewBinderForClass(cls.getSuperclass());
}
//(省略代码)将viewBinder放入缓存
return viewBinder;
}
我刚开始看到这的时候心中打了一个大大的问号,啥玩意?Class.forName(clsName + "$$ViewBinder")
??这是啥,好的吧,至此正常路线就看不到任何有价值的东西。那么问题留在这里,我们开始看Processor。
实际上从前言中介绍的注解处理你已经心理有个谱,就是在注解处理器里面重写一下process
方法。那么JW大神是怎么生成的代码?生成什么样的代码?我们还是应该去一探究竟。
首先看一下ButterKnifeProcessor
这个类,顶上一个亮闪闪的@AutoService(Processor.class)
,从此不用再关心Processor被javac解析的事情了。先来看看process
方法里干了什么吧:
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue();
try {
bindingClass.brewJava().writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write view binder for type %s: %s", typeElement, e.getMessage());
}
}
return true;
}
这个函数信息量很大,我将它拆成两部分来说:
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseBind(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, Bind.class, e);
}
}
...
}
这部分代码浅显易懂,解析所有的被@Bind
所注解的Element
(肯定是field,因为@Bind
只能修饰field),并处理它。在parseBind
中会初步判断Element
的修饰符,不可以是private
或static
下的变量,不可以是私有类下的变量,也不可以是非类对象下的field(比如enum)。之后判断被注解的field是否List、Array,但是它们最终都会走向parseBindOne()
函数(解析单个Bind)。那么我们来看看这个函数:
private void parseBindOne(Element element,
Map<TypeElement, BindingClass> targetClassMap,
Set<TypeElement> erasedTargetNames) {
// (省略代码)验证element是否继承自View
// (省略代码)验证@Bind中是否至少含有一个id
// (省略代码)如果上述两个验证失败,则退出。
int id = ids[0];
// 判断element所在的类是否存在对应的BindingClass
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass != null) {
// (省略代码)判断待绑定的id已经被绑定过
} else {
// 创建一个对应类的BindingClass
bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
}
String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
// 在对应BindingClass中添加FieldViewBinding&id组合
FieldViewBinding binding = new FieldViewBinding(name, type, required);
bindingClass.addField(id, binding);
// 将绑定变量所在的类添加到待unBind序列中。
erasedTargetNames.add(enclosingElement);
}
这里出现了两个新词汇:BindingClass
和FieldViewBinding
,我们来看看它们都是啥。
BindingClass
:所有应用ButterKnife的类都有一个单独的BindingClass
与其挂钩,它里面包含了很多信息:Field绑定、Drawable绑定、Collection绑定等等很多信息。以类名+”$$ViewBinder”、包名标识。这个东西似乎前面看见过啊!有点激动,继续往下看。
FieldViewBinding
:存放于BindingClass
内,用于绑定变量View(与之对应还有绑定Drawable、Bitmap、List、Method等)。记录每个Field的绑定信息,如变量名、类名。每个FieldViewBinding
都与一个id对应。同一个id添加第二个FieldViewBinding
时会在之前出现的parseBindOne()
函数中报错。
如果@Bind
修饰符不为空,则意味着该FieldViewBinding
需要绑定。
至此@Bind
的解析工作就结束了,所有的信息都以FieldViewBinding
的形式存在于注解所在的类对应的BindingClass
中。
再次介绍一个大杀器javapoet,square出品的生成.java文件的Java API。通过清晰的语法来生成一个文件结构,无敌。
那我们再回到原先的注解处理process()
方法中。它会遍历每一个生成的BindingClass
,并调用bindingClass.brewJava().writeTo(filer);
。其中filer
是通过ProcessingEnvironment.getFiler()
获取的,它的作用在注释中说了:
Returns the filer used to create new source, class, or auxiliary files.
就是用来生成代码的,APT允许你生成新的代码并且将他们一起编译。由此看来writeTo(filer)
方法就是将生成的代码结构变成.java文件,看来奥秘应该是在brewJava()
这个函数中!那我们赶紧来看看!
这个函数会做几件事情:
生成一个public class {CLASS}$$ViewBinder<T extends {CLASS}> implements ViewBinder<T>
的类结构。其中{CLASS}
代表了ButterKnife注解所在的类名。这就解释了我们最开始百思不得其解的Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder")
。同时它会在其中添加一个实现方法bind(finder, target, source)
,我们来看看它是怎么做的:
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");
// (省略代码) 是否绑定资源
// (省略代码) 是否调用父类的bind
// If the caller requested an unbinder, we need to create an instance of it.
if (hasUnbinder()) {
final String statment;
if (parentUnbinder != null) {
// Explicitly call super in case this class has child's as well.
statment = "$T unbinder = super.accessUnbinder($N)";
else {
statment = "$T unbinder = createUnbinder($N)";
}
result.addStatement(statment, unbinderBinding.getUnbinderClassName(), "target");
}
// 正主来了!
if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
// 声明一个View供后续调用
result.addStatement("$T view", VIEW);
// 对每一个绑定过的id,添加绑定代码
for (ViewBindings bindings : viewIdMap.values()) {
addViewBindings(result, bindings);
}
// (省略代码) List/Array的绑定
}
// (省略代码) Resource/Bitmap/Drawable的绑定
return result.build();
}
接下来计入到addViewBindings
中,分作两部:找到View,将View赋值给变量。
找到View:如果该ViewBinding需要绑定,则添加如下语句:
result.addStatement("view = finder.findRequiredView(source, $L, $S)", bindings.getId(), asHumanDescription(requiredViewBindings));
我们看看Finder.findRequiredView()
里面是什么,它会添加到到时候执行的代码之中:
public <T> T findRequiredView(Object source, int id, String who) {
T view = findOptionalView(source, id, who);
if (view == null) {
//抛出异常
}
return view;
}
public <T> T findOptionalView(Object source, int id, String who) {
View view = findView(source, id);
return castView(view, id, who);
}
还记得开头出现的Finder.ACTIVITY
吗!回去看看,恍然大悟有没有!绑定不同的Finder
会有不同的代码加到生成的代码里去寻找View。JakeWharton真tm是个天才。
赋值给原变量
View找着了之后呢,肯定要把它赋值回原来的变量里:
private void addFieldBindings(MethodSpec.Builder result, ViewBindings bindings) {
Collection<FieldViewBinding> fieldBindings = bindings.getFieldBindings();
for (FieldViewBinding fieldBinding : fieldBindings) {
if (fieldBinding.requiresCast()) {
result.addStatement("target.$L = finder.castView(view, $L, $S)", fieldBinding.getName(),
bindings.getId(), asHumanDescription(fieldBindings));
} else {
result.addStatement("target.$L = view", fieldBinding.getName());
}
}
}
上面这段代码已经非常简明易懂:如果需要Cast,就添加一个cast语句的赋值;否则直接赋值。
最终都会通过Javapoet生成一个文件流写入到Filer
中,被javac编译。
这一切都只是通过一个注解和一句ButterKnife.bind()
。
如果喜欢这篇,欢迎帮助转发,点击文末阅读原文,可直接访问姚家艺同学GitHub主页。