ButterKnife原理以及应用

0.文章导入

ButterKnife算是一款知名老牌 Android 开发框架了,通过注解绑定视图,避免了 findViewById() 的操作,广受好评!下面我们从原理开始,一步一步解析黄油刀吧!

1.注解框架

见另外一篇内容,这里就跳过了:https://www.jianshu.com/writer#/notebooks/18080563/notes/42184171

2.原理分析

绑定黄油刀,需要在inflate之后调用ButterKnife.bind(this),那么分析以下bind方法到底干了什么事情。源码如下:

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

可见,首先获取getDecorView,这view是PhoneWindow对应的布局,是一个FrameLayout,DecorView内部又分为两部分,一部分是ActionBar,另一部分是ContentParent,即activity在setContentView对应的布局。然后调用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 constructor = findBindingConstructorForClass(targetClass);
    if (constructor == null) {
      return Unbinder.EMPTY;
    }
    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source);
    }
  }

createBinding 方法中 主要是先找到类对应的构造器constructor,然后通过constructor来new一个新的对象。接下来看看findBindingConstructorForClass是怎么找到类的构造器的:

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

先检查BINDINGS是否存在 Class 对应的 Constructor,如果存在则直接返回,否则去构造对应的 Constructor。其中BINDINGS是一个LinkedHashMap:
Map, Constructor> BINDINGS = new LinkedHashMap<>(),缓存了对应的 Class 和 Constructor 以提高效率!
接下来看当不存在对应 Constructor 时如何构造一个新的,首先如果clsName是系统相关的,则直接返回 null,否则先创建一个新的 Class,再获取类名,clsName + "_ViewBinding",如:MainActivity_ViewBinding.class ,在获取类中的构造方法。这个构造方法中是怎么实现的呢?源码如下:

@UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;

    target.mTextView = Utils.findRequiredViewAsType(source, R.id.hello, "field 'mTextView'", TextView.class);
  }

好,我们明白了,原来在这里有通过id找到了target.mTextView。再看看 Utils.findRequiredViewAsType这个方法是怎么实现的。

  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,仅此而已!

3.注解处理器

还记得刚才的MainActivity_ViewBinding.class是怎么生成的吗?要生成这个类就要先得到这个类必须的基础信息,这就涉及到了annotationProcessor技术,和 APT(Annotation Processing Tool)技术类似,它是一种注解处理器,项目编译时对源代码进行扫描检测找出存活时间为RetentionPolicy.CLASS的指定注解,然后对注解进行解析处理,进而得到要生成的类的必要信息,然后根据这些信息动态生成对应的 java 类。接下来分析一下butterknife-compiler 这个黄油刀的注解处理器,主要的方法是:process;

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

第一步是findAndParseTargets(env),这个方法是注解信息的扫描收集。这个方法有点长,只看看主要的流程部分。

private Map findAndParseTargets(RoundEnvironment env) {
        Map builderMap = new LinkedHashMap<>();
        Set erasedTargetNames = new LinkedHashSet<>();

        scanForRClasses(env);
        ......
        ......
        // 获取所有使用BindView注解的元素
        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
            try {
                parseBindView(element, builderMap, erasedTargetNames);
            } catch (Exception e) {
                logParsingError(element, BindView.class, e);
            }
        }
        ......
        ......
        // 将builderMap中的数据添加到队列中
        Deque> entries =
                new ArrayDeque<>(builderMap.entrySet());
        Map bindingMap = new LinkedHashMap<>();
        while (!entries.isEmpty()) {
            // 出队列
            Map.Entry entry = entries.removeFirst();

            TypeElement type = entry.getKey();
            BindingSet.Builder builder = entry.getValue();
            // 查找当前类元素的父类元素
            TypeElement parentType = findParentType(type, erasedTargetNames);
            // 如果没找到则保存TypeElement和对应BindingSet
            if (parentType == null) {
                bindingMap.put(type, builder.build());
            } else {
                BindingSet parentBinding = bindingMap.get(parentType);
                if (parentBinding != null) {
                    // 如果找到父类元素,则给当前类元素对应的BindingSet.Builder设置父BindingSet
                    builder.setParent(parentBinding);
                    bindingMap.put(type, builder.build());
                } else {
                    // 再次入队列
                    entries.addLast(entry);
                }
            }
        }
        return bindingMap;
    }

4.JavaPoet

以上方法遍历 bindingMap,根据BindingSet得到一个JavaFile对象,然后输入 java 类,这个过程用到了JavaPoet开源库,提供了一种友好的方式来辅助生成 java 类代码,同时将类代码生成文件,否则需要自己拼接字符串来实现,可以发现BindingSet除了保存信息目标类信息外,还封装了 JavaPoet 生成目标类代码的过程。
最终生成了一个_ViewBinding.class的文件,关于javapoet后面再学习。

你可能感兴趣的:(ButterKnife原理以及应用)