真的,这次我们不用再放弃了。

说明
因butterKnife不同版本之间的实现存在一定的差异,本文分析的源码来自最新版本-8.5.1,若小伙伴们在阅读的过程中也想自己进行调试,最好保持版本一致。
使用
ButterKnife的使用是极其简单的,并且省去了我们的大量的绑定视图、设置监听的操作。下面就给出一个简单的例子。
public class MainActivity extends AppCompatActivity {
//2. 在类中通过注解的方式声明了当前页面中的button
@BindView(R.id.btn)
Button button;
private Unbinder bind;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1. 在onCreate()中绑定当前Activity
bind = ButterKnife.bind(this);
}
//3. 通过注解的方式为button设置了点击监听
@OnClick({R.id.btn})
void onClick(View view) {
switch (view.getId()) {
case R.id.btn:
Toast.makeText(this,"Hello ButterKnife",Toast.LENGTH_SHORT).show();
break;
}
}
@Override
protected void onDestroy() {
//4. 最后,在onDestroy()中取消了绑定
bind.unbind();
super.onDestroy();
}
}
上面的例子主要做了以下四件事:
- 在onCreate()中绑定当前Activity
- 在类中通过注解的方式声明了当前页面中的button
- 通过注解的方式为button设置了点击监听
- 最后,在onDestroy()中取消了绑定
我们来看一下效果,

如何绑定视图
没有findViewById(),ButterKnife到底是如何绑定视图的?
我们从ButterKnife.Bind(this)开始,点开这个方法。
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
//1.通过MainActiviy获取DecorView
View sourceView = target.getWindow().getDecorView();
//2.调用createBinding()方法,得到Unbinder对象并返回
return createBinding(target, sourceView);
}
跟着它,我们再点开createBinding()方法,看它有哪些处理。
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
//1.获取Activity对应的Class
Class<?> targetClass = target.getClass();
//2.获取构造器类
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
//返回一个默认的对象
return Unbinder.EMPTY;
}
//3.执行构造器方法
return constructor.newInstance(target, source);
}
这里,我去除了一些打印日志和异常处理的代码,剩下的就是我们看到的,第一步和第三步不用说,第二步到底是什么鬼,获取的对象的泛型很奇怪啊。
带着疑问,再点开findBindingConstructorForClass()方法,来看看到底做了哪些事情。
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
//1.在map中查找是否有对应的数据
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
return bindingCtor;
}
//2.获取全类名,如果是“android.”或“java.”开始的,直接返回null
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
return null;
}
try {
//3.获取clsName+"ViewBinding"类的Class,反射获取它的构造器
Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
//4.如果报ClassNotFound异常,递归查找他的父类
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
//5.将获取的Constructor<? extends Unbinder>放入map中,并返回
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
去除一些日志打印,我们看到这个方法的内容也不长,必要的地方我也简单的加上了注释。
首先来看第一步,结合下面一行代码可以看出,BINDINGS是一个Map类型的静态的类的成员变量,:
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
再结合第五步,我们可以很清楚的看出,这一步是为了性能优化,通过存入容器的方式做个缓存,下次再bind的时候,直接取出,这也是很多框架的源码中常用的方式。
接着来看第二步,获取全类名后,接着判断是不是以"android."或"java."开始的。如果是,直接返回null,这一步整的有点懵,我好好的一个Activity,包名怎么会以"android."或者"java."开头呢,先不管它,我们继续往下看。
走到第三步,也是整个方法的精华所在,在当前的demo中,这个clsName就是MainActivity,但是MainActivty_ViewBinding又是个什么类。心理上,我对这个类是抵触的,我希望它就是抛了ClassNotFoundException,这样就走到了第四步。
那好,我们先来看看第四步,获取MainActivity的父类后,继续执行当前方法,这是个递归方法,要一直执行下去,那这个递归方法的终止条件是什么呢?这时,再看看第二步的代码,原来它是有用的,当MainActivity的继承树上的某个父类的包名是以"android."或"java."开始的,就停止继续执行。
回过头来,我们再看第三步,通过在Android Studio中连续两次按下shift键的方式来找到MainActivity_ViewBinding类。
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131427414;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.btn, "field 'button' and method 'onClick'");
target.button = Utils.castView(view, R.id.btn, "field 'button'", Button.class);
view2131427414 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onClick(p0);
}
});
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.button = null;
view2131427414.setOnClickListener(null);
view2131427414 = null;
}
}
在findBindingConstructorForClass()方法中通过反射获取了MainActivity_ViewBinding类的Class对象,再获取它的Constructor对象并强转成Constructor<? extends Unbinder>类型最后返回,这里需要一定的反射和泛型的知识。
终于结束了findBindingConstructorForClass()方法,我们再回到createBinding()方法中,在该方法的第三步中,有如下代码
//3.执行构造器方法
return constructor.newInstance(target, source);
这句代码的含义就是执行MainActivity_ViewBinding类的构造方法,得到一个MainActivity_ViewBinding的对象,返回后赋值给了bind,在MainActivity_ViewBinding的构造方法中,我们找到了如下代码
view = Utils.findRequiredView(source, R.id.btn, "field 'button' and method 'onClick'");
再点开findRequiredView()方法,
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()了,终于找到你。底下的一些异常处理的,增加框架健壮性的代码,我们就不继续看了。
回到MainActivity_ViewBinding的构造器中,我们继续往下看,
target.button = Utils.castView(view, R.id.btn, "field 'button'", Button.class);
这里的target就是MainActivity,通过castView()方法将上一步获取的view强转成button,这时我们MainActivity中的button就有值了。
呼~,长呼一口气,终于跑通了findViewById的逻辑了,我们为button 绑定上了视图。
点击监听的绑定
继续往下走,看到这样的代码:
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onClick(p0);
}
});
一目了然,设置了一个点击监听,并回调给MainActivity中的onClick()方法处理。
ButterKnife解绑
在MainActivity的Destroy中,我们调用了bind.unbind()。bind就是一个MainActivity_ViewBinding的对象,在它的unbind()方法中,做了一些防止内存泄露的处理。
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.button = null;
view2131427414.setOnClickListener(null);
view2131427414 = null;
}
流程图
基本的执行流程如下

关于 MainActivity_ViewBinding 类
作者:“哈哈哈,到这里,咱们就走通了一遍使用ButterKnife绑定一个button,并为其设置一个点击事件,最后再解绑的流程了。大家伙,走过路过不要忘记点赞啊!!!”
读者:“你TM少糊弄我,你还没说这个MainActivity_ViewBinding是怎么来的呢!!!”
作者:“额,这个,这个,我们下次再讨论如何生成MainActivity_ViewBinding类。”
其实这个MainActivity_ViewBinding是ButterKnife通过我们写的注解帮助我们动态生成的。限于篇幅的原因,我们留到下篇文章继续分析。
题外话
调用ButterKnife的bind方法最后生成的类居然叫Unbinder,每次调用时,都觉得有些异样,真是逼疯处女座。
网友评论