ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码
Posted Y_ZhiWen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码相关的知识,希望对你有一定的参考价值。
简介
在之前简单分析了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
的主要逻辑
解析@Bind注解
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
- BindingClass:代表的是生成的
MainActivity$$ViewBinder
类的信息 - ViewBinds:代表一个控件(id)的相关信息,有该控件字段信息,监听器方法
- FieldViewBinding :代表某控件的类型和字段名
到这里@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集合代表什么了吧。
- TypeElement:代表使用了ButterKnife注解的元素信息,可以把它理解成一个Activity或者View
- BindingClass:代表该元素(Activity或者View)中,注解的使用信息,如上面分析的View注入信息,监听器信息,资源信息等。
那么有了这里信息,怎么生成源代码呢:
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等代码在编译期间生成出来,提高了开发效率,何乐而不为,但是使用时不能关会用,要知道原理,这也是很重要的。
最后,关于本文,如有不足之处或者错误的地方,欢迎指出,谢谢。
以上是关于ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码的主要内容,如果未能解决你的问题,请参考以下文章
ButterKnife -- 源码分析 -- 在‘编译期’间生成findViewById等代码