ButterKnife 从入门到精通 - 源码级分析 (一)

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

《ButterKnife 从入门到精通 - 源码级分析 (一)》

logo

说明

因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();
  }
}

上面的例子主要做了以下四件事:

  1. 在onCreate()中绑定当前Activity
  2. 在类中通过注解的方式声明了当前页面中的button
  3. 通过注解的方式为button设置了点击监听
  4. 最后,在onDestroy()中取消了绑定

我们来看一下效果,

《ButterKnife 从入门到精通 - 源码级分析 (一)》

我是一个小例子

如何绑定视图

没有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;
}

流程图

基本的执行流程如下

《ButterKnife 从入门到精通 - 源码级分析 (一)》

流程图

关于 MainActivity_ViewBinding 类

作者:“哈哈哈,到这里,咱们就走通了一遍使用ButterKnife绑定一个button,并为其设置一个点击事件,最后再解绑的流程了。大家伙,走过路过不要忘记点赞啊!!!”
读者:“你TM少糊弄我,你还没说这个MainActivity_ViewBinding是怎么来的呢!!!”
作者:“额,这个,这个,我们下次再讨论如何生成MainActivity_ViewBinding类。”
其实这个MainActivity_ViewBinding是ButterKnife通过我们写的注解帮助我们动态生成的。限于篇幅的原因,我们留到下篇文章继续分析。

题外话

调用ButterKnife的bind方法最后生成的类居然叫Unbinder,每次调用时,都觉得有些异样,真是逼疯处女座。

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/58eb29edb123db1ad0670dad
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