1. 简介
分析完Java中的反射(有关Java反射的使用看这一篇就够了)和注解(Java注解全面总结),如果就这样结束了,总感觉缺少了些什么,不拿一个开源库来分析分析,然后动手实践一遍,怎么能体会到反射和注解的博大精深呢。于是想到了Android中的一个开源库ButterKnife,用法简单,为我们减少了大量垃圾代码(例如findViewById)。本篇文章首先从ButterKnife的基本用法入手,然后着手分析ButterKnife的实现原理,最后自己动手撸一把,实现最简单的替换findViewById功能,能看到开源库并不代表就会了,只有自己动手实现一遍,才能够真正掌握,希望阅读完本篇文章,能够给大家一定的技术提升。
2. 基本用法
本篇文章重点在于ButterKnife的源码分析和动手实践,只是简单介绍下最基本用法,对于更多的用法细节,不在讨论范围内。
2.1 ButterKnife项目地址
https://github.com/JakeWharton/butterknife
2.2 基本配置
百度相关文章,看到很多博客在写AS配置ButterKnife的时候,都说首先需要在Project 的 build.gradle中添加相关代码,然后在APP的build.gradle中添加 apply plugin ……, 最后在dependencies中添加相关依赖等等,其实并非如此,虽然这样配置是不影响最终的结果的,但还是有些多余,或者说对于Module是application和Module是library,两者的配置是有些不一样的,可以参考官方文档。
1) Module为application的配置
如果你仅仅是在Module为application的模块下使用ButterKnife,那么配置非常简单,只需要在Module下的build.gradle文件中添加如下代码即可:
dependencies { ......... compile 'com.jakewharton:butterknife:8.8.1' annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' }
2) Module为library的配置
如果你的Module为library,配置稍微复杂一点,其实也非常简单,首先在Project下的build.gradle添加如下代码:
buildscript { repositories { jcenter() } dependencies { ............ classpath 'com.jakewharton:butterknife-gradle-plugin:8.8.1' } }
在library的build.gradle文件中添加如下代码:
apply plugin: 'com.android.library' apply plugin: 'com.jakewharton.butterknife' // 新增
dependencies { .................. compile 'com.jakewharton:butterknife:8.8.1' annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1' }
2.3 基本使用
ButterKnife的使用十分简单,只是在Module是application和Module是library稍有不同,下面为Module是application的最简单使用方式:
public class ButterKnifeTestActivity extends AppCompatActivity {
@BindView(R.id.tv_show_info)
TextView tvInfo;
public static void startActivity(Context context){
Intent intent = new Intent(context, ButterKnifeTestActivity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_butter_knife_test);
//绑定初始化ButterKnife
ButterKnife.bind(this);
initView();
}
private void initView(){
tvInfo.setText("测试信息");
}
}
布局文件非常简单,此处就省略了,当进入该activity,textview显示的信息为 测试信息。如果Module是library的情况,需要将@BindView(R.id.tv_show_info) 中的R 修改为 R2即可,官方文档也有相关说明。
3. ButterKnife的实现原理
了解了基本的使用方法,有没有很想知道它的实现原理呢?
文章主要就是从以上基本使用发放入手,一步步探索实现该功能的基本原理,以上用法实在是很简单。
一个使用到ButterKnife.bind(this);另一个就是用到注解@BindView(R.id.tv_show_info) TextView tvInfo;
- @BindView(R.id.tv_show_info) TextView tvInfo
首先我们来看看BindView注解:
@Retention(CLASS) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface IdRes {
}
注解十分简单,BindView为编译时注解,作用域在字段,对其成员变量再次使用注解,对于BindView我们暂且分析到这里。看到是编译型注解,我们是否可以大胆想象一下,ButterKnife通过编写型注解然后结合JavaPoet生成某些class类,对View进行绑定呢,下面我们来看看ButterKnife.bind(this)具体做了些什么。
- ButterKnife.bind(this)
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView(); // 获取当前activity的顶层view
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);
}
................
}
代码首先获取传入activity的顶层view视图对象,然后调用createBinding函数,createBinding中又调用了findBindingConstructorForClass函数,返回一个构造器,看过我前面的有关反射的文字,就知道通过构造器可以实例化对应的对象,事实却是如此,后面调用constructor.newInstance(target, source)函数,构造了具体的实例对象。到这里我们不得不看看findBindingConstructorForClass具体做了些什么事了。
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); // 首先从一个全局的Map中获取,也是为了性能优化
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName(); // 获取传入activity的完整类名(包括包名)
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
// 排除系统自带的类
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
// 此处是最关键的地方了,通过类加载器加载另外一个类,类名为传入的 类_ViewBinding
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
// 通过反射获取该类中具有两个参数的构造器,一个参数为传入的activity,另一个为View
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);
}
// 最后将找到的构造器和传入的class类进行绑定,方便下次进来无需进行下面的一系列反射操作
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
代码中已经做了很清晰的注释,最终通过反射,获取传入类的类名加上一个“_ViewBinding”这样一个类的class对象,然后在获取该类中的有参构造,最后返回。
到这里逻辑依然不复杂,ButterKnife.bind(this)最终的意义就是调用类名+“_ViewBinding”的有个有参构造,生成一个实例对象。我们来看看这个类名+“_ViewBinding”的类到底是何方神圣。我这里仍然是以我上面的Demo为例来查看。
通过搜索或者在build/generated/source/apt/debug下可以看到ButterKnifeTestActivity_ViewBinding这个类(注意一定要先编译整个工程后,才会有)。
public class ButterKnifeTestActivity_ViewBinding implements Unbinder {
private ButterKnifeTestActivity target;
@UiThread
public ButterKnifeTestActivity_ViewBinding(ButterKnifeTestActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public ButterKnifeTestActivity_ViewBinding(ButterKnifeTestActivity target, View source) {
this.target = target;
target.tvInfo = Utils.findRequiredViewAsType(source, R.id.tv_show_info, "field 'tvInfo'", TextView.class);
}
@Override
@CallSuper
public void unbind() {
ButterKnifeTestActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.tvInfo = null;
}
}
我们看到ButterKnifeTestActivity_ViewBinding(ButterKnifeTestActivity target, View source)这个构造函数里面的内容,其实上面通过反射调用的构造函数也就是改函数,看里面的代码是否很诧异,尽然有这么一个类为我们定义的activity进行遍历赋值。findRequiredViewAsType这个函数就没必要讲了,返回一个TextView赋值给target的tvInfo。
看到这里大家是否想起了点什么? 我们在自己的activity中对某一个变量使用BindView注解,是不能声明为private的,由于此处直接调用了变量名,这就是原因所在。
分析到此处,那相信大家都了解到,使用BindView注解,在编译期会给我们生产“类名+_ViewBinding”类,那么该类是如何生成的呢?
看过我前面一篇有关注解的文章相信已经了解了,这里再次说明下整个流程。
- 使用到编译型注解,通常情况下回结合注解处理器一起使用;
- 如果需要通过代码额外生成Java类,通常会只用开源库JavaPoet;
那么我们来看看ButterKnife是否用到了这两点技术:在ButterKnife源代码中在project下的build.gradle中定义
javapoet: 'com.squareup:javapoet:1.10.0',
在butterknife-compiler module下的build.gradle文件中添加了依赖:api deps.javapoet, 同时我们也可以butterknife-compiler module下找到ButterKnifeProcessor这个注解处理器类,看到process函数:
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) { Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env); for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingSet binding = entry.getValue(); JavaFile javaFile = binding.brewJava(sdk, debuggable, useAndroidX); try { javaFile.writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage()); } } return false; }
该函数作用就是生成对应的Java代码,此处不在详细分析。
关于ButterKnife的基本原理就分析到这里,当然这只是ButterKnife的冰山一角,但是实现原理基本相同。
4. 自定义注解实现ButterKnife简单功能
ButterKnife采用的是编译时注解,为什么采用编译时注解呢?这里给我自己一点看法,大家都知道Java反射是一个相对比较耗性能的操作,正常情况下,如果可以减少使用反射那当然是不去使用,因此ButterKnife最终采用编译时注解,这样大大减少反射的使用,仅仅只是在构造ViewBinding对象时使用,当然另一方面同时又增加了很多class类文件,只是这对性能的影响及apk包大小的影响非常小。
在这里,为了简单化,我采用的是运行时注解来实现ButterKnife的简单功能。代码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyBindView {
int value();
}
public class MyButterKnife {
public static void bind(Activity activity){
Class c = activity.getClass();
Field[] fields = c.getFields();
for (Field field:fields) {
MyBindView myBindView = field.getAnnotation(MyBindView.class);
if(myBindView != null){
try {
field.set(activity, activity.findViewById(myBindView.value()));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
首先定一个运行是注解,作用域为字段(变量)。然后在定一个MyButterKnife类,实现bind函数,bind函数中主要通过反射获取当前对象中哪些变量使用了MyBindView注解,由于是通过getFields来获取变量,因此变量名必须声明为public类型,然后获取到对应的注解值,通过set函数设置到具体的变量上。下面看看使用的代码:
public class MyButterKnifeTestActivity extends AppCompatActivity {
@MyBindView(R.id.tv_show_info)
public TextView tvShowInfo;
public static void startActivity(Context context){
Intent intent = new Intent(context, MyButterKnifeTestActivity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_butter_knife_test);
MyButterKnife.bind(this);
initView();
}
private void initView(){
tvShowInfo.setText("重新设置显示信息");
}
}
可以正常运行,显示没问题。是不是非常简单,当然我这只是简单的实现,有些地方考虑也不太周全,仅仅只是用来学习。至此,文章分析到这里就结束了,希望对大家有所帮助。