Butterknife是一个Android View注入框架,配合Android ButterKnife Zelezny插件能够快速的完成View的初始化操作。相比较之前findViewById()方式来获取view的方式,它不仅简洁了代码,提高了编码效率,而且还避免了大量的强转。本文基于ButterKnife7.0.1版本进行分析。
1,ButterKnife的引入和使用
build.gradle文件下添加butterknife的依赖
dependencies {
implementation'com.jakewharton:butterknife:7.0.1'
annotationProcessor 'com.jakewharton:butterknife:7.0.1'
}
需要注意的是我使用的是Android Studio 3.0.1版本,如果你是3.0以下依赖写法不一致,你只需要使用compile'com.jakewharton:butterknife:7.0.1'来添加依赖。
添加完依赖之后,我们来看看ButterKnife的使用
public class MainActivity extends AppCompatActivity {
@Bind(R.id.show_tip_txt)
TextView showTipTxt;
@Bind(R.id.butter_knife_btn)
Button butterKnifeBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick({R.id.butter_knife_btn})
void butterBtn(){
Toast.makeText(this,"点击了我",Toast.LENGTH_LONG).show();
}
@Override
protected void onDestroy() {
super.onDestroy();
ButterKnife.unbind(this);
}
}
在这里我们看到,ButterKnife.bind(this)需要在setContentView()方法后面,并且@Bind的参数不能用private和static修饰,具体原因后面有分析,ButterKnife是通过@Bind是用来绑定View的Id的,此外它还提供了更多的绑定方法提供给我们,如:BindBool,BindColor,BindDimen,BindDrawable,BindInt,BindString,OnClick等。在这里就不一一的介绍了,读者感兴趣的话,可自行实验。
上面的绑定View在Activity中的使用,其实ButterKnife不仅仅可以在Activity中使用,还可以在View,Dialog中使用,我们看看ButterKnife文件中的定义。
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();
}
};
private static T[] filterNull(T[] views) {
int end = 0;
for (int i = 0; i < views.length; i++) {
T view = views[i];
if (view != null) {
views[end++] = view;
}
}
return Arrays.copyOfRange(views, 0, end);
}
当然在我们不需要的时候,我们还需要解绑操作
@Override
protected void onDestroy() {
super.onDestroy();
ButterKnife.unbind(this);
}
2,ButterKnife执行原理
通过上面我们了解到ButterKnife是通过@Bind来绑定View的,先来看看Bind的定义
/**
* Bind a field to the view for the specified ID. The view will automatically be cast to the field
* type.
*
* {@literal @}Bind(R.id.title) TextView title;
*
*/
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
/** View ID to which the field will be bound. */
int[] value();
}
Bind是一个是自定义的annotation文件,需要注意一下它和接口文件的区别:一个@interface ,另外一个interface。
我们先看看Bind上的注解@Rentention,@Target:
@Retention:对Annotation的“生命周期”限制,代表有效期
RetentionPolicy.SOURCE,在源文件中有效
RetentionPolicy.CLASS,在Class文件中有效
RetentionPolicy.RUNTIME,在运行时有效
@Target:规定自定义Annotation所修饰的对象范围
ElementType.CONSTRUCTOR,构造器声明
ElementType.FIELD,成员变量、对象、属性(包括enum实例)
ElementType.LOCAL_VARIABLE,局部变量声明
ElementType.METHOD,方法声明
ElementType.PACKAGE,包声明
ElementType.PARAMETER,参数声明
ElementType.TYPE,类、接口(包括注解类型)或enum声明
默认不设置@Target表示修饰所有。
当你在Build项目工程的时候,
ButterKnife使用了Annotion Processing(注解处理),因此在编译的时候,Annotation会由APT(Annotation Processing Tool)解析。ButterKnife工程中ButterKnifeProcessor类的process()方法会执行以下操作:
A,扫描Java代码中所有的ButterKnife注解@Bind,@OnClick等;
B,当有扫描到为ButterKnife注解的时候,ButterKnifeProcessor会自动生成一个实现了ViewBinder接口的XX$$ViewBinder的java文件,
C,在生成的ViewBinder中,包含了该类所有注解对于的实现,如@Bind会处理成findViewById,@OnClick处理成setOnClickListener等;
在运行代码的时候,遇到ButterKnife.bind()的时候,Butterknife会加载在编译期生成的ViewBinder类,并调用对应的注解如,@Bind等;
在取消注解的时候,会调用到ViewBinder的unbind方法。
public class MainActivity$$ViewBinder implements ViewBinder {
@Override public void bind(final Finder finder, final T target, Object source) {
View view;
view = finder.findRequiredView(source, 2131165287, "field 'showTipTxt'");
target.showTipTxt = finder.castView(view, 2131165287, "field 'showTipTxt'");
view = finder.findRequiredView(source, 2131165219, "field 'butterKnifeBtn' and method 'butterBtn'");
target.butterKnifeBtn = finder.castView(view, 2131165219, "field 'butterKnifeBtn'");
view.setOnClickListener(
new butterknife.internal.DebouncingOnClickListener() {
@Override public void doClick(
android.view.View p0
) {
target.butterBtn();
}
});
}
@Override public void unbind(T target) {
target.showTipTxt = null;
target.butterKnifeBtn = null;
}
}
3,ButterKnife 源码解析
既然View是通过ButterKnife.bind(this)方法来加载ViewBinder类,并调用对应注解的,那我们先看bind(this)方法:
public static void bind(Activity target) {
bind(target, target, Finder.ACTIVITY);
}
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
我们看到这个方法主要是通过传入的参数,然后去找到对应的viewBinder,并绑定注解,这如我们之前所说的一样。那我们在看看它是怎么找到viewBinder的,现在我们进入findViewBinderForClass(targetClass)方法看看:
private static ViewBinder
通过上面的代码,我们看到先会在BINDERS这个Map里面去查找是否有ViewBinder的缓存,如果找到就直接返回viewBinder,如果没有找到就继续判断类名是不是android,或者java开头,接下来会加载自动生成的ViewBinder文件,如果出现ClassNotFoundException,则会继续查找cls.getSuperclass(),最后将viewBinder放入缓存,并返回viewBinder。
if (viewBinder != null) {
viewBinder.bind(finder, target, source);
}
bind()方法中拿到了viewBinder就会去调用viewBinder.bind()方法,也就是自动生成的XX$$ViewBinder中的bind方法。
另外,解绑的时候会最终调用
@Override public void unbind(T target) {
target.showTipTxt = null;
target.butterKnifeBtn = null;
}
分析到这里,基本的绑定/解绑也就搞明白了,现在我们再来看看是如果生成XX$$ViewBinder的方法的。
4,ButterKnife生成XX$$ViewBinder文件
ButterKnife中ButterKnifeProcessor去继承了AbstractProcessor
public final class ButterKnifeProcessor extends AbstractProcessor {}
Annotaion process tool会在编译期自动的查找所有继承了AbstractProcessor类的文件,然后调用它们的process方法去生成java文件。我们先看看,ButterKnife工程中ButterKnifeProcessor类的process()的方法:
@Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) {
//查找并解析注解
Map targetClassMap = findAndParseTargets(env);
for (Map.Entry 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) {
error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
e.getMessage());
}
}
return true;
}
这个方法中,先查找并解析注解,然后再写入文件,那我们先看看findAndParseTargets()方法:
private Map findAndParseTargets(RoundEnvironment env) {
//TypeElement 代表等待注入的类元素,BindingClass 该类包含待注入元素的集合
Map targetClassMap = new LinkedHashMap();
Set erasedTargetNames = new LinkedHashSet();
// Process each @Bind element.处理@Bind
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.处理BindBool
for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
try {
parseResourceBool(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindBool.class, e);
}
}
//...省略其他代码
// Try to find a parent binder for each.尝试去找到父类的绑定
for (Map.Entry entry : targetClassMap.entrySet()) {
String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames);
if (parentClassFqcn != null) {
entry.getValue().setParentViewBinder(parentClassFqcn + SUFFIX);
}
}
return targetClassMap;
}
这个方法,主要去处理不同类型的绑定,以及他们父类的绑定。在这里我们看看,parseBind():
private void parseBind(Element element, Map targetClassMap,
Set erasedTargetNames) {
// Verify common generated code restrictions.
if (isInaccessibleViaGeneratedCode(Bind.class, "fields", element)
|| isBindingInWrongPackage(Bind.class, element)) {
return;
}
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)) {
error(element, "@%s must be a List or array. (%s.%s)", Bind.class.getSimpleName(),
((TypeElement) element.getEnclosingElement()).getQualifiedName(),
element.getSimpleName());
} else {
parseBindOne(element, targetClassMap, erasedTargetNames);
}
}
这个方法里面有两个很重要的判断isInaccessibleViaGeneratedCode(),该方法判断了
1、修饰符不能为private或static;
2、不能用于非Class类;
3、当前类修饰符不能为private
private boolean isInaccessibleViaGeneratedCode(Class extends Annotation> annotationClass,
String targetThing, Element element) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify method modifiers.
Set modifiers = element.getModifiers();
//修饰符不能为private或static
if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
error(element, "@%s %s must not be private or static. (%s.%s)",
annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
// 不能用于非Class类
if (enclosingElement.getKind() != CLASS) {
error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
// Verify containing class visibility is not private.
//当前类修饰符不能为private
if (enclosingElement.getModifiers().contains(PRIVATE)) {
error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
return hasError;
}
在isBindingInWrongPackage()方法中校验类不能以“android.”或者“"java.”开头;
public static final String ANDROID_PREFIX = "android.";
public static final String JAVA_PREFIX = "java.";
private boolean isBindingInWrongPackage(Class extends Annotation> annotationClass,
Element element) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String qualifiedName = enclosingElement.getQualifiedName().toString();
if (qualifiedName.startsWith(ANDROID_PREFIX)) {
error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
annotationClass.getSimpleName(), qualifiedName);
return true;
}
if (qualifiedName.startsWith(JAVA_PREFIX)) {
error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
annotationClass.getSimpleName(), qualifiedName);
return true;
}
return false;
}
回过头去再看看parseBind()方法,里面有parseBindMany()这是绑定的List或者Array,还有一个parseBindOne(),我们来看看:
private void parseBindOne(Element element, Map targetClassMap,
Set erasedTargetNames) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify that the target type extends from View.
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
// Assemble information on the field.
int[] ids = element.getAnnotation(Bind.class).value();
if (ids.length != 1) {
error(element, "@%s for a view must only specify one ID. Found: %s. (%s.%s)",
Bind.class.getSimpleName(), Arrays.toString(ids), enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
if (hasError) {
return;
}
int id = ids[0];
//获取BindingClass
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass != null) {
ViewBindings viewBindings = bindingClass.getViewBinding(id);
if (viewBindings != null) {
Iterator 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 binding = new FieldViewBinding(name, type, required);
// 再将带有@Bind注解的元素信息添加进BindingClass
bindingClass.addField(id, binding);
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement.toString());
}
先获取BindingClass,然后再将带有@Bind注解的元素添加进BindingClass。
我们再来看看获取BindingClass的方法getOrCreateTargetClass
private BindingClass getOrCreateTargetClass(Map 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);
//process方法中一路传递过来targetClassMap,添加BindingClass
targetClassMap.put(enclosingElement, bindingClass);
}
return bindingClass;
}
这个方法内会再一次判断通过targetClassMap获取的BindingClass是否为空,为空的时候会从新new一个BindingClass,并再次添加到targetClassMap。这里的targetClassMap就是process方法传递过来的,最后返回process。到这里ButterKnife的大概流程就完结了。
总结:
ButterKnife使用了注解的方式,在编译的时候,Annotaion process tool会在编译期自动的查找ButterKnifeProcessor文件,然后调用了它的process方法去生成XX$$ViewBinder.java文件。在执行ButterKnife.bind(this)操作的时候,会先去BINDERS(key为class,Value为ViewBinder)中查找,如果没有缓存,则会通过反射的方式去拿到在编译期生成的ViewBinder,真正执行的绑定的操作就是在ViewBinder中。ButterKnife相比与其他的View注入框架,它使用的是注解的方式,虽然在获取生成的ViewBinder类使用的是反射,但是有缓存,这使得ButterKnife执行效率较高。此外,@Bind()等注解不能使用private修饰,是它因为根据Id来获取View,如果使用private就只能通过反射拿到,这会很消耗性能。