ButterKnife(二): 原理解析篇

在上一篇文章ButterKnife使用篇里面已经详细介绍过如何使用ButterKnife了,本篇文章将分析它的原理,了解该工具是如何实现绑定控件的. 本文分析基于ButterKnife版本
'com.jakewharton:butterknife:10.2.1'

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(bindLayoutID());
        mBind = ButterKnife.bind(this);
        initView(savedInstanceState);
    }

@Override
    protected void onDestroy() {
        super.onDestroy();
        if (mBind != null && mBind != Unbinder.EMPTY) {
            mBind.unbind();
            mBind = null;
        }
    }

 @Override
    protected int bindLayoutID() {
        return R.layout.activity_main;
    }

这是我们使用ButterKnife时所写的代码,那么我们就从 ButterKnife.bind(this)入手,去一步步查看源码

  /**
   * 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 bind(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) {
    Class targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    Constructor 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);
    }
  }

第一个单参数bind(@NonNull Activity target)方法中,获取了当前window的顶级父View,它是一个FrameLayout,然后调用Unbinder bind(@NonNull Object target, @NonNull View source)方法,这个方法中主要是通过Class类获取构造器,并获取一个新的实例.下面我们继续查看findBindingConstructorForClass方法和constructor.newInstance(target, source)方法

  @VisibleForTesting
  static final Map, Constructor> BINDINGS = new LinkedHashMap<>();


@Nullable @CheckResult @UiThread
  private static Constructor findBindingConstructorForClass(Class cls) {
    Constructor 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) 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;
  }

首先从Map中获取该构造对象,如果存在则直接返回对象,如果不存在,则去构造对应的Constructor.使用Map缓存是为了提高重复使用的效率.
如果类名以"android.","java.","androidx."开头,则直接返回null,否则创建一个新的Class,名字由传入的clsName+"_ViewBinding",如果我传入的类名是ButterKnifeFragment,则返回的实例是ButterKnifeFragment_ViewBinding,查找该文件,可以发现在build中可以找到,如图:


文件目录.png

下面具体看看这个类

public class ButterKnifeFragment_ViewBinding implements Unbinder {
  private ButterKnifeFragment target;

  private View view7f070046;

  @UiThread
  public ButterKnifeFragment_ViewBinding(final ButterKnifeFragment target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.button, "field 'mButton' and method 'onClick'");
    target.mButton = Utils.castView(view, R.id.button, "field 'mButton'", Button.class);
    view7f070046 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.onClick(Utils.castParam(p0, "doClick", 0, "onClick", 0, Button.class));
      }
    });
    target.mEtList = Utils.listFilteringNull(
        Utils.findRequiredViewAsType(source, R.id.et_1, "field 'mEtList'", EditText.class), 
        Utils.findRequiredViewAsType(source, R.id.et_2, "field 'mEtList'", EditText.class), 
        Utils.findRequiredViewAsType(source, R.id.et_3, "field 'mEtList'", EditText.class));
  }

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

    target.mButton = null;
    target.mEtList = null;

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

我在ButterKnifeFragment页面添加了4个控件(3EditText和一个Button,button绑定了点击事件,此处只分析button),还记得前面的双参数bind(@NonNull Object target, @NonNull View source)方法么,就是调用了这个构造方法.它里面有3个函数,一个是findRequiredView,一个是castView,还有一个点击事件,下面我们一个个查看源码

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

public static  T castView(View view, @IdRes int id, String who, Class 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);
    }
  }

在findRequiredView里面,我们看到了熟悉的findViewById方法...最终,还是回归到了系统的API. castView方法也是调用系统的cast方法..
那么,它是如何实现点击事件防抖动的呢? 继续往下看

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

点击一次后,让该控件enabled = false;再调用v.post(ENABLE_AGAIN);恢复控件的状态.

到此为止,我们了解到ButterKnife底层依旧是使用的findViewById方法,但是我们依旧有个疑问,ButterKnifeFragment_ViewBinding 并非我们自己写的,它是如何生成的呢?要知道这个就必须了解注解器相关知识了.

回顾我们使用ButterKnife的条件

dependencies {
  implementation 'com.jakewharton:butterknife:10.2.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
  //kapt 'com.jakewharton:butterknife-compiler:10.2.1'  //Kotlin
}

  @BindView(R.id.button1) Button button1;
  @BindView(R.id.button2) Button button2;

ButterKnifeProcessor完成了注解处理的核心逻辑,下面我们来看看它是如何实现的.

@AutoService(Processor.class)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC)
@SuppressWarnings("NullAway") // TODO fix all these...
public final class ButterKnifeProcessor extends AbstractProcessor {
    //忽略部分代码,只查看核心代码
.......
 @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if (sdk != null) {
      try {
        this.sdk = Integer.parseInt(sdk);
      } catch (NumberFormatException e) {
        env.getMessager()
            .printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
                + sdk
                + "'. Falling back to API 1 support.");
      }
    }

    debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));

    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
      try {
        // Get original ProcessingEnvironment from Gradle-wrapped one or KAPT-wrapped one.
        for (Field field : processingEnv.getClass().getDeclaredFields()) {
          if (field.getName().equals("delegate") || field.getName().equals("processingEnv")) {
            field.setAccessible(true);
            ProcessingEnvironment javacEnv = (ProcessingEnvironment) field.get(processingEnv);
            trees = Trees.instance(javacEnv);
            break;
          }
        }
      } catch (Throwable ignored2) {
      }
    }
  }


@Override public Set getSupportedOptions() {
    ImmutableSet.Builder builder = ImmutableSet.builder();
    builder.add(OPTION_SDK_INT, OPTION_DEBUGGABLE);
    if (trees != null) {
      builder.add(IncrementalAnnotationProcessorType.ISOLATING.getProcessorOption());
    }
    return builder.build();
  }

  @Override public Set getSupportedAnnotationTypes() {
    Set types = new LinkedHashSet<>();
    for (Class annotation : getSupportedAnnotations()) {
      types.add(annotation.getCanonicalName());
    }
    return types;
  }


  private Set> getSupportedAnnotations() {
    Set> annotations = new LinkedHashSet<>();

    annotations.add(BindAnim.class);
    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindFont.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }


 @Override public boolean process(Set elements, RoundEnvironment env) {
    Map bindingMap = findAndParseTargets(env);

    for (Map.Entry entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }
.......   //忽略部分代码,只查看核心代码
}

注意,ButterKnifeProcessor类上使用了@AutoService(Processor.class)注解并继承了AbstractProcessor ,来实现注解处理器的注册,注册到 javac 后,在项目编译时就能执行注解处理器了。

我的ButterKnife相关文章

ButterKnife(一): 使用篇
ButterKnife(二): 原理解析篇
ButterKnife(三): ButterKnifeProcessor解析

你可能感兴趣的:(ButterKnife(二): 原理解析篇)