ButterKnife源码解析(2)

上一篇文章讲解了ButterKnife在编译阶段通过注解生成java文件,今天需要讲解的是ButterKnife绑定上下文。

一般的,绑定上下文的操作十分简单,在activity中通常是这样的:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);
}

在Fragment中类似下面这样:

public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View view = inflater.inflate(R.layout.fragment_word_detail_display, container, false);
    ButterKnife.bind(this, view);
    return view;
}

在adapter里的ViewHolder里面使用:

static class ViewHolder {

    @Bind(R.id.item_icon)
    ImageView itemIcon;
    @Bind(R.id.note_label)
    TextView noteLabel;

    ViewHolder(View view) {
        ButterKnife.bind(this, view);
    }
}

从上文就可以看出,一般Butterknife通过bind方法绑定上下文,下面我们来阅读源码:

 public static void bind(Activity target) {
      bind(target, target, Finder.ACTIVITY);
}

bind方法:

  static void bind(Object target, Object source, Finder finder) {
  Class<?> targetClass = target.getClass();
  try {
    if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
    ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
    if (viewBinder != null) {
      viewBinder.bind(finder, target, source);
    }
  } catch (Exception e) {
    throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
  }
}

在这个方法里面, 通过findViewBinderForClass(targetClass)获取与此activity相对应的我们通过注解在编译阶段生成java类的实例:

private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
    throws IllegalAccessException, InstantiationException {
  ViewBinder<Object> viewBinder = BINDERS.get(cls);
  if (viewBinder != null) {
    if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
    return viewBinder;
  }
  String clsName = cls.getName();
  if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) {
    if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
    return NOP_VIEW_BINDER;
  }
  try {
    Class<?> viewBindingClass = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
    //noinspection unchecked
    viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
    if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
  } catch (ClassNotFoundException e) {
    if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
    viewBinder = findViewBinderForClass(cls.getSuperclass());
  }
  BINDERS.put(cls, viewBinder);
  return viewBinder;
}

这里的核心代码就是

 Class<?> viewBindingClass = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
//noinspection unchecked
viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();

在这里找到Activity对应的生成java文件的class,然后生成它的实例,最后将该实例以class为键,实例为值放进hashMap里,找到该实例后,那么接下来就应该开始绑定了,因为我们生成的class都实现了ViewBinder接口,所以bind方法会跑到具体实现类的方法里面去:

@Override public void bind(final Finder finder, final T target, Object source) {
  View view;
  view = finder.findRequiredView(source, 2131492943, "field 'textTv'");
  target.textTv = finder.castView(view, 2131492943, "field 'textTv'");
  view = finder.findRequiredView(source, 2131492944, "field 'clickBtn'");
  target.clickBtn = finder.castView(view, 2131492944, "field 'clickBtn'");
}

Finder是个Enum类,在这里我们传的是Finder.ACTIVITY,首先调用 finder.findRequiredView方法:

public <T> T findRequiredView(Object source, int id, String who) {
  T view = findOptionalView(source, id, who);
  if (view == null) {
    String name = getContext(source).getResources().getResourceEntryName(id);
    throw new IllegalStateException("Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' annotation.");
  }
  return view;
}

public <T> T findOptionalView(Object source, int id, String who) {
  View view = findView(source, id);
  return castView(view, id, who);
}

@SuppressWarnings("unchecked") // That's the point.
public <T> T castView(View view, int id, String who) {
  try {
    return (T) view;
  } catch (ClassCastException e) {
    if (who == null) {
      throw new AssertionError();
    }
    String name = view.getResources().getResourceEntryName(id);
    throw new IllegalStateException("View '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was of the wrong type. See cause for more info.", e);
  }
}

在findOptionalView方法里面调用的findView(source,id)最终会走到这里:

 ACTIVITY {
  @Override protected View findView(Object source, int id) {
    return ((Activity) source).findViewById(id);
  }

  @Override public Context getContext(Object source) {
    return (Activity) source;
  }
}

这串代码我们就很熟悉了,就是我们经常使用的findViewById()方法,通过这几个方法ButterKnife终于将我们需要绑定的控件转换成了一个View(没有具体到是什么种类的控件,例如textView,edittext什么的),具体到种类的控件是在第二个方法finder.castView方法实现的,还是以上面的例子说明,target.textTv是TextView,那么castView的返回值就是TextView的具体实例,这个的实现是通过强转实现。

一般的控件绑定就是这样,在这里,还有一个情况就是点击事件的绑定,其实这个也很简单。首先我们一般这样定义点击事件:

 @OnClick(R.id.text_tv)
void textTvClick(){
    Toast.makeText(this,"hello world",Toast.LENGTH_SHORT).show();
}

那么编译时生成的相应代码就是:

View view;
view = finder.findRequiredView(source, 2131492943, "field 'textTv' and method 'textTvClick'");
target.textTv = finder.castView(view, 2131492943, "field 'textTv'");
view.setOnClickListener(
  new butterknife.internal.DebouncingOnClickListener() {
    @Override public void doClick(
      android.view.View p0
    ) {
      target.textTvClick();
    }
  });

DebouningOnClickListener实现了View.OnClickListener的接口,其实现很简单,源代码如下:

public abstract class DebouncingOnClickListener implements View.OnClickListener {
  private 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);
}

到这里,ButterKnife源码就告一段落了,第一次剖析开源框架的源代码,写的有点粗糙,如果有什么不足的话,欢迎批评指正。