ButterKnife源码分析(一)
简介
上篇文章介绍了ButterKnife的使用方法(ButterKnife使用详解跳转链接),这篇文章分析一下ButterKnife的源码。算是ButterKnife的代码阅读笔记。
- 一句话介绍:他可以让你省去让人厌烦的findViewById,借助插件可以实现自动绑定Java和XML
- 源码网址:https://github.com/JakeWharton/butterknife
- 最新版本:8.5.1(最新版本的代码与老代码还是有很大变化的,不过好在基本思路没变)
- 本文内容:本文将分析ButterKnife的源码,让我们看看ButterKnife是怎么通过注解实现findViewById的
目录
- ButterKnife源码分析一
- 简介
- 目录
- 举个栗子
- 问题一 BindView是如何生效的
- 问题二 onClick是如何转换的
- 问题三 注解Optional的作用
- 问题四 ButterKnife在Fragment中有什么不同
- 问题四 为什么注解的View不能是private
- 问题六 unBind干了些什么
- 问题五 ButterKnife的类结构是什么样子的呢
- 问题七 MainActivity_ViewBinding是如何生成的呢
- 总结
举个栗子
要看ButterKnife的源码我们先要找一个切入点,给自己提几个问题。带着问题去看源码,效率会高很多哦。
下面我们来举一个最简单的butterKnife的使用例子。
假设有一个:MainActivity.java
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_content)
TextView tvContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}
对应的xml文件为:activity_main.xml。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!" />
</RelativeLayout>
看完这个例子后,我们的第一个问题来了。
问题一: BindView是如何生效的
换句话说,我们想知道ButterKnife是如何将代码
@BindView(R.id.tv_content)
TextView tvContent;
变换成代码
TextView tvContent = findViewById(R.id.tv_content)
首先我们从代码
ButterKnife.bind(this);
入手
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
代码首先会通过
target.getWindow().getDecorView();
语句获取activity的跟视图decorView。读过view源码的同学知道,activity的根view为名叫mDecor的Framlayout,也就是这里获取到的sourceView。
获取到sourceView后,将target(就是MainActivity对象)与sourceView(MainActivity的跟视图)作为参数createBinding。
这里我们通过返回值可以猜测一下。通过createBinding获取到的是继承了Unbinder接口的一个实例。
再看createBinding(target, sourceView)方法
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
//拿到MainActivity.class
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
//通过targetClass获取一个构造器
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
//使用构造器构造一个实例,参数是target(MainActivity)和source(MainActivity对应的根视图)
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
方法挺长不过,我们真正关心的主要是两句:
通过MainActivity.class获取对应的构造器,看到这里我们可以猜到是通过反射创建Unbinder实例。
果然紧接着就是
但究竟是如何将MainActivity与Unbinder对应的呢?继续看findBindingConstructorForClass(targetClass)
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
//首先从BINDINGS获取Constructor,BINDINGS可以理解为缓存,已经用过的Constructor都会记录在BINDINGS中
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
//如果从BINDINGS找到了Constructor直接返回
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
//通过类名如果发现是android类文件或者java类文件,则返回空。这里可能奇怪为什么会遇到android或者java文件?我们带着疑问继续看
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
//通过反射获取到名叫MainActivity_ViewBinding的类的构造器
Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
//如果MainActivity_ViewBinding这个类没有找到就获取MainActivity.class的父类,继续执行 findBindingConstructorForClass,这里是个递归,这个递归什么时候停止呢?知道遇到android或者java类文件。
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
//最后将这个Constructor放到缓存中,这样再次用到MainActivity_ViewBinding时就可以不用反射,可以提高运行效率
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
findBindingConstructorForClass方法是一个比较核心的方法,我们来总结一下。这个方法的作用就是利用MainActivity.class的类名,找到名叫MainActivity_ViewBinding的类,并创建一个实例。
所以我们可以把原来MainActivity的方法改一下,这里ButterKnife经过不懈努力,将语句
ButterKnife.bind(this)
替换成了
new MainActivity_ViewBinding(this, getWindow().getDecorView());
两句话的效果是一样的,不信可以试试,哈哈。
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_content)
TextView tvContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//ButterKnife.bind(this);
new MainActivity_ViewBinding(this, getWindow().getDecorView());
}
}
继续提问:MainActivity_ViewBinding在哪里?又是什么样子的呢?我们可以搜索一下这个类
看一下这个类里都有什么
// Generated code from Butter Knife. Do not modify!
package com.example.admin.butterknifedemo;
import android.support.annotation.CallSuper;
import android.support.annotation.UiThread;
import android.view.View;
import android.widget.TextView;
import butterknife.Unbinder;
import butterknife.internal.Utils;
import java.lang.IllegalStateException;
import java.lang.Override;
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
target.tvContent = Utils.findRequiredViewAsType(source, R.id.tv_content, "field 'tvContent'", TextView.class);
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.tvContent = null;
}
}
这个类有两个构造方法,和一个unbind函数。我们先看构造方法。不知道大家还记得么,构造方法的两个参数:target(MainActivity) , source (MainActivity的根视图)。 构造方法只有一个方法findRequiredViewAsType,我们看下这个方法做了什么:
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}
调用了两个方法,先看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。。。。哈哈,可以看到,函数的返回值是View,我们需要的是TextView,所以第二个函数castView出场了,这个函数的作用就是类型强制转换。
所以经过了这一系列的转换。我们的MainActivity又可以改一下了。
public class MainActivity extends AppCompatActivity {
TextView tvContent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvContent = (TextView) getWindow().getDecorView().findViewById(R.id.tv_content);
}
}
经过ButterKnife再一次的不懈努力,我们成功的将
ButterKnife.bind()
转换成了:
这里可能有人会问,我们一般在Activity中不会写
getWindow().getDecorView().findViewById(R.id.tv_content)
这么麻烦,只会写
findViewById(R.id.tv_content)
,这里大家可以看下findViewById的源码(如下所示)。我们发现其实两个语句根本是一样的。
@Nullable
public View findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
问题二: @onClick是如何转换的
我们改一下MainActivity:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick(R.id.tv_content)
public void onContentClick() {
}
}
编译一下,看看生成的 MainActivity_ViewBinding:
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.tv_content, "method 'onContentClick'");
view2131427414 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onContentClick();
}
});
}
这里不难理解,只是为什么是
DebouncingOnClickListener
不是
View.OnClickListener
。看看代码:
/**
* A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the
* same frame. A click on one button disables all buttons for that frame.
*/
public abstract class DebouncingOnClickListener implements View.OnClickListener {
static boolean enabled = true;
private static final Runnable ENABLE_AGAIN = new Runnable() {
@Override public void run() {
enabled = true;
}
};
@Override public final void onClick(View v) {
if (enabled) {
enabled = false;
v.post(ENABLE_AGAIN);
doClick(v);
}
}
public abstract void doClick(View v);
}
通过注释不难理解,这个类的作用是当一个Button被按下是,其他的Button都disable。
问题三: 注解@Optional的作用
还是上面的例子
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
//注意这里增加了@Optional注解
@Optional
@OnClick(R.id.tv_content)
public void onContentClick() {
}
}
看下生成的代码
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = source.findViewById(R.id.tv_content);
if (view != null) {
view2131427414 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onContentClick();
}
});
}
}
比之前多了个view空判断。
问题四: ButterKnife在Fragment中有什么不同。
这里我们要看下ButterKnife.bind()方法,之前我们只取了一个例子。这里我们看一下全家福
//适用于activity,view注入
public static Unbinder bind(@NonNull Activity target){
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
//在view中绑定子view使用
public static Unbinder bind(@NonNull View target){
return createBinding(target, target);
}
//适用于Dialog,view注入
public static Unbinder bind(@NonNull Dialog target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
//适用于在Activity外部使用Activity的view
public static Unbinder bind(@NonNull Object target, @NonNull Activity source) {
View sourceView = source.getWindow().getDecorView();
return createBinding(target, sourceView);
}
//适用于Fragment中,view注入
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
return createBinding(target, source);
}
//在dialog外部使用Dialog的view
public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) {
View sourceView = source.getWindow().getDecorView();
return createBinding(target, sourceView);
}
如果大家觉得我写的不够清楚,可以直接看源码注释。
问题四: 为什么注解的View不能是private
我们发现用注解修饰的变量,和方法都不能试private,这是为什么呢,我们可以看一下,MainActivity_ViewBinding的包名,与MainActivity是相同的。而 MainActivity_ViewBinding使用了MainActivity中的tvContent变量和onContentClick方法,如果设置成private会导致编译出错。所以不能试private的。在生成MainActivity_ViewBinding时会逐一检查注解修饰的所有变量和方法是否是private。这部分将在下一篇文章中讲述。
@BindView(R.id.tv_content)
TextView tvContent;
@OnClick(R.id.tv_content)
public void onContentClick() {
}
问题六: unBind干了些什么
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.tvContent = null;
}
主要是将一些view置空,跟上一篇同样的结论,感觉这样做法稍稍有点多余,也许一些特殊的需求会用到吧。
问题五: ButterKnife的类结构是什么样子的呢
呵呵,本文所讲述的源码,除去注解之外一共只有四个类和一个接口,大家看以仔细看一下,代码还是很简单,很清晰的。8.5.1的代码明显比7.0.1版本的代码清晰了很多。
问题七: MainActivity_ViewBinding是如何生成的呢。
这个将放在下一篇博客介绍,原谅我,头都大了,今天就到这吧。
总结
总结一下,ButterKnife的作用就是将ButterKnife.bind(this)转换成了,我们数据的findViewById,所以下次见到ButterKnife.bind(this)时,大脑可以自动将他认为是很多的view自动finderViewById。
另外在整个过程中一次ButterKnife.bind(this)会使用1次反射(同一个类,第二次调用ButterKnife.bind(this)时,就不再用反射了),会影响一点速度。