上篇文章介绍了ButterKnife的使用方法(ButterKnife使用详解跳转链接),这篇文章分析一下ButterKnife的源码。算是ButterKnife的代码阅读笔记。
一句话介绍:他可以让你省去让人厌烦的findViewById,借助插件可以实现自动绑定Java和XML源码网址:https://github.com/JakeWharton/butterknife最新版本:8.5.1(最新版本的代码与老代码还是有很大变化的,不过好在基本思路没变)本文内容:本文将分析ButterKnife的源码,让我们看看ButterKnife是怎么通过注解实现findViewById的要看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>看完这个例子后,我们的第一个问题来了。
换句话说,我们想知道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); } }方法挺长不过,我们真正关心的主要是两句:
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);通过MainActivity.class获取对应的构造器,看到这里我们可以猜到是通过反射创建Unbinder实例。 果然紧接着就是
return constructor.newInstance(target, source);但究竟是如何将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()转换成了:
tvContent = (TextView) getWindow().getDecorView().findViewById(R.id.tv_content);这里可能有人会问,我们一般在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); }我们改一下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。
还是上面的例子
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.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); }如果大家觉得我写的不够清楚,可以直接看源码注释。
我们发现用注解修饰的变量,和方法都不能试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() { }主要是将一些view置空,跟上一篇同样的结论,感觉这样做法稍稍有点多余,也许一些特殊的需求会用到吧。
呵呵,本文所讲述的源码,除去注解之外一共只有四个类和一个接口,大家看以仔细看一下,代码还是很简单,很清晰的。8.5.1的代码明显比7.0.1版本的代码清晰了很多。
这个将放在下一篇博客介绍,原谅我,头都大了,今天就到这吧。
总结一下,ButterKnife的作用就是将ButterKnife.bind(this)转换成了,我们数据的findViewById,所以下次见到ButterKnife.bind(this)时,大脑可以自动将他认为是很多的view自动finderViewById。
另外在整个过程中一次ButterKnife.bind(this)会使用1次反射(同一个类,第二次调用ButterKnife.bind(this)时,就不再用反射了),会影响一点速度。