一,写在前面
这篇文章将介绍ButterKnife框架的一些基本使用,并会介绍ButterKnife的实现原理。由于ButterKnife是以注解的形式被使用,阅读本篇文章需要读者有Java注解相关的知识。对Java注解不太了解的可参考文章Java注解,文章中涉及注解的知识将不再阐述。
本文将包括如下内容:
- ButterKnife框架背景介绍
- 为什么要使用ButterKnife
- ButterKnife的基本使用
- ButterKnife原理浅析
二,轻松聊背景
ButterKnife是Square公司前工程师Jake Wharton,开发的一款Android框架,主要用于替换传统的findViewById获取控件引用,以及setOnClickListener...等设置监听的方法。在离开伟大的Square公司前,Jake Wharton经常被google邀请参加一些技术分享,然后结局嘛~顺利被挖走啦。于2017年正式入职Google,一家更伟大的公司。为啥说square是一家伟大的公司呢,因为如今很多常用的开源框架都是该公司开源的,大大方便了开发者。
三,凭什么使用ButterKnife
不知道大家有木有发现,在一个项目越来越大界面越来越多时,每当我们写好了界面,就要在Activity里敲那些重复到吐的findViewByid。俺在工作中就遇到领导要求写个测试接口的apk,而且一看接口有十多个,需要写十多个按钮,findViewByid,setOnClickListener十多次,有种写吐的感觉。那么,使用ButterKnife框架就可以不用写findViewById,setOnClickListener这些重复的代码,当然ButterKnife的用处远不止这些。
对注解的同学一定知道,注解放在一个元素上面就像该元素的一个标签,注解在编译,运行阶段本身没有任何效果。要想注解生效,必须要人为的处理注解,有两种方式:反射提取注解,注解处理器生成辅助类。文章Java注解里介绍了如何使用反射提取注解,这里不再阐述,注解处理器在后面讲到实现原理这块会做简单的介绍。我们知道用反射提取注解有个缺点:使用反射“曲线救国”的方式调用方法,不如直接调用方法快。且反射是在运行阶段执行,运行阶段花费太多时间,会消耗应用一定性能。
也就是说,反射是在运行阶段处理注解对性能灰常不友好,那么能不能将处理注解的操作放在编译阶段呢?答案是,可以,注解处理器就是做的这件事。注解处理器会对整个源码的特定注解(ButterKnife规定使用的注解)进行解析,并将数据存放在Map集合中,最终通过JavaPoet开源库提供的API将集合里数据转化为Java源代码。也就是说,注解处理器生成了Java源码,该Java文件在module特定的目录中(后面会具体介绍),这些Java源码都是一些辅助类,用于帮助开发者完成findViewById等操作。可想而知,ButterKnife框架处理注解是在编译阶段,即采用注解处理器生成一些辅助类,减少性能的损耗。
四,ButterKnife的使用
使用ButterKnife就是使用一堆注解,可使用注解包括:BindView,BindViews,BindColor,BindDimen,BindDrawable,BindString,OnClick,onLongClick...等,完整注解参考ButterKnife注解。
在使用ButterKnife框架前,需要在app的build.gradle文件里添加一些依赖:
dependencies {
//...
implementation 'com.jakewharton:butterknife:8.5.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}
Activity中使用ButterKnife
@BindView注解:绑定View
首先来看下面一个activity_main.xml布局文件,代码如下:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="www.abc.com.butterknife.MainActivity">
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="14sp"
android:text="@string/textview_name"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button_name"
android:layout_below="@+id/text1"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp"/>
<EditText
android:id="@+id/edittext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/button"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp"
android:text="@string/edittext_name"/>
<ImageView
android:id="@+id/iv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:src="@drawable/ic_launcher_background"/>
</RelativeLayout>
以往获取控件的引用,都是在setContentView方法后调用findViewById方法。那使用ButterKnife框架该怎么用呢,直接上代码:
public class MainActivity extends Activity {
@BindView(R.id.text1)
TextView text1;
@BindView(R.id.button)
Button button;
@BindView(R.id.edittext)
EditText edit_text;
@BindView(R.id.iv)
ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}
第4行:定义一个TextView text1,并在该元素上面添加注解@BindView,注解属性的值为R.id.text1;
第19行:在setContentView方法后调用ButterKnife.bind(this);
如上两个步骤,即替代以往TextView text1 = (TextView)findViewById(R.id.text1)。使用框架后不再需要写findViewById,不需要强制向下转型,直接定义一个控件的引用,并在其上添加注解@BindView(...)。
需要注意,如果使用private,static修饰控件的引用,Build->Make Project编译后会报如下错误:
错误: @BindView fields must not be private or static. (www.abc.com.butterknife.MainActivity.text1)
意思是:添加@BindView注解的字段,不可以使用private,static修饰,具体原因会在分析原理时做出解答。
@BindString,@BindColor,@BindDrawable...绑定资源文件
获取一个字符串资源,需要调用res.getString方法;获取颜色资源,需要调用res.getColor方法;获取图片资源,需要调用res.getDrawable方法。使用ButterKnife框架,分别使用注解@BindString,@BindColor,@BindDrawable替换,注解属性值为资源的id。
代码如下:
@BindString(R.string.app_name)
String app_name;
@BindDrawable(R.drawable.ic_launcher_background)
Drawable d;
@BindColor(R.color.red)
int color_red;
同样,在setContentView方法后调用ButterKnife.bind(this)。
@onClick设置控件的监听
以往给一个View添加点击事件的监听,需要调用方法View$setOnClickListener(OnClickListener l),需要创建OnClickListener接口的子类,并重写onClick方法。
使用ButterKnife框架后,代码简化如下:
@OnClick({R.id.button, R.id.text1})
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
text1.setTextColor(Color.RED);
break;
case R.id.text1:
text1.setTextColor(Color.BLUE);
break;
default:
break;
}
}
第2行:方法名onClick可以随意定义,方法内参数可写,可不写,根据需要进行添加,非常灵活。
第1行:在方法上面添加@onClick注解,由于该注解的属性值是int类型的数组,当同时给R.id.button,R.id.text1添加事件监听时,属性的值用大括号包起来,中间用逗号隔开。关于注解的基础知识,可以参考文章Java注解。
ViewHolder中使用ButterKnife
ButterKnife不仅可以在Activity里绑定View,还可以在ViewHolder,Fragment中使用。我们知道ViewHolder经常用在ListView,RecyclerView中,用于封装item里控件的引用。
栗子
接下来看一下传统方式,使用ListView+ViewHolder的代码:
private class MyBaseAdapter extends BaseAdapter {
@Override
public int getCount() {
return 10;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v;
ViewHolder holder = null;
if (convertView != null) {
v = convertView;
holder = ((ViewHolder) v.getTag());
} else {
v = View.inflate(getApplicationContext(), R.layout.list_item, null);
holder = new ViewHolder();
holder.tv = v.findViewById(R.id.item_text);
holder.btn = v.findViewById(R.id.item_button);
v.setTag(holder);
}
holder.tv.setText("TextView " + position);
holder.btn.setText("button " + position);
return v;
}
}
static class ViewHolder {
TextView tv;
TextView btn;
}
第28,29行:使用findViewById方法获取控件引用。
采用ButterKnife框架替换后,代码如下:
private class MyBaseAdapter extends BaseAdapter {
@Override
public int getCount() {
return 10;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v;
ViewHolder holder = null;
if (convertView != null) {
v = convertView;
holder = ((ViewHolder) v.getTag());
} else {
v = View.inflate(getApplicationContext(), R.layout.list_item, null);
holder = new ViewHolder(v);
v.setTag(holder);
}
holder.tv.setText("TextView " + position);
holder.btn.setText("button " + position);
return v;
}
}
static class ViewHolder {
@BindView(R.id.item_text) TextView tv;
@BindView(R.id.item_button) Button btn;
public ViewHolder(View view) {
ButterKnife.bind(this, view);
}
}
第37,38行:用法同Activity;
第41行:ButterKnife有多个bind的重载方法,查看该bind方法源码:
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
return createBinding(target, source);
}
第一个参数target,传入的是ViewHolder的实例。第二个参数source,传入需要绑定的View,在这里对应item的布局资源返回的View对象。所以,第27行需要通过ViewHolder的有参构造方法,给ViewHolder注入View对象。
Fragment中使用ButterKnife
那么在Fragment中如何使用ButterKnife框架呢,使用方式跟Activity中一样,区别在于需要调用ButterKnife$bind的重载方法。
代码如下:
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.frag_res, container, false);
Unbinder unbinder = ButterKnife.bind(this, view);
//...
}
第3行,第一个参数target,传入Fragment的实例。第二个参数source,传入fragment对应资源文件返回的View对象。
bind方法返回了一个Unbinder对象,在回调onDestroyView方法时,调用unbind方法释放资源。
代码如下:
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
@BindViews的使用
ButterKnife有很多注解,这里再介绍一个@BindViews注解的使用。当同一类型的多个View,例如两个TextView控件,需要做同样的操作时,可以使用BindViews进行统一管理。
@BindViews({R.id.frag_text, R.id.frag_text1})
List<TextView> views;
private Unbinder unbinder;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.frag_res, container, false);
unbinder = ButterKnife.bind(this, view);
ButterKnife.Action<TextView> mAction = new ButterKnife.Action<TextView>() {
@Override
public void apply(@NonNull TextView view, int index) {
view.setTextColor(Color.BLUE);
}
};
ButterKnife.Setter<TextView, Float> mSetter = new ButterKnife.Setter<TextView, Float>() {
@Override
public void set(@NonNull TextView view, Float value, int index) {
view.setAlpha(value);
}
};
//对两个TextView进行同样的操作
ButterKnife.apply(views, mAction);
ButterKnife.apply(views, mSetter, 0.2f);
ButterKnife.apply(views, View.SCALE_X, 2f);
return view;
}
第2行:定义一个存储TextView的List集合容器;
第1行:在List集合上添加注解@BindViews,该注解的属性为int类型的数组,可以存放多个TextView的id值。
第10行:创建一个ButterKnife.Action的匿名子类对象,重写apply方法,;
第13行:设置所有的TextView的文字为blue;
第17行:创建一个ButterKnife.Setter的匿名子类对象,重写set方法;
第20行:给所有的TextView设置透明度为value变量的值;
第25,26,27行,apply三个重载方法,查看其源码如下:
public static <T extends View> void apply(
@NonNull List<T> list,
@NonNull Action<? super T> action) {
for (int i = 0, count = list.size(); i < count; i++) {
action.apply(list.get(i), i);
}
}
public static <T extends View, V> void apply(
@NonNull List<T> list,
@NonNull Setter<? super T, V> setter,
V value) {
for (int i = 0, count = list.size(); i < count; i++) {
setter.set(list.get(i), value, i);
}
}
public static <T extends View, V> void apply(
@NonNull List<T> list,
@NonNull Property<? super T, V> setter,
V value) {
//noinspection ForLoopReplaceableByForEach
for (int i = 0, count = list.size(); i < count; i++) {
setter.set(list.get(i), value);
}
}
ButterKnife框架提供了三个重载方法,分别支持Action接口,Setter接口,Property实例。
上述代码执行后,两个TextView的文字颜色为blue,透明度为0.2f,x方向比例扩大1倍。
效果如下:
五,ButterKnife实现原理浅析
使用一些注解就可以替换findViewById,setOnClickListener这些传统的方式,那ButterKnife框架是如何实现的呢?前面有讲到,ButterKnife框架处理注解是在编译阶段,使用注解处理器生成一些辅助类,帮助开发者完成相关的操作。从前面ButterKnife的基本使用可以知道,注解与Activity产生关联必然是调用了ButterKnife$bind方法的缘故。
查看ButterKnife$bind源码:
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
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);
}
}
第2行:获取Activity资源文件对应视图的父窗口DecorView,DecorView是一个FrameLayout布局。
第3行:继续调用createBinding方法,第一个参数target是MainActivity的实例,第二个参数source是DecorView对象;
第7行:获取MainActivity的Class实例;
第9行:获取某一个类的构造器对象,后面会继续分析方法findBindingConstructorForClass;
第17行:调用该构造器对应的构造函数,第一个参数为MainActivity的实例,第二个参数为DecorView的实例,并返回一个unBinder接口的子类对象。
继续查看ButterKnife$findBindingConstructorForClass方法的源码:
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
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) {
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);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
第2行:BINDINGS是一个Map集合,key是MainActivity的Class实例,value是前面提到的“某一个类”的构造器实例;
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
第5行:如果bindingCtor不为空,则直接在Map集合里取出来,Map集合用于缓存“某一个类”的构造器实例。
注意:只有第一次调用ButterKnife$bind方法才会使用一次反射获取这个构造器实例,当Map集合中有该构造器的缓存时,会直接从缓存中取。前面提到ButterKnife没有使用反射技术,那为啥这里使用了反射技术呢?要知道是大量的反射调用才会损失“可观”的性能,Only once,对性能影响很小。
第8行:检查MainActivity的包名,是否以原生API的android,Java开头;
第13行:拼接一个字符串"包名.MainActivity_ViewBinding",它就是前面提到的“某一个类”;
第15行:获取MainActivity_ViewBinding类的构造器实例,第一个参数为Class类型,第二个参数为View类型;
那么,这个MainActivity_ViewBinding类在哪呢?选择project视图,点击build->generated->source->apt->debug。
构造方法源码如下:
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131165315;
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.text1, "field 'text1' and method 'onClick'");
target.text1 = Utils.castView(view, R.id.text1, "field 'text1'", TextView.class);
view2131165315 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onClick(p0);
}
});
}
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.text1 = null;
view2131165315.setOnClickListener(null);
view2131165315 = null;
}
}
第1行:MainActivity_ViewBinding实现了接口Unbinder,会重写unbind方法,该方法里用于释放对象资源。
第14,15行:继续查看Utils$findRequiredView,Utils$castView源码如下:
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
//...code
}
public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
try {
return cls.cast(view);
} catch (ClassCastException e) {
//...code
}
}
第2行:调用了source.findViewById(id),并返回view;
第12行:cls参数传入的TextView.class,于是将View强制向下转型为TextView类型;
看到这里是不是恍然大悟,框架居然是调用findViewById方法,并强制向下转型获取View的引用。ButterKnife只是生成了一个辅助类MainActivity_ViewBinding,该类封装了获取控件引用的过程。
继续回到MainActivity_ViewBinding的构造方法
第14行:就是findViewByid,前面已经分析;
第15行:强制向下转型为TextView,前面已经分析。注意target.text1,如果text1被private修饰,target.text1将出现编译错误。
第20行:设置监听器DebouncingOnClickListener后,onClick方法里调用了target.onClick(p0)。可以发现ButterKnife仍然是使用setOnClickListener设置监听,只是做了一个封装,让开发者不用再写一大堆setOnClickListener等方法。
第28,30,32,33行:注销监听器,释放对象资源;
六,注解处理器
前面提到,ButterKnife框架生成了一个辅助类MainActivity_ViewBinding,那么该类是如何生成的呢?答案是:注解处理器在编译阶段生了该辅助类。实现一个注解处理器,需要继承抽象类AbstractProcessor,并重写init,getSupportedAnnotationTypes,getSupportedSourceVersion,getSupportedOptions,process五个方法。在process方法中,会对应用源代码中ButterKnife相关的注解进行扫描并解析其内容,将相关数据存放在一个Map集合中,最后使用Java Poet提供的JavaFile类,将集合中数据串接成字符串生成Java文件。
关于注解处理器,有一篇翻译自国外大神的文章:Java注解处理器(建议用chrome浏览器打开链接)
O(∩_∩)O