超级白话ButterKnife源码
ButterKnife简单使用
class ExampleActivity extends Activity {
@BindView(R.id.title) TextView title;
@BindView(R.id.subtitle) TextView subtitle;
@BindView(R.id.footer) TextView footer;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
首先使用注解标记View,然后再onCreate中setContentView之后调用bind方法,被注解标记的View就可以直接使用了。省去了findViewById的操作,但是是真的省去了吗?那就开始看源码吧~
源码分析
既然是注解标记,那就从注解开始看起。就看BindView这个注解。
注解解析源码
代码1
//Retention标记注解解析的时机,CLASS说明是编译时候生成.class文件,编译时注解。
//Target标记注解修饰的类型,FIELD说明该注解修饰字段
@Retention(CLASS) @Target(FIELD)
public @interface BindViews {
/** View IDs to which the field will be bound. */
//value 是 字段(也就是View)的Id
@IdRes int[] value();
}
既然是编译时注解,那就应该看看编译时注解的使用。这就用到了强大的apt(annotation process tools)。 编译时注解,需要自己写一个类,继承AbstractProcessor这个类,这个类是存在在Java包里的,所以应该使用JavaLibrary开发。这个类有个非常重要的方法,getSupportedAnnotationTypes,通过这个方法的名字可以看出来,是获取这个processor能够支持的注解类型。还有个方法是process。这个方法就是处理被注解标记的element。 所以我们先看下ButterKnife里面实现的Processor类。代码有点长,慢慢看。
我这里用的是8.0.1,butterknife-compiler这个包并没有编译到本地,使用apt “com.jakewharton:butterknife-compiler:8.0.1”进行编译。可以下载源码看。
首先看支持的注解。
代码2
//把注解的名字保存在一个Set里面。看一下它支持的注解,当然我们的BindView也在里面(这是废话,当然必须在里面)
//这个方法最终就是返回了支持类型的Set。看代码3
@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindArray.class);
annotations.add(BindBitmap.class);
annotations.add(BindBool.class);
annotations.add(BindColor.class);
annotations.add(BindDimen.class);
annotations.add(BindDrawable.class);
annotations.add(BindFloat.class);
annotations.add(BindInt.class);
annotations.add(BindString.class);
annotations.add(BindView.class);
annotations.add(BindViews.class);
annotations.addAll(LISTENERS);
return annotations;
}
private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(//
OnCheckedChanged.class, //
OnClick.class, //
OnEditorAction.class, //
OnFocusChange.class, //
OnItemClick.class, //
OnItemLongClick.class, //
OnItemSelected.class, //
OnLongClick.class, //
OnPageChange.class, //
OnTextChanged.class, //
OnTouch.class //
);
代码3
//最关键的是这个方法,为什么这个方法代码这么少。
//其实,所有关键的动作,都是在这里完成的。里面的函数慢慢看。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//首先看下findAndParseTargets()这个方法。转代码4
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
//看完代码4,这里就知道了bindingMap里面保存了关于被注解标记的所有元素的信息
//接下来就是开始解析这些保存的信息了。
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
//生成的文件的信息就都在brewJava这个方法里面了。看下,转代码9
JavaFile javaFile = binding.brewJava(sdk);
try {
//终于写了。这个在源码里追溯的话可以看到文件最开始其实是写了属于哪个包(package ...)还有导入了什么类(import ...)属于的包就是跟被bindView这些注解标记的元素的包。所以可以调用生成的class文件中的方法。也就是我们在声明View的时候,不能够声明成private的。那样就没有权限使用这些方法了。
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
//这里我们就已经把class文件写完了。由于是编译期完成的(编译时注解),没有用到反射,在运行时性能没有影响。接下来就应该看看生成的文件了。
return false;
}
代码4
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
scanForRClasses(env);
//这里省略了一些扫描注解的源码,都是跟下面这个类似
// Process each @BindView element.
//getElementsAnnotatedWith这个方法是获得所有被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 {
//这个方法开始解析,看这个方法,转代码5
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
//这里也省略了一些
// Process each annotation that corresponds to a listener.
for (Class<? extends Annotation> listener : LISTENERS) {
findAndParseListener(env, listener, builderMap, erasedTargetNames);
}
//到这里,builderMap是把所有的被注解标注的信息存起来了。然后开始构建buildingMap。我们看一下这个Map的key-value就可以了。
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) {
//key就是被注解标注的element,value就是BindingSet,用builder模式构建的,我们刚刚在builderMap中只是存了他的Builder,这里挨个build()了。那BindingSet里面的字段大概有个了解了。
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;
}
//看完代码4回去看代码3了
代码5
//首先可以看到,传进来的参数,element是刚刚获得的被BindView注解的对象,bulderMap是findAndParseTargets一开始声明的builderMap,erasedTargetNames也是刚刚的方法声明
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
//这个getEnclosingElement是返回封装此元素的最里层的子元素。
//TypeElement 表示一个类或接口元素,继承了Element
//因为我们用BindView注解标注的是一个View,一个类,所以我们知道这个是TypeElement,所以这里把它强转成了TypeElement
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.
//校验目标对象的类型是继承自View类的。也就是判断使用BindView注解标记的元素是一个View。
//asType()返回此元素定义的类型
//TypeMirror表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。
TypeMirror elementType = element.asType();
//getKind()返回类型的种类,而TypeKind是一个枚举类型。里边包含上面描述的类型。而TYPEVAR是代表了类型变量
if (elementType.getKind() == TypeKind.TYPEVAR) {
//如果是类型变量,那就可以强转成TypeVariable,TypeVariable继承了TypeMirror,所以可以强转。
TypeVariable typeVariable = (TypeVariable) elementType;
//getUpperBound()方法返回类型变量的上边界
elementType = typeVariable.getUpperBound();
}
//getQualifiedName()返回此类型元素的完全限定名称,Name是继承了CharSequence的接口
Name qualifiedName = enclosingElement.getQualifiedName();
//getSimple返回该元素的简单名称
Name simpleName = element.getSimpleName();
//这里是判断,如果既不是一个View,也不是一个接口,那么就直接返回,hasError=true.
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
if (elementType.getKind() == TypeKind.ERROR) {
//这里如果判断是个ERROR类型,就只是打印LOG
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.
//这里终于拿到了BindView标记的View的id
int id = element.getAnnotation(BindView.class).value();
//这里出现一个BindingSet类,还是build模式。我们先不看这个类,就根据方法名字看看能不能看懂在干嘛
//首先从刚刚传进来的bulderMap里get一下,第一次肯定没有的吧,是null.
BindingSet.Builder builder = builderMap.get(enclosingElement);
//这个方法还是看一下。QualifiedId构造函数接收两个参数,一个是包名,一个是id。为什么传了一个element进去。看看到底干了什么勾当。转代码6
QualifiedId qualifiedId = elementToQualifiedId(element, id);
//看完代码6,知道了qualifiedId里面存了element 的包名,还有id
//首先判断builder是不是null,如果第一次,一定是null 啊,看来这里是缓存了下来,不然每次也是挺费劲的。那就看第一次的吧。
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。看看去,转代码7
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
//看完代码7明白了,这里首先在getOrCreateBindingBuilder构建了一个builder,然后将enclosingElement做key,builder做value保存到了builderMap里,最后返回了刚刚构建的builder.
}
String name = simpleName.toString();
//TypeName是javapoet包里的,这里就是获取了elementType类型的名字
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
//还是通过方法的名字看。
//第一个参数是是获取了view的id。第二个参数是用element的name,还有TypeName构建了一个FieldViewBinding的对象。还是看看addField这个方法吧。转代码8
builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
//这里把这个element添加到了erasedTargetNames。然后这个方法就结束了。
erasedTargetNames.add(enclosingElement);
//看完这个方法,我大概知道了builderMap中是有东西了。回过去看代码4
}
代码6
//原来里面做了处理,elementUtils是个Elements,是用来对程序元素进行操作的使用工具方法。在init里面初始化过了。
//getPackageOf可以很好理解了,就是返回element元素的包。
//后面的方法也说过,就是获取全限定名称。
//所以这里就把传进来的element转换成了所在包的全限定名称。回到代码5
private QualifiedId elementToQualifiedId(Element element, int id) {
return new QualifiedId(elementUtils.getPackageOf(element).getQualifiedName().toString(), id);
}
代码7
private BindingSet.Builder getOrCreateBindingBuilder(
Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
//这里还是先看看有没有缓存,从方法名字也可以看出来,应该有这一步,毕竟是getOrCreate嘛
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder == null) {
//这里把用注解标记的元素传进去,构建了一个builder,然后用元素本身做key,builder做value。保存到了builderMap里面。注意,这里是引用传递,也就是直接修改了传进来的参数。回到代码5
builder = BindingSet.newBuilder(enclosingElement);
builderMap.put(enclosingElement, builder);
}
return builder;
}
代码8
//拿到id后是创建了一个ViewBinding.Builder对象,并且还以id作为key,用这个对象作为value,存到了viewIdMap中。接着又setFieldBinding,跟id一样,是ViewBinding.Builder的一个字段。先回到代码5
void addField(Id id, FieldViewBinding binding) {
getOrCreateViewBindings(id).setFieldBinding(binding);
}
private ViewBinding.Builder getOrCreateViewBindings(Id id) {
ViewBinding.Builder viewId = viewIdMap.get(id);
if (viewId == null) {
viewId = new ViewBinding.Builder(id);
viewIdMap.put(id, viewId);
}
return viewId;
}
代码9
//又是Builder模式,那就看这个Builder了。里面有个createType方法,转代码10。
JavaFile brewJava(int sdk) {
return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
//看到代码12.我们没有仔细看,但是类文件中代码的内容应该是构造好了。就等着写了。回到代码3
}
代码10
//哇塞,终于知道你。就是在这里,创建了文件的内容,随便找一个看看。就看看createBindingConstructor,转代码11
private TypeSpec createType(int sdk) {
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));
if (hasViewBindings() || parentBinding == null) {
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
代码11
private MethodSpec createBindingConstructor(int sdk) {
//前面省略了很多
if (hasViewBindings()) {
//前面省略了很多
//就看addViewBinding这个方法,转代码12
for (ViewBinding binding : viewBindings) {
addViewBinding(constructor, binding);
}
//后面省略了很多
}
//后面省略了很多
}
代码12
//大体看一眼,看到了findViewById这个方法,好熟悉啊。总之这个就是在生成类文件中的代码。其实就是用字符串拼起来的。哦,这样子。还是回到代码9吧
private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
if (binding.isSingleFieldBinding()) {
// Optimize the common case where there's a single binding directly to a field.
FieldViewBinding fieldBinding = binding.getFieldBinding();
CodeBlock.Builder builder = CodeBlock.builder()
.add("target.$L = ", fieldBinding.getName());
boolean requiresCast = requiresCast(fieldBinding.getType());
if (!requiresCast && !fieldBinding.isRequired()) {
builder.add("source.findViewById($L)", binding.getId().code);
} else {
builder.add("$T.find", UTILS);
builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
if (requiresCast) {
builder.add("AsType");
}
builder.add("(source, $L", binding.getId().code);
if (fieldBinding.isRequired() || requiresCast) {
builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
}
if (requiresCast) {
builder.add(", $T.class", fieldBinding.getRawType());
}
builder.add(")");
}
result.addStatement("$L", builder.build());
return;
}
//省略了一些
}
生成的文件与ButterKnife源码
首先看一下生成的文件格式
生成的文件
//生成的class文件实现了ViewBinder接口。可以看到ViewBinder接口里面有个bind方法。并且第一个参数是Finder,着我们就应该看看Finder是个什么东西了。转代码13
public class NewsFragment$$ViewBinder<T extends NewsFragment> implements ViewBinder<T> {
@Override
public Unbinder bind(final Finder finder, final T target, Object source){
View view;
//这里就开始找view ,转类型
view = finder.findRequiredView(source, 2131624662, "field 'myView'");
target.svNews = finder.castView(view, 2131624662, "field 'myView'");
}
}
代码13
//这里省略了很多代码
//可以看到,Finder是一个枚举。里面有三种枚举类型。VIEW,ACTIVITY,DIALOG。并且都有了findView,getContext等方法。我们看一下刚刚在生成文件中调用的findRequiredView这个方法。
public enum Finder {
VIEW {
@Override protected View findView(Object source, int id) {
return ((View) source).findViewById(id);
}
@Override public Context getContext(Object source) {
return ((View) source).getContext();
}
@Override protected String getResourceEntryName(Object source, int id) {
final View view = (View) source;
// In edit mode, getResourceEntryName() is unsupported due to use of BridgeResources
if (view.isInEditMode()) {
return "<unavailable while editing>";
}
return super.getResourceEntryName(source, id);
}
},
ACTIVITY {
@Override protected View findView(Object source, int id) {
//在这里才调用了findViewById,不过总算找到了。最后返回这个View。还没有强转。回去看findOptionalView
return ((Activity) source).findViewById(id);
}
@Override public Context getContext(Object source) {
return (Activity) source;
}
},
DIALOG {
@Override protected View findView(Object source, int id) {
return ((Dialog) source).findViewById(id);
}
@Override public Context getContext(Object source) {
return ((Dialog) source).getContext();
}
};
//又调了findOptionalView
public <T> T findRequiredView(Object source, int id, String who) {
T view = findOptionalView(source, id, who);
return view;
}
//这里能区分到底是Activity还是view了。假设source是Activity,那它调用的应该是刚刚ACTIVITY枚举类型的findView,去看看。
public <T> T findOptionalView(Object source, int id, String who) {
View view = findView(source, id);
return castView(view, id, who);
}
//到这里才强转成功
@SuppressWarnings("unchecked") // That's the point.
public <T> T castView(View view, int id, String who) {
try {
return (T) view;
} catch (ClassCastException e) {
}
}
protected String getResourceEntryName(Object source, int id) {
return getContext(source).getResources().getResourceEntryName(id);
}
protected abstract View findView(Object source, int id);
public abstract Context getContext(Object source);
}
看到这里,我们知道了生成文件是怎么工作的,但是我们怎么调用到生成文件中的方法呢。这时候我们回到我们最开始看到的那个ButterKnife.bind方法。 ButterKnife.bind(this); 我们进去看看这个方法。
代码14
//哇,我们看到Finder的类型了。
//其实bind方法有很多重载,这只是其中一个,最终都会调用到bind(target, target, Finder);这个。所以我们看这个
public static Unbinder bind(@NonNull Activity target) {
return bind(target, target, Finder.ACTIVITY);
}
static Unbinder bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {
Class<?> targetClass = target.getClass();
try {
//哦?我们好像找到了。先看下findViewBinderForClass
//看完代码15就很明白了。就是根据传进来的这个Activity或者其他对象的名字来构建了一个对应的ViewBinder实例,也就是我们根据不同的Activity或者Fragment或者其他声明注解的那些类手动写的类的实例。然后再调用他们各自的bind方法。这样target,source,Finder都有了,刚刚的也就都打通了。小总结一下,转小总结
ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
return viewBinder.bind(finder, target, source);
} catch (Exception e) {
throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
}
}
代码15
//从这个方法名字以及参数来看,大概是通过传进来的类,确定ViewBinder的一个实例。
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
throws IllegalAccessException, InstantiationException {
//仍然是做了一个缓存
ViewBinder<Object> viewBinder = BINDERS.get(cls);
if (viewBinder != null) {
if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
return viewBinder;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return NOP_VIEW_BINDER;
}
try {
//刚刚看到了,生成的文件的名字是(Activity或者Fragment或者其他注解所在类的名字 + $$ +ViewBinder)。所以将传进来的Activity的名字加上这个构造了一个类。这里使用反射生成的。对象生成之后就存到了Map里面,防止每次都用反射。
Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
//noinspection unchecked
viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
viewBinder = findViewBinderForClass(cls.getSuperclass());
}
BINDERS.put(cls, viewBinder);
return viewBinder;
}
小总结
ButterKnife的核心就是编译时注解,注解解析器(apt)会解析这些注解,然后手动生成对应的class文件,每个类都会实现ViewBinder这个接口,重写bind这个方法。 findViewById是在枚举类Finder中完成的,里面包含了三个枚举对象,分别是ACTIVITY,VIEW,DIALOG。 当我们在Activity中调用ButterKnife.bind方法的时候,首先通过反射生成对应类的实例。然后会调用对应类的bind方法。在这个方法中传入了Finder,target,source等参数。而根据传入的不同的枚举值,我们就知道调用哪个findViewById。也就不需要自己手动写了。 注意,生成的文件跟原来的文件在一个package下面,所以,被注解标记的View或者方法,都不能是private或者protected修饰的,因为那样生成类中的方法就访问不到了。