写在最前面

上一篇中,我们简单的分析了下 Butterknife 的项目结构,同时也分析了反射效率较低的原因,本篇就正式看看 Butterknife-reflect 这个库,看看如何用反射实现一个 ButterKnife。

项目分析

好的,最开始我们还是先来分析下,整个工程的项目结构。

.
├── README.md
├── build.gradle
├── butterknife-reflect.iml
├── gradle.properties
├── proguard-rules.txt
└── src
    └── main
        ├── AndroidManifest.xml
        └── java
            └── butterknife
                ├── ButterKnife.java
                ├── CompositeUnbinder.java
                ├── EmptyTextWatcher.java
                ├── FieldUnbinder.java
                └── ListenerUnbinder.java

整个结构简单的出奇,ok,那么直接来看代码。

视图绑定

ok 首先我们回忆下,通常我们在绑定一个 View 时,会有怎样的操作

  • @BindView(R.id.user) EditText username; 指出要绑定的类。
  • ButterKnife.bind(this); 在onCreate方法中,调用 bind 方法。

那么源码中我们也按照这样分析,看下 Butterknife 中的相关函数:

public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
}

public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    List<Unbinder> unbinders = new ArrayList<>();
    Class<?> targetClass = target.getClass();
    if ((targetClass.getModifiers() & PRIVATE) != 0) {
      throw new IllegalArgumentException(targetClass.getName() + " must not be private.");
    }

    while (true) {
      String clsName = targetClass.getName();
      if (clsName.startsWith("android.") || clsName.startsWith("java.")
          || clsName.startsWith("androidx.")) {
        break;
      }

      for (Field field : targetClass.getDeclaredFields()) { ... }

      for (Method method : targetClass.getDeclaredMethods()) { ... }

      targetClass = targetClass.getSuperclass();
    }

    if (unbinders.isEmpty()) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return Unbinder.EMPTY;
    }

    if (debug) Log.d(TAG, "HIT: Reflectively found " + unbinders.size() + " bindings.");
    return new CompositeUnbinder(unbinders);
}

结构很清晰:获取当前的 DecorView,和 Activity 这个实例类绑定到一起,当然也有可能是别的实例类,这里我们用 Clazz 来代替。那这个bind函数做了这么几件事:

  • 获取当前 Clazz 的可见性,private 的不可以。其实如果要用反射来绑定时,private 也是完全可以的,不过为了和编译时注解保持一致,因此不许 private。
  • 递归遍历 Field 和 Method,直到达到基类为 官方类 时。对 Field 和 Method 进行而外操作,这里我们先不深究。
  • 将所有的 binder 对象放回,方便统一管理

接下来看看他对遍历的 Field 对象都做了什么事情:

for (Field field : targetClass.getDeclaredFields()) {
    int unbinderStartingSize = unbinders.size();
    Unbinder unbinder;

    unbinder = parseBindView(target, field, source);
    if (unbinder != null) unbinders.add(unbinder);

    ...

    if (unbinders.size() - unbinderStartingSize > 1) {
        throw new IllegalStateException(
            "More than one bind annotation on " + targetClass.getName() + "." + field.getName());
    }
}

其实就是遍历尝试,看看 这个 Field 是否被相应的注解所修饰,这里我们以 @BindView 作为例子来看看其内部是怎么操作的。

parseBindView

private static @Nullable Unbinder parseBindView(Object target, Field field, View source) {

    //查找 Field 是否被 BindView 所修饰,不是就跳过,进行下一个 注解的判断,例如 BindViews
    BindView bindView = field.getAnnotation(BindView.class);
    if (bindView == null) {
      return null;
    }

    //校验 Field 是否是 private 或者 static 的。其实可以直接修改,但是原因还是 要和 编译时注解保持一致。
    validateMember(field);

    //获取 BindView 的 id:value
    int id = bindView.value();

    //获取 Field 对象的类信息
    Class<?> viewClass = field.getType();

    //如果 Field 不是不是继承于 View 的话,或者一个 接口也行(Checkable),就抛异常
    if (!View.class.isAssignableFrom(viewClass) && !viewClass.isInterface()) {
      throw new IllegalStateException(
          "@BindView fields must extend from View or be an interface. ("
              + field.getDeclaringClass().getName()
              + '.'
              + field.getName()
              + ')');
    }

    String who = "field '" + field.getName() + "'";

    // 在 DecroView 中根据 id 获取相应的 View,并且将这个 View 进行强转成 定义的 Field 类型。
    Object view = Utils.findOptionalViewAsType(source, id, who, viewClass);

    // 最后一个 设置 类似与 TextView tv = findViewById(R.id.tv);
    // 这样便把一个 未初始化的 View 和 相应对象 绑定到了一起。
    trySet(field, target, view);

    return new FieldUnbinder(target, field);
}

public static <T> T findOptionalViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
    View view = source.findViewById(id);
    return castView(view, id, who, cls);
}

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

static void trySet(Field field, Object target, @Nullable Object value) {
    try {
      field.set(target, value);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to assign " + value + " to " + field + " on " + target, e);
    }
}

上面的注解已经把整个流程分析的很透彻了,整体来看主要分成这几步:

  • 类可见性校验。
  • 迭代查找类,遍历其 Field 和 Method 对象。
  • 针对 Field
    • 尝试解析相应的注解,对注解的校验等
    • 校验 Field 对象的可见性
    • 检验 Field 的具体类是否继承与 View
    • 从 DecorView 获取相应的 视图,并强转为 对应的 Field 的类型
    • 对 Field 进行 set value

操作绑定

上面我们说了 BindView 相关的内容,那这里我们看看 OnClick 又会有怎样的操作。

