美文网首页
ButterKnife原理以及应用

ButterKnife原理以及应用

作者: 鲁班0号 | 来源:发表于2019-05-05 00:34 被阅读0次

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

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

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

    先检查BINDINGS是否存在 Class 对应的 Constructor,如果存在则直接返回,否则去构造对应的 Constructor。其中BINDINGS是一个LinkedHashMap:
    Map<Class<?>, Constructor<? extends Unbinder>> 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<? extends TypeElement> elements, RoundEnvironment env) {
        Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
        for (Map.Entry<TypeElement, BindingSet> 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<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
            Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
            Set<TypeElement> 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<Map.Entry<TypeElement, BindingSet.Builder>> entries =
                    new ArrayDeque<>(builderMap.entrySet());
            Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
            while (!entries.isEmpty()) {
                // 出队列
                Map.Entry<TypeElement, BindingSet.Builder> 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原理以及应用

          本文链接:https://www.haomeiwen.com/subject/dqfruqtx.html