美文网首页Android源码分析
ButterKnife源码初探

ButterKnife源码初探

作者: My_Hubery | 来源:发表于2018-04-06 11:15 被阅读227次

    Butterknife是一个Android View注入框架,配合Android ButterKnife Zelezny插件能够快速的完成View的初始化操作。相比较之前findViewById()方式来获取view的方式,它不仅简洁了代码,提高了编码效率,而且还避免了大量的强转。本文基于ButterKnife7.0.1版本进行分析。

    1,ButterKnife的引入和使用

    build.gradle文件下添加butterknife的依赖

    dependencies {
        implementation'com.jakewharton:butterknife:7.0.1'
        annotationProcessor 'com.jakewharton:butterknife:7.0.1'
    }
    

    需要注意的是我使用的是Android Studio 3.0.1版本,如果你是3.0以下依赖写法不一致,你只需要使用compile'com.jakewharton:butterknife:7.0.1'来添加依赖。

    添加完依赖之后,我们来看看ButterKnife的使用

    public class MainActivity extends AppCompatActivity {
    
        @Bind(R.id.show_tip_txt)
        TextView showTipTxt;
        @Bind(R.id.butter_knife_btn)
        Button butterKnifeBtn;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ButterKnife.bind(this);
        }
    
        @OnClick({R.id.butter_knife_btn})
        void butterBtn(){
            Toast.makeText(this,"点击了我",Toast.LENGTH_LONG).show();
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            ButterKnife.unbind(this);
        }
    }
    

    在这里我们看到,ButterKnife.bind(this)需要在setContentView()方法后面,并且@Bind的参数不能用private和static修饰,具体原因后面有分析,ButterKnife是通过@Bind是用来绑定View的Id的,此外它还提供了更多的绑定方法提供给我们,如:BindBool,BindColor,BindDimen,BindDrawable,BindInt,BindString,OnClick等。在这里就不一一的介绍了,读者感兴趣的话,可自行实验。
    上面的绑定View在Activity中的使用,其实ButterKnife不仅仅可以在Activity中使用,还可以在View,Dialog中使用,我们看看ButterKnife文件中的定义。

    public enum Finder {
        VIEW {
          @Override protected View findView(Object source, int id) {
            return ((View) source).findViewById(id);
          }
    
          @Override public Context getContext(Object source) {
            return ((View) source).getContext();
          }
        },
        ACTIVITY {
          @Override protected View findView(Object source, int id) {
            return ((Activity) source).findViewById(id);
          }
    
          @Override public Context getContext(Object source) {
            return (Activity) source;
          }
        },
        DIALOG {
          @Override protected View findView(Object source, int id) {
            return ((Dialog) source).findViewById(id);
          }
    
          @Override public Context getContext(Object source) {
            return ((Dialog) source).getContext();
          }
        };
        private static <T> T[] filterNull(T[] views) {
          int end = 0;
          for (int i = 0; i < views.length; i++) {
            T view = views[i];
            if (view != null) {
              views[end++] = view;
            }
          }
          return Arrays.copyOfRange(views, 0, end);
        }
    

    当然在我们不需要的时候,我们还需要解绑操作

     @Override
        protected void onDestroy() {
            super.onDestroy();
            ButterKnife.unbind(this);
        }
    

    2,ButterKnife执行原理

    通过上面我们了解到ButterKnife是通过@Bind来绑定View的,先来看看Bind的定义

    /**
     * Bind a field to the view for the specified ID. The view will automatically be cast to the field
     * type.
     * <pre><code>
     * {@literal @}Bind(R.id.title) TextView title;
     * </code></pre>
     */
    @Retention(CLASS) @Target(FIELD)
    public @interface Bind {
      /** View ID to which the field will be bound. */
      int[] value();
    }
    

    Bind是一个是自定义的annotation文件,需要注意一下它和接口文件的区别:一个@interface ,另外一个interface。
    我们先看看Bind上的注解@Rentention,@Target:
    @Retention:对Annotation的“生命周期”限制,代表有效期
    RetentionPolicy.SOURCE,在源文件中有效
    RetentionPolicy.CLASS,在Class文件中有效
    RetentionPolicy.RUNTIME,在运行时有效

    @Target:规定自定义Annotation所修饰的对象范围
    ElementType.CONSTRUCTOR,构造器声明
    ElementType.FIELD,成员变量、对象、属性(包括enum实例)
    ElementType.LOCAL_VARIABLE,局部变量声明
    ElementType.METHOD,方法声明
    ElementType.PACKAGE,包声明
    ElementType.PARAMETER,参数声明
    ElementType.TYPE,类、接口(包括注解类型)或enum声明
    默认不设置@Target表示修饰所有。

    当你在Build项目工程的时候,
    ButterKnife使用了Annotion Processing(注解处理),因此在编译的时候,Annotation会由APT(Annotation Processing Tool)解析。ButterKnife工程中ButterKnifeProcessor类的process()方法会执行以下操作:
    A,扫描Java代码中所有的ButterKnife注解@Bind,@OnClick等;
    B,当有扫描到为ButterKnife注解的时候,ButterKnifeProcessor会自动生成一个实现了ViewBinder接口的XX$$ViewBinder的java文件,
    C,在生成的ViewBinder中,包含了该类所有注解对于的实现,如@Bind会处理成findViewById,@OnClick处理成setOnClickListener等;

    在运行代码的时候,遇到ButterKnife.bind()的时候,Butterknife会加载在编译期生成的ViewBinder类,并调用对应的注解如,@Bind等;
    在取消注解的时候,会调用到ViewBinder的unbind方法。

    public class MainActivity$$ViewBinder<T extends com.ponycar.huberytest.MainActivity> implements ViewBinder<T> {
      @Override public void bind(final Finder finder, final T target, Object source) {
        View view;
        view = finder.findRequiredView(source, 2131165287, "field 'showTipTxt'");
        target.showTipTxt = finder.castView(view, 2131165287, "field 'showTipTxt'");
        view = finder.findRequiredView(source, 2131165219, "field 'butterKnifeBtn' and method 'butterBtn'");
        target.butterKnifeBtn = finder.castView(view, 2131165219, "field 'butterKnifeBtn'");
        view.setOnClickListener(
          new butterknife.internal.DebouncingOnClickListener() {
            @Override public void doClick(
              android.view.View p0
            ) {
              target.butterBtn();
            }
          });
      }
    
      @Override public void unbind(T target) {
        target.showTipTxt = null;
        target.butterKnifeBtn = null;
      }
    }
    

    3,ButterKnife 源码解析

    既然View是通过ButterKnife.bind(this)方法来加载ViewBinder类,并调用对应注解的,那我们先看bind(this)方法:

     public static void bind(Activity target) {
        bind(target, target, Finder.ACTIVITY);
      }
    
    static void bind(Object target, Object source, Finder finder) {
        Class<?> targetClass = target.getClass();
        try {
          if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
          ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
          if (viewBinder != null) {
            viewBinder.bind(finder, target, source);
          }
        } catch (Exception e) {
          throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
        }
      }
    

    我们看到这个方法主要是通过传入的参数,然后去找到对应的viewBinder,并绑定注解,这如我们之前所说的一样。那我们在看看它是怎么找到viewBinder的,现在我们进入findViewBinderForClass(targetClass)方法看看:

    private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
          throws IllegalAccessException, InstantiationException {
        ViewBinder<Object> viewBinder = BINDERS.get(cls);
        if (viewBinder != null) {
          if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
          return viewBinder;
        }
        String clsName = cls.getName();
        //判断是不是系统文件
        if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) {
          if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
          return NOP_VIEW_BINDER;
        }
        try {
          //加载自动生成的ViewBinder文件
          Class<?> viewBindingClass = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
          //noinspection unchecked
          viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
          if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
        } catch (ClassNotFoundException e) {
          if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
          //如果没有找到,就继续查找它父类
          viewBinder = findViewBinderForClass(cls.getSuperclass());
        }
        //放入缓存中
        BINDERS.put(cls, viewBinder);
        return viewBinder;
      }
    

    通过上面的代码,我们看到先会在BINDERS这个Map里面去查找是否有ViewBinder的缓存,如果找到就直接返回viewBinder,如果没有找到就继续判断类名是不是android,或者java开头,接下来会加载自动生成的ViewBinder文件,如果出现ClassNotFoundException,则会继续查找cls.getSuperclass(),最后将viewBinder放入缓存,并返回viewBinder。

    if (viewBinder != null) {
            viewBinder.bind(finder, target, source);
    }
    

    bind()方法中拿到了viewBinder就会去调用viewBinder.bind()方法,也就是自动生成的XX$$ViewBinder中的bind方法。

    另外,解绑的时候会最终调用

      @Override public void unbind(T target) {
        target.showTipTxt = null;
        target.butterKnifeBtn = null;
      }
    

    分析到这里,基本的绑定/解绑也就搞明白了,现在我们再来看看是如果生成XX$$ViewBinder的方法的。

    4,ButterKnife生成XX$$ViewBinder文件

    ButterKnife中ButterKnifeProcessor去继承了AbstractProcessor

    public final class ButterKnifeProcessor extends AbstractProcessor {}
    

    Annotaion process tool会在编译期自动的查找所有继承了AbstractProcessor类的文件,然后调用它们的process方法去生成java文件。我们先看看,ButterKnife工程中ButterKnifeProcessor类的process()的方法:

      @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        //查找并解析注解
        Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
    
        for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
          TypeElement typeElement = entry.getKey();
          BindingClass bindingClass = entry.getValue();
    
          try {
            //写入文件
            JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
            Writer writer = jfo.openWriter();
            writer.write(bindingClass.brewJava());
            writer.flush();
            writer.close();
          } catch (IOException e) {
            error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
                e.getMessage());
          }
        }
        return true;
      }
    

    这个方法中,先查找并解析注解,然后再写入文件,那我们先看看findAndParseTargets()方法:

      private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
        //TypeElement 代表等待注入的类元素,BindingClass 该类包含待注入元素的集合
        Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>();
        Set<String> erasedTargetNames = new LinkedHashSet<String>();
    
        // Process each @Bind element.处理@Bind
        for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
          try {
            parseBind(element, targetClassMap, erasedTargetNames);
          } catch (Exception e) {
            logParsingError(element, Bind.class, e);
          }
        }
    
        // Process each annotation that corresponds to a listener.处理监听
        for (Class<? extends Annotation> listener : LISTENERS) {
          findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
        }
    
        // Process each @BindBool element.处理BindBool
        for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
          try {
            parseResourceBool(element, targetClassMap, erasedTargetNames);
          } catch (Exception e) {
            logParsingError(element, BindBool.class, e);
          }
        }
    
        //...省略其他代码
    
        // Try to find a parent binder for each.尝试去找到父类的绑定
        for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
          String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames);
          if (parentClassFqcn != null) {
            entry.getValue().setParentViewBinder(parentClassFqcn + SUFFIX);
          }
        }
    
        return targetClassMap;
      }
    

    这个方法,主要去处理不同类型的绑定,以及他们父类的绑定。在这里我们看看,parseBind():

    private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
          Set<String> erasedTargetNames) {
        // Verify common generated code restrictions.
        if (isInaccessibleViaGeneratedCode(Bind.class, "fields", element)
            || isBindingInWrongPackage(Bind.class, element)) {
          return;
        }
    
        TypeMirror elementType = element.asType();
        if (elementType.getKind() == TypeKind.ARRAY) {
          parseBindMany(element, targetClassMap, erasedTargetNames);
        } else if (LIST_TYPE.equals(doubleErasure(elementType))) {
          parseBindMany(element, targetClassMap, erasedTargetNames);
        } else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) {
          error(element, "@%s must be a List or array. (%s.%s)", Bind.class.getSimpleName(),
              ((TypeElement) element.getEnclosingElement()).getQualifiedName(),
              element.getSimpleName());
        } else {
          parseBindOne(element, targetClassMap, erasedTargetNames);
        }
      }
    

    这个方法里面有两个很重要的判断isInaccessibleViaGeneratedCode(),该方法判断了
    1、修饰符不能为private或static;
    2、不能用于非Class类;
    3、当前类修饰符不能为private

    private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass,
          String targetThing, Element element) {
        boolean hasError = false;
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    
        // Verify method modifiers.
        Set<Modifier> modifiers = element.getModifiers();
        //修饰符不能为private或static
        if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
          error(element, "@%s %s must not be private or static. (%s.%s)",
              annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
              element.getSimpleName());
          hasError = true;
        }
    
        // 不能用于非Class类
        if (enclosingElement.getKind() != CLASS) {
          error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
              annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
              element.getSimpleName());
          hasError = true;
        }
    
        // Verify containing class visibility is not private.
        //当前类修饰符不能为private
        if (enclosingElement.getModifiers().contains(PRIVATE)) {
          error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
              annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
              element.getSimpleName());
          hasError = true;
        }
    
        return hasError;
      }
    

    在isBindingInWrongPackage()方法中校验类不能以“android.”或者“"java.”开头;

     public static final String ANDROID_PREFIX = "android.";
      public static final String JAVA_PREFIX = "java.";
    
    private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationClass,
          Element element) {
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        String qualifiedName = enclosingElement.getQualifiedName().toString();
    
        if (qualifiedName.startsWith(ANDROID_PREFIX)) {
          error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
              annotationClass.getSimpleName(), qualifiedName);
          return true;
        }
        if (qualifiedName.startsWith(JAVA_PREFIX)) {
          error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
              annotationClass.getSimpleName(), qualifiedName);
          return true;
        }
        return false;
      }
    

    回过头去再看看parseBind()方法,里面有parseBindMany()这是绑定的List或者Array,还有一个parseBindOne(),我们来看看:

      private void parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap,
          Set<String> erasedTargetNames) {
        boolean hasError = false;
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    
        // Verify that the target type extends from View.
        TypeMirror elementType = element.asType();
        if (elementType.getKind() == TypeKind.TYPEVAR) {
          TypeVariable typeVariable = (TypeVariable) elementType;
          elementType = typeVariable.getUpperBound();
        }
        if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
          error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
              Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
          hasError = true;
        }
    
        // Assemble information on the field.
        int[] ids = element.getAnnotation(Bind.class).value();
        if (ids.length != 1) {
          error(element, "@%s for a view must only specify one ID. Found: %s. (%s.%s)",
              Bind.class.getSimpleName(), Arrays.toString(ids), enclosingElement.getQualifiedName(),
              element.getSimpleName());
          hasError = true;
        }
    
        if (hasError) {
          return;
        }
    
        int id = ids[0];
        //获取BindingClass
        BindingClass bindingClass = targetClassMap.get(enclosingElement);
        if (bindingClass != null) {
          ViewBindings viewBindings = bindingClass.getViewBinding(id);
          if (viewBindings != null) {
            Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
            //判断不能重复绑定
            if (iterator.hasNext()) {
              FieldViewBinding existingBinding = iterator.next();
              error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
                  Bind.class.getSimpleName(), id, existingBinding.getName(),
                  enclosingElement.getQualifiedName(), element.getSimpleName());
              return;
            }
          }
        } else {
          bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
        }
    
        String name = element.getSimpleName().toString();
        String type = elementType.toString();
        boolean required = isRequiredBinding(element);
    
        FieldViewBinding binding = new FieldViewBinding(name, type, required);
        // 再将带有@Bind注解的元素信息添加进BindingClass
        bindingClass.addField(id, binding);
    
        // Add the type-erased version to the valid binding targets set.
        erasedTargetNames.add(enclosingElement.toString());
      }
    

    先获取BindingClass,然后再将带有@Bind注解的元素添加进BindingClass。
    我们再来看看获取BindingClass的方法getOrCreateTargetClass

      private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
          TypeElement enclosingElement) {
        BindingClass bindingClass = targetClassMap.get(enclosingElement);
        if (bindingClass == null) {
          String targetType = enclosingElement.getQualifiedName().toString();
          String classPackage = getPackageName(enclosingElement);
          String className = getClassName(enclosingElement, classPackage) + SUFFIX;
    
          bindingClass = new BindingClass(classPackage, className, targetType);
          //process方法中一路传递过来targetClassMap,添加BindingClass
          targetClassMap.put(enclosingElement, bindingClass);
        }
        return bindingClass;
      }
    

    这个方法内会再一次判断通过targetClassMap获取的BindingClass是否为空,为空的时候会从新new一个BindingClass,并再次添加到targetClassMap。这里的targetClassMap就是process方法传递过来的,最后返回process。到这里ButterKnife的大概流程就完结了。

    总结:

    ButterKnife使用了注解的方式,在编译的时候,Annotaion process tool会在编译期自动的查找ButterKnifeProcessor文件,然后调用了它的process方法去生成XX$$ViewBinder.java文件。在执行ButterKnife.bind(this)操作的时候,会先去BINDERS(key为class,Value为ViewBinder)中查找,如果没有缓存,则会通过反射的方式去拿到在编译期生成的ViewBinder,真正执行的绑定的操作就是在ViewBinder中。ButterKnife相比与其他的View注入框架,它使用的是注解的方式,虽然在获取生成的ViewBinder类使用的是反射,但是有缓存,这使得ButterKnife执行效率较高。此外,@Bind()等注解不能使用private修饰,是它因为根据Id来获取View,如果使用private就只能通过反射拿到,这会很消耗性能。

    相关文章

      网友评论

      本文标题:ButterKnife源码初探

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