天天看点

ButterKnife源码分析(一)ButterKnife源码分析(一)

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在哪里?又是什么样子的呢?我们可以搜索一下这个类

ButterKnife源码分析(一)ButterKnife源码分析(一)

看一下这个类里都有什么

// 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的类结构是什么样子的呢

ButterKnife源码分析(一)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)时,就不再用反射了),会影响一点速度。