ButterKnife,你真的了解吗?
源自JakeWharton的经典制作ButterKnife, 相信大多数人都用的很熟练, 它一定程度上简化了我们的findViewById以及OnClick等操作,给平时开发带来了很大的便利。此处对是否应该使用它,我不作评论,也不是本篇的目的。
这个使用了多年的框架, 你真的知道是什么原理吗?它的实现是怎样的,你有产生过好奇心吗?那么就让我们来解读一下。
解读ButterKnife之准备篇
反射
这是一个多数框架都会使用的技术,只要知道类或对象,我们可以解析出关于此类的一切信息,为所欲为。一提反射,很多人会觉得性能消耗不会很大吗?肯定你也带着同样的想法吧?那么我来声明一下,反射存在性能消耗是有的,但是没有想象中的夸张;如果结合缓存使用的话,可以极大的抵消带来的性能的消耗,从而更好的享受它所带来的便利。作为一个Java开发者,不会反射真的有些过分了..
AnnotationProcessor
大家都知道,ButterKnifed 便利来自于注解。那么既然存在注解,注解处理器技术的使用是必然的。现在的框架不同于以往;以前的框架类似XUtils的注解很大程度上是使用反射来解析的,上面说过,反射带来性能消耗还是有的;但是现在, 大多数的注解框架都是基于Apt或AnnotationProcessor的编译时解析实现的。试想一下,在程序编译时就完成了注解解析的工作,又会给性能带来什么影响呢?答案当然是没影响。
早期的注解处理器大多使用开源的Apt, 然而一是Apt已不再被作者所维护;二则Google推出了AnnotationProcessor来替代它,更是集成到了API中,所以怎么看,使用AnnotationProcessor都是一个很好的选择。
Javapoet
这是一个很神奇的技术,借助它,我们可以便捷的生成我们想要的代码。至少目前来看,GreenDao生成的Bean与Dao, ButterKnife生成的Binding以及ARouter生成的Router&&Group&&xx等,都是使用了Poet技术。
以上3项技术被大多数框架所使用,如果你还不会,你已经Out了。那么读完这篇文章,让你脱离Out的领域。
ButterKnife的使用
这里不着重说怎么用ButterKnife, 或者说作为一个如此简单的框架,压根就不用多说。这里引入它的使用,为ButterKnife的解析作为一个引导。
// 初始化
ButterKnife.bind(Object target, View source);
// 加入注解
@BindView(id)
Button
以上就是ButterKnife的基本调用。初始化,然后添加各种注解,如BindView, OnClick, BindBitmap。 如果你看过它的源码,你就会知道,ButterKnife支持的注解主要分为3类: BindView之绑定View, BindBitmap/ BindColor等绑定资源, OnClick等绑定事件。至于为什么不添加绑定setContentView, 大神说只能替换一行代码而已,没意思。大神就是这么任性...
ButterKnife的实现思想
从注解, 从我列举的准备技术,我想聪明的你已经猜到了ButterKnife是如何实现了吧。是的,它所使用的核心技术可以称之为依赖注入。俗话说就是,我们不在Activity中初始化这些View, 这堆资源, 这系列的点击事件;我们通过生成自定义类来偷偷的初始化,偷偷的绑定监听。对,偷偷的...
1)添加注解
这个没什么多说的, ButterKnife的核心即是通过注解来简化操作。
2)解析注解,生成注入类
加注解的目的是什么?就是偷偷生成一个类,在这个类里面悄悄的消化你注解的内容,用我们最熟悉的方式。这样我们看起来只要加个注解就实现了,好厉害;如果你看了生成的Binding类,你就知道,切,这不是我们整天写的吗?
现在看一下生成的类
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131165245;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.id_button, "method 'doClick'");
view2131165245 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.doClick(p0);
}
});
}
@Override
@CallSuper
public void unbind() {
if (target == null) throw new IllegalStateException("Bindings already cleared.");
target = null;
view2131165245.setOnClickListener(null);
view2131165245 = null;
}
}
上面的类就是ButterKnife自动生成的类。里面包含的@OnClick的处理,我来解析一下,你就明白了。
首先看类名, 类名的形式你一看就明白了,就是@注解的类 + 一个后缀ViewBinding。
其次,看一下变量, MainActivity不用解释了吧,你是在MainActivity的方法上添加的@OnClick; View即是你点击的控件。
再者看一下构造,构造传入了MainActivity, 然后findRequiredView,其实这里面就是findViewById。后面的实现看一眼就明白了,在生成类里面设置监听,然后调用你添加注解的Method。难以理解吗? 很简单的实现吧。这就是回调的使用吧,相信我们都经常使用,在View点击的时候,调用指定的方法。
最后看unbind()方法,在页面执行结束后,是会自动释放资源的。
再来看下BindView生成的Binding类
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
target.btn = Utils.findRequiredViewAsType(source, R.id.id_button, "field 'btn'", Button.class);
target.tv = Utils.findRequiredViewAsType(source, R.id.id_textview, "field 'tv'", TextView.class);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.btn = null;
target.tv = null;
}
}
和OnClick生成的类很相似,只是改成了findRequiredViewAsType。 然而这个方法的实现不过是findViewById加类型转换,也就是类似的
Button btn = (Button)findViewById(R.id.btn);
看到这里,或许有人就奇怪了,你是怎么生成的? 答:技术就是使用JavaPoet。 至于这些属性怎么得到的, 如类名, id值, Activity与View, 控件是什么, 绑定的onClick方法是什么? 那你得去问AnnotationProcessor了,就是这么神通广大。
我们来捋一下思路,
1 添加注解
2)在AnnotationProcessor中解析注解(至于怎么解析,下面说)
3)根据解析的信息,自动生成Binding类
4)在ButterKnife.bind时,一般是onCreate, 得到对应的Binding类,通过反射,生成Binding类的对象,执行Binding类的构造函数。在构造中偷偷的findViewById, setOnClickListener等。这时候在对应的Activity中添加注解的控件就有值了。添加OnClick的控件就设置了监听了。
看了以上的思路,是不是恍然大悟?所以我才说这是一个很简单的框架。那么接下来,解读一**解处理的源码与生成类的源码
首先看配置
// 导入生成代码的库
api deps.javapoet
// 导入自动注册的库
compileOnly deps.auto.service
// 上面的库是什么
javapoet: 'com.squareup:javapoet:1.10.0',
'auto': [
'service': 'com.google.auto.service:auto-service:1.0-rc4',
'common': 'com.google.auto:auto-common:0.10',
],
然后看**解处理器的代码, ButterKnifeProcessor, 其中process为解析的方法
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
######################################
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
scanForRClasses(env);
// Process each @BindAnim element.
for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceAnimation(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindAnim.class, e);
}
}
// Process each @BindArray element.
for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceArray(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindArray.class, e);
}
}
// Process each @BindBitmap element.
for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceBitmap(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindBitmap.class, e);
}
}
// Process each @BindBool element.
for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceBool(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindBool.class, e);
}
}
// Process each @BindColor element.
for (Element element : env.getElementsAnnotatedWith(BindColor.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceColor(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindColor.class, e);
}
}
// Process each @BindDimen element.
for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceDimen(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindDimen.class, e);
}
}
// Process each @BindDrawable element.
for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceDrawable(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindDrawable.class, e);
}
}
// Process each @BindFloat element.
for (Element element : env.getElementsAnnotatedWith(BindFloat.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceFloat(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindFloat.class, e);
}
}
// Process each @BindFont element.
for (Element element : env.getElementsAnnotatedWith(BindFont.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceFont(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindFont.class, e);
}
}
// Process each @BindInt element.
for (Element element : env.getElementsAnnotatedWith(BindInt.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceInt(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindInt.class, e);
}
}
// Process each @BindString element.
for (Element element : env.getElementsAnnotatedWith(BindString.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceString(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindString.class, e);
}
}
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// 遍历带有{@interface: BindView}的元素
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
// Process each @BindViews element.
for (Element element : env.getElementsAnnotatedWith(BindViews.class)) {
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindViews(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindViews.class, e);
}
}
// Process each annotation that corresponds to a listener.
for (Class<? extends Annotation> listener : LISTENERS) {
findAndParseListener(env, listener, builderMap, erasedTargetNames);
}
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
// which starts at the roots (superclasses) and walks to the leafs (subclasses).
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
return bindingMap;
}
############################################
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Start by verifying common generated code restrictions.
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
|| isBindingInWrongPackage(BindView.class, element);
// Verify that the target type extends from View.
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
Name qualifiedName = enclosingElement.getQualifiedName();
Name simpleName = element.getSimpleName();
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
if (elementType.getKind() == TypeKind.ERROR) {
note(element, "@%s field with unresolved type (%s) "
+ "must elsewhere be generated as a View or interface. (%s.%s)",
BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
} else {
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
BindView.class.getSimpleName(), qualifiedName, simpleName);
hasError = true;
}
}
if (hasError) {
return;
}
// Assemble information on the field.
int id = element.getAnnotation(BindView.class).value();
BindingSet.Builder builder = builderMap.get(enclosingElement);
QualifiedId qualifiedId = elementToQualifiedId(element, id);
if (builder != null) {
String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
if (existingBindingName != null) {
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBindingName,
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}
以上代码片段分为3部分。
部分1为process执行的部分,从代码可以得知,主要有两部实现: 1) 找到所有的注解标签与信息 2)生成代码
部分2为查找所有的注解信息,如下以BindView为例
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// 遍历带有{@interface: BindView}的元素
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
遍历所有带BindView的标签,然后解析注解信息。
部分3为解析注解信息, 我们可以得到注解的类的包名,类名, 注解的值是什么等等。
得到以上信息, 我们有了生成Binding类的所有条件。那么看一下怎么生成类的
JavaFile brewJava(int sdk, boolean debuggable) {
return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
private TypeSpec createType(int sdk, boolean debuggable) {
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(FINAL);
}
if (parentBinding != null) {
result.superclass(parentBinding.bindingClassName);
} else {
result.addSuperinterface(UNBINDER);
}
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
if (!constructorNeedsView()) {
// Add a delegating constructor with a target type + view signature for reflective use.
result.addMethod(createBindingViewDelegateConstructor());
}
result.addMethod(createBindingConstructor(sdk, debuggable));
if (hasViewBindings() || parentBinding == null) {
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
private MethodSpec createBindingConstructorForView() {
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC)
.addParameter(targetTypeName, "target");
if (constructorNeedsView()) {
builder.addStatement("this(target, target)");
} else {
builder.addStatement("this(target, target.getContext())");
}
return builder.build();
}
看了吗? 根据MethodSpec可以生成任意的方法,根据TypeSpec可以生成任意的类;只要我们取得生成需要的相关信息。
整个流程都顺畅了,那么怎么调用的Binding类实现注入的呢? 答案是在ButterKnife.bind时。
注意:在编译的时候已经生成对应的Binding类, 在bind时只需要查找对应的类
看一下源码
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) {
View sourceView = source.getWindow().getDecorView();
return createBinding(target, sourceView);
}
核心方法是在createBinding中,继续往下看
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
你看到了什么? findBindingConstructorForClass很明显是根据targetClass(MainActivity)得到构造函数。然后 return constructor.newInstance(target, source); 生成实例,这时候就调用了构造函数。而在构造函数中,它执行了findViewById与setOnClickListener, 并完成了赋值。这时候, 添加注解的Button 、View等就完成findViewById的赋值了。Button.setOnClickListener也完成了。
如果你不甘心,继续看一下如何得到构造函数的吧,这就用到了反射了。
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
没骗你吧, 加载Binding类,然后getConstructure,得到构造函数了。
通过以上的分析,你还迷茫吗?如果你把我列举的3个技术准备,那么你就可以完成自己的ButterKnife了。是不是很简单呢? 毕竟是最简单的框架之一了。好了不早了,拜拜了~!
还是那句话,如果你觉得这篇文章对你有帮助,请帮我打Call, 如有疑问,请留言,谢谢!!!