我研究Butterknife源码的目的是为了解决如下几个我在使用过程当中所思考的问题:java
基于Butterknife 8.8.1版本。 为了更好地分析代码,我写了一个demo: MainActivity.java:android
public class MainActivity extends Activity {
@BindView(R.id.text)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick(R.id.text)
public void textClick() {
Toast.makeText(MainActivity.this, "textview clicked", Toast.LENGTH_LONG);
}
}
复制代码
咱们从Butterknife.bind()方法,即方法入口开始分析: ButterKnife#bind():git
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
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);
}
}
@Nullable @CheckResult @UiThread
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().loadClass(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;
}
复制代码
代码仍是比较清晰的,bind()方法的流程:github
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
BINDINGS.put(cls, bindingCtor);
复制代码
按照所写的代码,这里会加载一个MainActivity_ViewBinding类,而后获取这个类里面的双参数(Activity, View)构造方法,最后放在BINDINGS里面,它是一个map,主要做用是缓存。在下次使用的时候,就能够从缓存中获取到:缓存
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
复制代码
在上面分析过程当中,咱们知道最后咱们会去加载一个MainActivity_ViewBinding类,而这个类并非咱们本身编写的,而是经过编译时注解(APT - Annotation Processing Tool)的技术生成的。 这一节将会介绍一下这个技术。app
注解其实很常见,好比说Activity自动生成的onCreate()方法上面就有一个@Override注解 框架
通常有些人提到注解,广泛就会以为性能低下。可是真正使用注解的开源框架却不少例如ButterKnife,Retrofit等等。因此注解是好是坏呢? 首先,并非注解就等于性能差。更确切的说是运行时注解这种方式,因为它的原理是java反射机制,因此的确会形成较为严重的性能问题。 可是像Butterknife这个框架,它使用的技术是编译时注解,它不会影响app实际运行的性能(影响的应该是编译时的效率)。 一句话总结:ide
这里要借助到一个类:AbstractProcessor函数
public class TestProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// TODO Auto-generated method stub
return false;
}
}
复制代码
重点是process()方法,它至关于每一个处理器的主函数main(),能够在这里写相关的扫描和处理注解的代码,他会帮助生成相关的Java文件。后面咱们能够具体看一下Butterknife中的使用。post
咱们了解了编译时注解的基本概念以后,咱们先看一下MainActivity_ViewBinding类具体实现了什么。 在编写完demo以后,须要先build一下项目,以后能够在build/generated/source/apt/debug/包名/下面找到这个类,如图所示:
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
// 1
view = Utils.findRequiredView(source, R.id.text, "field 'textView' and method 'textClick'");
// 2
target.textView = Utils.castView(view, R.id.text, "field 'textView'", TextView.class);
// 3
view2131165290 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.textClick();
}
});
}
复制代码
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}
复制代码
看到这里咱们已经解决了第二个问题:到最后仍是会调用findViewById()方法,并无彻底舍弃这个方法,这里的source表明着在上面代码中传入的MainActivity的DecorView。你们能够尝试一下将Activity转化为Fragment的状况~
在这里,咱们解决了第三个问题,绑定各类view时不能使用private修饰,而是须要用public或default去修饰,由于若是采用private修饰的话,将没法经过对象.成员变量方式获取到咱们须要绑定的View。 Util#castView():
public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
try {
return cls.cast(view);
} catch (ClassCastException e) {
String name = getResourceEntryName(view, id);
throw new IllegalStateException("View '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was of the wrong type. See cause for more info.", e);
}
}
复制代码
这里直接调用Class.cast强制转换类型,将View转化为咱们须要的view(TextView)。
view2131165290 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.textClick();
}
});
复制代码
这里会生成一个成员变量来保存咱们须要绑定的View,重点是下面它会调用setOnClickListener()方法,传入的是DebouncingOnClickListener:
/** * A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the * same frame. A click on one button disables all buttons for that frame. */
public abstract class DebouncingOnClickListener implements View.OnClickListener {
static boolean enabled = true;
private static final Runnable ENABLE_AGAIN = new Runnable() {
@Override public void run() {
enabled = true;
}
};
@Override
public final void onClick(View v) {
if (enabled) {
enabled = false;
v.post(ENABLE_AGAIN);
doClick(v);
}
}
public abstract void doClick(View v);
}
复制代码
这个DebouncingOnClickListener是View.OnClickListener的一个子类,做用是防止必定时间内对view的屡次点击,即防止快速点击控件所带来的一些不可预料的错误。我的认为这个类写的很是巧妙,既完美解决了问题,又写的十分优雅,一点都不臃肿。 这里抽象了doClick()方法,实现代码中是直接调用了target.textClick(),这里解决了第四个问题:绑定监听事件的时候方法命名是没有限制的,不必定须要严格命名为onClick,也不必定须要传入View参数。
上文提到,MainActivity_ViewBinding类是经过编译时注解技术生成的,咱们找到Butterknife相关的继承于AbstractProcessor的类,ButterKnifeProcessor,咱们直接看process()方法:
public final class ButterKnifeProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 1
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;
}
}
复制代码
一、findAndParseTargets() 这个方法的做用是处理全部的@BindXX注解,咱们直接看处理@BindView的部分:
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
// 省略代码
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// 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);
}
}
// 省略代码
}
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);
}
复制代码
代码逻辑是处理获取相关注解的信息,好比绑定的资源id等等,而后经过获取BindingSet.Builder类的实例来建立一一对应的关系,这里有一个判断,若是builderMap存在相应实例则直接取出builder,不然经过getOrCreateBindingBuilder()方法生成一个新的builder,最后调用builder.addField()方法。
后续的话返回到findAndParseTargets()方法的最后一部分:
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
// bindView()
// 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;
}
复制代码
这里会生成一个bindingMap,key为TypeElement,表明注解元素类型,value为BindSet类,经过上述的builder.build()生成,BindingSet类中存储了不少信息,例如绑定view的类型,生成类的className等等,方便咱们后续生成java文件。最后回到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;
}
复制代码
最后经过brewJava()方法生成java代码。 这里使用到的是javapoet。javapoet是一个开源库,经过处理相应注解来生成最后的java文件,这里是项目地址传送门,具体技术再也不分析。
这篇文章会同步到个人我的日志,若有问题,请你们踊跃提出,谢谢你们!