平方X 发表于 2018-4-18 12:10:01

[2528]ButterKnife 学习


Q:
* 注入的实现原理
* 不同 context 的注入 √
* 字段和点击事件的注入区别 √
* 为什么不能 private √
* 为什么使用 R2 √
* DebouncingOnClickListener 的使用 √

当前学习版本为 8.8.1,为了测试在 module 中的使用,又退回了 8.4.0
注入的使用将再另一篇文章中介绍
# 0x00 官网介绍
(https://github.com/JakeWharton/butterknife)
> Field and method binding for Android views which uses annotation processing to generate boilerplate code for you.

> Remember: A butter knife is like a dagger only infinitely less sharp.
这可能是它的命名原因?

> Instead of slow reflection, code is generated to perform the view look-ups. Calling bind delegates to this generated code that you can see and debug.
后续解析 private

# 0x01 基本使用
## 1.1 结合 kotlin 的使用
新项目其实可以不使用 butterknife 的,为了学习使用一下。
```
apply plugin: 'kotlin-kapt'

dependencies {
    ...
    compile "com.jakewharton:butterknife:$butterknife-version"
    kapt "com.jakewharton:butterknife-compiler:$butterknife-version"
}
@BindView(R2.id.title)
lateinit var title: TextView

@OnClick(R2.id.hello)
internal fun sayHello() {
    Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show()
}
```

## $app_debug
一开始点击事件无效,看了生成的代码,点击事件名后面多了 $app_debug
虽然后来发现只是因为忘了写 ButterKnife.bind 方法(教程中没写,一下子忘了),但还是不知道这个名字后面为什么加了 $app_debug 也能调用。

## 1.2 官网提供了哪些用法
(http://jakewharton.github.io/butterknife/)

1. RESOURCE BINDING
绑定资源
0. NON-ACTIVITY BINDING
绑定任意的 view root
0. VIEW LISTS
组合多个 view ,可以使用 apply 方法
0. LISTENER BINDING
监听
0. BINDING RESET
Fragment 中返回 Unbinder 可用于 unbind
0. OPTIONAL BINDINGS
使用 @Nullable 或 @Optional 注解,尤其注意在可能有继承的类中
0. MULTI-METHOD LISTENERS
含多个方法的监听

# 0x02 bind 源码查看
   
      @NonNull @UiThread
      public static Unbinder bind(@NonNull Activity target) {
      View sourceView = target.getWindow().getDecorView();
      return createBinding(target, sourceView);
      }
      butterknife.ButterKnife#createBinding 方法接受一个 target 和一个 source 参数,相关的 bind 方法重载即为获取这两个参数。
      其中 activity 和 dialog 均为 target.getWindow().getDecorView();
      
      
      private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
      Class<?> targetClass = target.getClass();
      Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
      try {
          return constructor.newInstance(target, source);
      }
      }
      可以看到,是查找构造函数,然后创建实例。
      
      @Nullable @CheckResult @UiThread
      private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
      //获取缓存
      Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
      if (bindingCtor != null) {
          return bindingCtor;
      }
      String clsName = cls.getName();
      //终止搜索
      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
          //获取构造函数
          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);
      }
      //加入缓存
      BINDINGS.put(cls, bindingCtor);
      return bindingCtor;
      }
      
从 bind 的源码,我们知道了
Q:为什么可以在基类中封装 ButterKnife.bind
因为 bind 方法在执行时 getClass 获取当前类(子类)名,而生成的文件是根据子类生成不同的 _ViewBinding 类。

Q:不同 context 的注入
只是取 sourceView 的不同

# 0x03 生成代码的查看
## 3.1 构造方法
    public final class MainActivity_ViewBinding implements Unbinder {
      private MainActivity target;

      private View view2131165320;

      @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.tv, "field 'tv' and method 'sayHello$app_debug'");
      //转换
      target.tv = Utils.castView(view, R.id.tv, "field 'tv'", TextView.class);
      //赋值
      view2131165320 = view;
      //点击事件
      view.setOnClickListener(new DebouncingOnClickListener() {
          @Override
          public void doClick(View p0) {
            target.sayHello$app_debug();
          }
      });
      }

      @Override
      public void unbind() {
      MainActivity target = this.target;
      if (target == null) throw new IllegalStateException("Bindings already cleared.");
      this.target = null;

      target.tv = null;

      view2131165320.setOnClickListener(null);
      view2131165320 = null;
      }
    }
## 3.2 查找
包含 findRequiredView 和 findOptionalViewAsType 方法
查找时即简单地 findViewById ,who 用来报错

      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.");
      }
      
## 3.3 转换
调用 java.lang.Class#cast 方法

      public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
      try {
          return cls.cast(view);
      } catch (ClassCastException e) {
          String name = getResourceEntryName(view, id);
          throw new IllegalStateException("View '"
            + name
            + "' with ID "
            + id
            + " for "
            + who
            + " was of the wrong type. See cause for more info.", e);
      }
      }
      
## 3.4 赋值
直接等号赋值的,这也是原文中说的
>Instead of slow reflection, code is generated to perform the view look-ups.

没有用反射,通过代码生成的。
也是为什么不能用 private 的原因,方法同理

## 3.5 点击事件 DebouncingOnClickListener
防止重复点击,查了一下相关方法
* 使用 throttleFirst
* 记录点击时间

以及这里的 DebouncingOnClickListener
TODO 完成一个防止重复点击的 view 和一个可以多次点击的 view


    /**
   * 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);
    }

# 0x04 在模块中使用

    项目中
    classpath 'com.jakewharton:butterknife-gradle-plugin:8.8.1'
   
    模块中
    apply plugin: 'com.jakewharton.butterknife'
   
    使用 R2 替换

使用时遇到了
(https://github.com/JakeWharton/butterknife/issues/963)

> Error:Unable to find method 'com.android.build.gradle.api.BaseVariant.getOutputs()Ljava/util/List;'.

退回了 8.4.0 测试成功
页: [1]
查看完整版本: [2528]ButterKnife 学习