ButterKnife 笔记

283 阅读4分钟

ButterKnife一个使用了非常久的库,今天抽空理了一下逻辑。先从编写一个小demo开始吧~

ButterKnife版本


  
    @OnClick(R.id.tvHello)
    public void changeText() {
        Toast.makeText(this, "1111", Toast.LENGTH_LONG).show();
    }

    @OnClick(R.id.tvHello2)
    public void changeText2() {
        Toast.makeText(this, "2222", Toast.LENGTH_LONG).show();
    }

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

原生版本

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //获取控件,设置监听
        findViewById(R.id.tvHello).setOnClickListener(view -> {
            Toast.makeText(this, "1111", Toast.LENGTH_LONG).show();
        });
        findViewById(R.id.tvHello2).setOnClickListener(view -> {
            Toast.makeText(this, "2222", Toast.LENGTH_LONG).show();
        });
    }

可以看到帮ButterKnife我们省去获取控件的过程。当我们需要给控件设置监听的时候,不需要先获取控件,然后在设置监听。这样的模板代码。(当然这只是冰山一角)

再看看原生的版本中注释的这几行代码,它的职责很单一,就是获取控件、设置监听。那么我们能不能把它抽离在一个单独的类中专门获取控件和设置监听呢?让我们尝试处理一下。

public class MainActivityViewBinding {

    public MainActivityViewBinding(MainActivity activity) {
        this(activity, activity.getWindow().getDecorView());

    }
    public MainActivityViewBinding(MainActivity activity, View root) {
        root.findViewById(R.id.tvHello).setOnClickListener(view -> {
            Toast.makeText(activity, "1111", Toast.LENGTH_LONG).show();
        });
        root.findViewById(R.id.tvHello2).setOnClickListener(view -> {
            Toast.makeText(activity, "2222", Toast.LENGTH_LONG).show();
        });
    }
}

这样,我们就将获取控件和设置控件的逻辑单独抽离在一个类上面了。这个时候你只需要在MainActivity中实例化这个控件即可。

public class MainActivity extends AppCompatActivity {

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       new MainActivityViewBinding(this);
   }
}

但是这样还存在有一些问题。

  1. MainActivityViewBinding 里面获取控件的代码每一个界面都需要手动去编写。能不能自动生成呢?
  2. 更多时候,我们的控件点击不仅仅是Toast,有可能是跳转、调用P层的代码进行操作,我们能不能将点击的事件传递给Activity来处理呢?
  3. 每次都手动实例化MainActivityViewBinding很繁琐,能不能简单一点呢?

带着这三个问题,让我们一起揭开ButterKnife的面纱。在开始之前,我们先要理解一个概念Annotation Processing

Annotation Processingjava编译器编译过程的一个钩子,用于分析用户定义的注释和句柄的源代码。(其实就是可以分析你的源代码然后进行加工操作)为什么要介绍这个呢? 因为ButterKnife使用到这种技术。ButterKnife在编译的时候,会在同包下生成一个类,然后来用封装一些模板代码(类似于上述的MainActivityViewBinding),让我一起来看看ButterKnife生成的代码吧。

image.png

上面是ButterKnife生成的文件路径,下面是代码。

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view7f07008e;

  @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.tvHello2, "method 'changeText2'");
    view7f07008e = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        //直接调用
        target.changeText2();
      }
    });
  }

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


    view7f07008e.setOnClickListener(null);
    view7f07008e = null;
  }
}

可以看到,ButterKnife生成的类跟我自己写的有点类似。解决了我们上述所说的模板代码和事件传递的问题。从上面的代码中,我们可以看到ButterKnife属于同包下,所以我们平时声明private的权限的时候,会出错。

我们已经知道了ButterKnife生成的类,那在什么时候调用呢?别忘了,我们在使用ButterKnife的时候经常会先调用ButterKnife.bind(this)这中间到底发生什么事呢?

  @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

     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 || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      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.");
    } 
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

上面删了一些容错的代码。 大概就是你在调用bind的时候,它先会获取到你这个类的名称,然后手动加上后缀_ViewBinding,然后利用ClassLoader将类动态加载进虚拟机。然后利用反射调用构造方法。中间会有缓存的操作。其实重点就两行代码

Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
constructor.newInstance(target, source)

这样完成了我们上述的第三个问题,那就是 每次都手动实例化MainActivityViewBinding,利用反射,我们就可以将动态实例化。

以上!!!