Butterknife 源码学习

学习Butterknife的源码,为了弄明白以下问题:Butterknife是怎么做到替换findViewById的?绑定的onclick方法为什么不能是private修饰的,必须是public或者default的?

一、我们在使用butterknife的时候,使用方法如下:

  • 1、在project的build.gradle中添加如下代码:

classpath 'com.jakewharton:butterknife-gradle-plugin:8.8.1'

  • 2、在project的build.gradle中添加如下代码:

apply plugin: 'com.jakewharton.butterknife'

如图所示:



  • 3、在activity的oncreate方法中绑定,

ButterKnife.bind(this);

在fragment中绑定:


要在oncreateView中绑定,在ondestoryView中解绑

二、从butterknife的bind方法入口进行分析:

/**
 * BindView annotated fields and methods in the specified {@link Activity}. The current content
 * view is used as the view root.
 *
 * @param target Target activity for view binding.
 */
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
  View sourceView = target.getWindow().getDecorView();
  return createBinding(target, sourceView);
}
/**
 * BindView annotated fields and methods in the specified {@code target} using the {@code source}
 * {@link View} as the view root.
 *
 * @param target Target class for view binding.
 * @param source View root on which IDs will be looked up.
 */
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
  return createBinding(target, source);
}

再看createBinding方法:

private static Unbinder createBinding(@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;
  }

  //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
  try {
    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);
  }
}
 
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
  if (bindingCtor != null) {
    if (debug) Log.d(TAG, "HIT: Cached in binding map.");
    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 {
    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方法的流程如下:

1、首先获取当前activity的sourceView,其次获取activity的decorView,decorView是整个viewTree的最顶层View,包含contentView和titleview两个元素,我们平时调用的setContentView就是往contentView中添加元素

2、其次,调用createBinding方法--》findBindingConstructorForClass方法,在findBindingConstructorForClass中,会调用

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);

按照以上代码,这里会加载一个MainActivity_ViewBinding类,然后从这个类中获取双参数(activity,view)的构造方法,最后放在BINGINGS里面,BINDINGS是一个map,主要作用是缓存,在下次使用的时候,就可以从缓存中直接获取了。

@VisibleForTesting
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
  if (debug) Log.d(TAG, "HIT: Cached in binding map.");
  return bindingCtor;
}

三、关于编译时注解:

从上面的分析可以知道,最后会去加载一个MainActivity_ViewBinding类,这个类不是我们自己编写的,而是通过编译时注解技术生成的。

1、什么是注解:

注解其实很常见,比如说Activity自动生成的onCreate()方法上面就有一个@Override注解。

  • 注解的概念:
    能够添加到 Java 源代码的语法元数据。类、方法、变量、参数、包都可以被注解,可用来将信息元数据与程序元素进行关联。
  • 注解的分类:
    • 标准注解,如Override, Deprecated,SuppressWarnings等
    • 元注解,如@Retention, @Target, @Inherited, @Documented。当我们要自定义注解时,需要使用它们
    • 自定义注解,表示自己根据需要定义的 Annotation
  • 注解的作用:
    • 标记,用于告诉编译器一些信息
    • 编译时动态处理,如动态生成java代码
    • 运行时动态处理,如得到注解信息

2、注解的优劣:

一般有些人提到注解,普遍就会觉得性能低下。但是真正使用注解的开源框架却很多例如ButterKnife,Retrofit,greenDAO等等。所以注解是好是坏呢?
首先,并不是注解就等于性能差。更确切的说是运行时注解这种方式,由于它的原理是java反射机制,所以的确会造成较为严重的性能问题。
但是像Butterknife这个框架,它使用的技术是编译时注解,它不会影响app实际运行的性能(影响的应该是编译时的效率)
一句话总结:

  • 运行时注解就是在应用运行的过程中,动态地获取相关类,方法,参数等信息,由于使用java反射机制,性能会有问题;
  • 编译时注解由于是在代码编译过程中对注解进行处理,通过注解获取相关类,方法,参数等信息,然后在项目中生成代码,运行时调用,其实和直接运行手写代码没有任何区别,也就没有性能问题了。

3、如何使用编译时注解技术:

这里需要借助到一个类--AbstractProcesser

 
public class TestProcessor extends AbstractProcessor
{
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
// TODO Auto-generated method stub
return false;
}
}

重点是process()方法,它相当于每个处理器的主函数main(),可以在这里写相关的扫描和处理注解的代码,他会帮助生成相关的Java文件(来源于链接:https://xudeveloper.github.io/2017/12/17/Butterknife%208.8.1%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/)

四、进一步分析MainActivity_ViewBinding类:

在编写完demo后,先build一下项目,在build/generated/source下可以找到这个类

按照上面的分析,最后会通过反射的方式去调用这个类的构造方法,因此直接看这个类的构造方法:

@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_click, "field 'btnClick' and method 'onViewClicked'");
  target.btnClick = Utils.castView(view, R.id.btn_click, "field 'btnClick'", Button.class);
  view2131165219 = view;
  view.setOnClickListener(new DebouncingOnClickListener() {
    @Override
    public void doClick(View p0) {
      target.onViewClicked();
    }
  });
}

再查看Utils的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方法,并没有完全舍弃此方法,这里的source就是上面传入进来的activity的 decorView

继续查看castView方法:

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

发现,这里直接调用了class的cast方法的强制类型转换,将view转换为我们需要的View,如果采用private修饰的话,将无法通过对象.成员变量的方法获取到我们需要绑定的View。

view2131165219 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
  @Override
  public void doClick(View p0) {
    target.onViewClicked();
  }
});

这里传入了一个成员变量来保存我们需要绑定的变量view,view调用setonclicklistener方法,传入一个debouncingOnclickListener对象,

/**
 * 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);
}
这个DebouncingOnClickListener是View.OnClickListener的一个子类,作用是防止一定时间内对view的多次点击,即防止快速点击控件所带来的一些不可预料的错误。个人认为这个类写的非常巧妙,既完美解决了问题,又写的十分优雅,一点都不臃肿。
这里抽象了doClick()方法,实现代码中是直接调用了target.onViewClicked()来实现。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值