在之前简单分析了xUtils的View模块注入,其通过注解,在程序运行时去获取注解的成员及方法,再通过反射及动态代理实现View的注入和监听器的绑定。这些都是在运行过程中进行的,难免会影响程序的性能。
而今天要分析的ButterKnife也是通过注解实现View模块的注入,但不同的是,它是在编译期生成View注入的代码,从而实现注入。也就是通过注解注释将要注入的View和方法,在编译期间生成findViewById(…)和setListener(…)的代码,在编译期间做注解处理,而程序运行时的性能消耗也就很小。
关于ButterKnife的使用就不多详细介绍,可以直接看官方文档
我项目中使用ButterKnife的Activity:
public class MainActivity extends AppCompatActivity {
@Bind(R.id.tv) TextView tv;
@Bind(R.id.btn)Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
tv.setText(".....");
}
@OnClick(R.id.btn)
public void onBtnClick(View view) {
Toast.makeText(MainActivity.this, "Btn click", Toast.LENGTH_SHORT).show();
}
}
生成的代码:
位于项目目录下DemoButterknife\app\build\intermediates\incremental-verifier\debug\com\yzw\demobutterknife
// Generated code from Butter Knife. Do not modify!
package com.yzw.demobutterknife;
import android.view.View;
import butterknife.ButterKnife.Finder;
import butterknife.ButterKnife.ViewBinder;
public class MainActivity$$ViewBinder<T extends com.yzw.demobutterknife.MainActivity> implements ViewBinder<T> {
@Override
public void bind(final Finder finder, final T target, Object source) {
View view;
view = finder.findRequiredView(source, 2131492944, "field 'tv'");
target.tv = finder.castView(view, 2131492944, "field 'tv'");
view = finder.findRequiredView(source, 2131492945, "field 'btn' and method 'onBtnClick'");
target.btn = finder.castView(view, 2131492945, "field 'btn'");
view.setOnClickListener(new butterknife.internal.DebouncingOnClickListener() {
@Override public void doClick(android.view.View p0) {
target.onBtnClick(p0);
}
});
}
@Override
public void unbind(T target) {
target.tv = null;
target.btn = null;
}
}
这里先进行部分解释:view = finder.findRequiredView(source, 2131492944, "field 'tv'")
可以看到根据source即对应Activity或View等目标资源和控件id(来源于R文件中8进制转换而来),找到对应id的控件,再进行强转,这里可以看到为什么要进行再次强转,因为在找到对应View的时候不知道器类型,所以也就需要通过直接赋值实现强转,但是可以看到target.tv,target就是保存对应该控件的目标,从MainActivity$$ViewBinder<T extends com.yzw.demobutterknife.MainActivity>
可以看出该target为对应Activity,类似,也可以是对应View和Dialog。直接通过target.tv = ...
来进行强制,这也为什么通过注解的成员变量不能被private修饰符修饰的原因,想想这可能是一个缺点,可能不符合平时写代码的规范,但是在平时注意下即可。
看下Bind注解,可以知道其生存期在编译期间:
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
/** View ID to which the field will be bound. */
int[] value();
}
看下ViewBinder<T>
接口
/** DO NOT USE: Exposed for generated code. */
public interface ViewBinder<T> {
void bind(Finder finder, T target, Object source);
void unbind(T target);
}
可以看到其生成MainActivity$$ViewBinder
类来实现该接口,在bind(…)方法中生成findViewById(…)和setListener(…)的代码,关于生产的代码中个参数和变量所代表的意义可以见上面代码。
看下Finder(只看主要代码),是个枚举类:
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(); } },
ACTIVITY {
@Override protected View findView(Object source, int id) { 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(); } };
/** * 相当于findViewById,其中source相当于Activity或者View或者Dialog */
public <T> T findRequiredView(Object source, int id, String who) {
T view = findOptionalView(source, id, who);
if (view == null) {
// id错误,抛出参数异常IllegalStateException
// 代码省略...
}
return view;
}
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) {
// 类型转换错误,抛出参数异常IllegalStateException
// 代码省略...
}
}
protected abstract View findView(Object source, int id);
public abstract Context getContext(Object source);
}
从上面可以看出ButterKnife在编译期间生成MainActivity$$ViewBinder
类,其中包括findViewById和setListenr等代码,从而做到真正的简化操作,性能开销方面也不是很大
看到这里,你可能有疑问,ButterKnife是怎么生成上面代码的?
这里主要通过ButterKnifeProcessor
来实现,它继承于AbstractProcessor
,是编译期间的注解处理器,功能非常强大。
主要看下ButterKnifeProcessor
的process(...)
方法,该方法在编译期间执行,可以获取所有相关注解,并做处理,这里便是获取相关@Bind
注解并生成代码
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// K:TypeElement 代表等待注入的类元素
// V:BindingClass 该类包含待注入元素的集合
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
// 循环
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue();
try {
JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
Writer writer = jfo.openWriter();
// 生成相应代码
writer.write(bindingClass.brewJava());
writer.flush();
writer.close();
} catch (IOException e) {
// 抛出异常...
}
}
看下findAndParseTargets
方法,主要是找到所有注解,并整合成Map<TypeElement, BindingClass>
类型
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
// K:TypeElement 代表等待注入的类元素
// V:BindingClass 该类包含待注入元素的集合
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>();
// 存储已经解析的TypeElement(只是标记)
Set<String> erasedTargetNames = new LinkedHashSet<String>();
// Process each @Bind element.
for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
try {
parseBind(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, Bind.class, e);
}
}
// Process each annotation that corresponds to a listener.
for (Class<? extends Annotation> listener : LISTENERS) {
findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
}
// Process each @BindBool element.
for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
try {
parseResourceBool(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindBool.class, e);
}
}
// Process each @BindColor element.
// ...
// Process each @BindDimen element.
// ...
// Process each @BindDrawable element.
// ...
// Process each @BindInt element.
// ...
// Process each @BindString element.
// ...
// Try to find a parent binder for each.
// ...
return targetClassMap;
}
可以看到findAndParseTargets
方法解析各种注解,
先看下parseBind
的主要逻辑
private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<String> erasedTargetNames) {
// 参数判断...
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.ARRAY) {
parseBindMany(element, targetClassMap, erasedTargetNames);
} else if (LIST_TYPE.equals(doubleErasure(elementType))) {
parseBindMany(element, targetClassMap, erasedTargetNames);
} else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) {
// 打出异常信息...
} else {
parseBindOne(element, targetClassMap, erasedTargetNames);
}
}
可以看到parseBind
解析ARRAY
和List
和单个@Bind
,这里主要看下parseBindOne
Set<String> erasedTargetNames) {
// 获取该元素element所在的类enclosingElement
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 参数检查...
// Assemble information on the field.
int[] ids = element.getAnnotation(Bind.class).value();
// 参数检查...
int id = ids[0];
// 下面代码逻辑:
// 根据Map<K,V> targetClassMap来获取对应的BindingClass
// 再将带有@Bind注解的元素信息添加进BindingClass
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass != null) {
ViewBindings viewBindings = bindingClass.getViewBinding(id);
if (viewBindings != null) {
Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
if (iterator.hasNext()) {
FieldViewBinding existingBinding = iterator.next();
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
Bind.class.getSimpleName(), id, existingBinding.getName(),
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
}
} else {
bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
}
String name = element.getSimpleName().toString();
String type = elementType.toString();
boolean required = isRequiredBinding(element);
// 创建FieldViewBinding代编一个View的注入,并添加到BindingClass中
FieldViewBinding binding = new FieldViewBinding(name, type, required);
bindingClass.addField(id, binding);
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement.toString());
}
private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
TypeElement enclosingElement) {
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass == null) {
String targetType = enclosingElement.getQualifiedName().toString();
String classPackage = getPackageName(enclosingElement);
String className = getClassName(enclosingElement, classPackage) + SUFFIX;
bindingClass = new BindingClass(classPackage, className, targetType);
targetClassMap.put(enclosingElement, bindingClass);
}
return bindingClass;
}
在这里,看下构造BindingClass的方法getOrCreateTargetClass
结合上面分析,可以看到BindingClass
代表的是生成的MainActivity$$ViewBinder
类的信息
private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
TypeElement enclosingElement) {
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass == null) {
String targetType = enclosingElement.getQualifiedName().toString();
String classPackage = getPackageName(enclosingElement);、
// SUFFIX = "$$ViewBinder"
String className = getClassName(enclosingElement, classPackage) + SUFFIX;
bindingClass = new BindingClass(classPackage, className, targetType);
targetClassMap.put(enclosingElement, bindingClass);
}
return bindingClass;
}
到这里,先来理解一下其他类
从上面可以看到FieldViewBinding
类代表绑定view的类型和字段名(如上面的tv和btn)
看下上面的BindingClass的addField
void addField(int id, FieldViewBinding binding) {
getOrCreateViewBindings(id).addFieldBinding(binding);
}
BindingClass的getOrCreateViewBindings
生成一个ViewBindings:其根据id代表View所绑定的相关信息,比如有监听器方法,字段类型FieldViewBinding
等
private ViewBindings getOrCreateViewBindings(int id) {
// 先查找是否会该id的ViewBingdings类,否则则创建
ViewBindings viewId = viewIdMap.get(id);
if (viewId == null) {
viewId = new ViewBindings(id);
viewIdMap.put(id, viewId);
}
return viewId;
}
到这里,小结一下BindingClass、ViewBinds、FieldViewBinding
MainActivity$$ViewBinder
类的信息到这里@Bind
注解的解析大致了解了一下
现在来看下关于监听器的解析
private void findAndParseListener(RoundEnvironment env,
Class<? extends Annotation> annotationClass, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) {
// 先通过env.getElementsAnnotatedWith(annotationClass)
// 获取所有该注解的元素(即方法),再调用parseListenerAnnotation解析
for (Element element : env.getElementsAnnotatedWith(annotationClass)) {
try {
parseListenerAnnotation(annotationClass, element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
// 输出异常信息
}
}
}
parseListenerAnnotation
方法有200多行,所以只看重点:
private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) throws Exception {
// 根据注解获取各种相关信息,以及各种类型检查
MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required);
BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
for (int id : ids) {
if (!bindingClass.addMethod(id, listener, method, binding)) {
//输出异常信息...
}
}
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement.toString());
}
可以看到其根据注解生成一个MethodViewBinding
,再加之添加到BindingClass中。相信从名字上大家也可以知道MethodViewBinding
代表各空间监听器的方法。这里简单了解一下:
new MethodViewBinding(name, Arrays.asList(parameters), required);
第一个参数代表可监听器相应的方法(为字符串类型),第二个参数为该相应方法待传入的参数,第三个参数为是否执行(默认为true)
根据上面View跟监听器的绑定,同理,关于资源的绑定的思路也是一样,这里只分析布尔资源的获取:
private void parseResourceBool(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<String> erasedTargetNames) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 信息换取,类型检查...
BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
FieldResourceBinding binding = new FieldResourceBinding(id, name, "getBoolean");
bindingClass.addResource(binding);
erasedTargetNames.add(enclosingElement.toString());
}
可以看到关于资源信息的类FieldResourceBinding
,其封装了资源id、字段名和相应获取资源方法"getBoolean"
看到应该知道Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
Map集合代表什么了吧。
那么有了这里信息,怎么生成源代码呢:
JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
Writer writer = jfo.openWriter();
writer.write(bindingClass.brewJava());
可以看到,通过filer.createSourceFile
创建一个文件(即 MainActivity$$ViewBinde
相关文件),再通过bindingClass.brewJava()
生成代码片段并写入
看下brewJava()
String brewJava() {
StringBuilder builder = new StringBuilder();
builder.append("// Generated code from Butter Knife. Do not modify!\n");
builder.append("package ").append(classPackage).append(";\n\n");
if (!resourceBindings.isEmpty()) {
builder.append("import android.content.res.Resources;\n");
}
if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
builder.append("import android.view.View;\n");
}
builder.append("import butterknife.ButterKnife.Finder;\n");
if (parentViewBinder == null) {
builder.append("import butterknife.ButterKnife.ViewBinder;\n");
}
builder.append('\n');
builder.append("public class ").append(className);
builder.append("<T extends ").append(targetClass).append(">");
if (parentViewBinder != null) {
builder.append(" extends ").append(parentViewBinder).append("<T>");
} else {
builder.append(" implements ViewBinder<T>");
}
builder.append(" {\n");
emitBindMethod(builder);
builder.append('\n');
emitUnbindMethod(builder);
builder.append("}\n");
return builder.toString();
}
可以看到跟前面的MainActivity$$ViewBinde
格式一模一样,emitBindMethod(builder)
方法则生成对应的Bind方法
而emitUnbindMethod(builder);
方法则生成对应的unBind
方法
现在各种注解信息都是BindingClass中,就是只是生成findViewById和setListener方法了
ButterKnife的使用是在Activity的onCreate()方法中调用ButterKnife.bind(this);
其最终会调用下面方法:
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,即MainActivity$$ViewBinder,
// 随后调用其bind方法来执行findViewById和setListerner
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);
}
}
可以看到程序最终在bind(...)
方法调用了findViewById和setListener方法
在理解ButterKnife之前我是抵制使用注解来注入的,但是理解了ButterKnife后,我发现到了注解的魅力所在。
到这里,ButterKnife源码分析告一段落,相信你可以看到ButterKnife发挥注解的强大之处,能够将烦躁的findViewById等代码在编译期间生成出来,提高了开发效率,何乐而不为,但是使用时不能关会用,要知道原理,这也是很重要的。
最后,关于本文,如有不足之处或者错误的地方,欢迎指出,谢谢。