for (Method method : targetClass.getDeclaredMethods()) {
    Unbinder unbinder;
    ...
    unbinder = parseOnClick(target, method, source);
    if (unbinder != null) unbinders.add(unbinder);
    ...
}

private static @Nullable Unbinder parseOnClick(final Object target, final Method method,
      View source) {

    // 查找 Method 是否被 OnClick 所修饰,不是就跳过,进行下一个 注解的判断,例如 OnTouch
    OnClick onClick = method.getAnnotation(OnClick.class);
    if (onClick == null) {
      return null;
    }

    // 校验 Method
    validateMember(method);

    // 校验返回类型。对于 OnClick 没啥用,主要是对于有返回值的方法有有用,例如 onTouch 这样的
    validateReturnType(method, void.class);

    // 参数传递,默认情况只有一个 View,用 ON_CLICK_TYPES 来表示 Class<?>[] ON_CLICK_TYPES = { View.class };
    final ArgumentTransformer argumentTransformer =
        createArgumentTransformer(method, ON_CLICK_TYPES);

    //获取视图列表
    List<View> views =
        findViews(source, onClick.value(), isRequired(method), method.getName(), View.class);

    //把 视图 操作 实现 完全解藕
    ViewCollections.set(views, ON_CLICK,
        v -> tryInvoke(method, target, argumentTransformer.transform(v)));

    return new ListenerUnbinder<>(views, ON_CLICK);
}

// 这里其实就是讨论一个问题,用户自己绑定定义的函数签名,应该和目标函数签名怎么匹配的问题。
// 因为我们知道 以 onClick 为例,虽然有一个参数 view ,但如果我不关心的话,我自己定义时是可以不写的。
// 但是如果 我写了但是和提供函数的参数对不上怎么办?
private static ArgumentTransformer createArgumentTransformer(Method method,
      Class<?>[] callbackParameterTypes) {
    Class<?>[] targetParameterTypes = method.getParameterTypes();

    // 用户定义的函数 完全没有参数。好啊,你不用参数,我就不给你传了。
    int targetParameterLength = targetParameterTypes.length;
    if (targetParameterLength == 0) {
      // Special case the common case of no arguments.
      return ArgumentTransformer.EMPTY;
    }

    // 用户定义的函数 要求的参数比能提供的还要多,淦,没了。抛异常。
    int callbackParameterLength = callbackParameterTypes.length;
    if (targetParameterLength > callbackParameterLength) {
      throw new IllegalStateException(method.getDeclaringClass().getName()
          + "."
          + method.getName()
          + " must have at most "
          + callbackParameterLength
          + " parameter(s).");
    }

    // 用户定义的函数 要求的参数和能提供的参数 完 全 一 致。ok 那就有多少返回多少
    if (Arrays.equals(targetParameterTypes, callbackParameterTypes)) {
      // Special case the common case of exact argument match.
      return ArgumentTransformer.IDENTITY;
    }

    // 如果个数不一致,或者顺序不一致,那就只能遍历来挑选了。
    boolean[] callbackIndexUsed = new boolean[callbackParameterLength];
    final int[] indexMap = new int[targetParameterLength];
    nextTarget: for (int targetIndex = 0; targetIndex < targetParameterLength; targetIndex++) {
      Class<?> targetParameterType = targetParameterTypes[targetIndex];
      for (int callbackIndex = 0; callbackIndex < callbackParameterLength; callbackIndex++) {
        if (callbackIndexUsed[callbackIndex]) {
          continue; // 我们已经用过这个返回参数了.
        }
        Class<?> callbackParameterType = callbackParameterTypes[callbackIndex];

        if (/* 精确匹配 */
            callbackParameterType.equals(targetParameterType)
            /* 或者是个View子类 */
            || (View.class.isAssignableFrom(callbackParameterType)
                && callbackParameterType.isAssignableFrom(targetParameterType))
            /* 或者是个接口 (like Checkable) */
            || targetParameterType.isInterface()) {

          //那么就设置上匹配关系,修改标志位
          indexMap[targetIndex] = callbackIndex;
          callbackIndexUsed[callbackIndex] = true;
          continue nextTarget; // This avoids the error handling code if loop exits normally.
        }
      }
      //如果无法精确匹配的话,那就说明 自定义函数的某个参数无法通过 提供的函数来提供只能抛出异常。
      throw new IllegalStateException("参数不匹配");
    }

    // 返回一个参数列表 根据 indexMap 来返回吧。
    return new ArgumentTransformer() {
      @Override public Object[] transform(Object... arguments) {
        Object[] newArguments = new Object[indexMap.length];
        for (int i = 0; i < indexMap.length; i++) {
          newArguments[i] = arguments[indexMap[i]];
        }
        return newArguments;
      }

      @Override public String toString() {
        StringBuilder builder = new StringBuilder("ArgumentTransformer[");
        for (int i = 0; i < indexMap.length; i++) {
          if (i > 0) {
            builder.append(", ");
          }
          builder.append(i).append(" => ").append(indexMap[i]);
        }
        return builder.append(']').toString();
      }
    };
}

整体来说绑定操作是这么一个步骤:

  • 类可见性校验。
  • 迭代查找类,遍历其 Field 和 Method 对象。
  • 针对 Method
    • 尝试解析相应的注解,对注解的校验等
    • 校验 Method 对象的可见性
    • 校验 Method 放回类型是否为 void 主要是给 onTouch 这类的有返回值的用的
    • 检验 Method 的参数和能提供的方法参数是否匹配(可以少 可以没 可以乱序,不可多,不可不匹配)
    • 获取视图列表
    • 使用 ViewCollections 来进行绑定操作等。

整体来说,思路比较简单,但是思路和抽象能力很棒。

results matching ""

    No results matching ""