美文网首页
ButterKnife,不简单的Annotation

ButterKnife,不简单的Annotation

作者: TutuJie | 来源:发表于2017-07-02 01:20 被阅读0次

    1、ButterKnife简介

    ButterKnife是Android大神JakeWharton出品的又一神器,它通过注解的方式来替代findViewById来绑定view和一系列匿名的view的事件。最常见的用法如下:

     @BindView(R.id.title) TextView title;
     @BindView(R.id.subtitle) TextView subtitle;
     @BindView(R.id.footer) TextView footer;
     
     @OnClick(R.id.submit)
     public void sayHi(Button button) {
         button.setText("Hello!");
    }
    

    但是ButterKnife除了绑定view和添加事件,还能绑定Resource,有@BindBool, @BindColor, @BindDimen, @BindDrawable, @BindInt, @BindString等标签。

    2、ButterKnife总体架构

    以截止到2017.6.29的最新版本8.6.0源码分析。它的各个模块的依赖关系如下:


    未命名文件.png-11.1kB未命名文件.png-11.1kB

    1、butterknife是对外提供接口的模块;
    2、butterknife-compile负责编译期对注解进行解析,然后生成绑定view的java文件,以xxx_ViewBinding命名,放在对应工程/build/generated/source目录下;
    3、butterknife-gradle—plugin是8.2.0之后为了支持library工程而新增的模块;
    4、butterknife-annotations是专门放置注解的包,我们常使用的Bindview、BindString、OnClick等注解都在这个包里;
    5、butterknife是针对butterknife-gradle—plugin提供的静态代码检查工具。

    3、注解(Annotation)

    阅读ButterKnife的源码,必须要对java的注解有一定的了解,比如@Override,@Deprecated等,这种注解大家一定见过。
    注解的目的是将一些本来重复性的工作,变成程序自动完成,简化和自动化该过程。比如用于生成Javadoc,比如编译时进行格式检查,比如自动生成代码等,用于提升软件的质量和提高软件的生产效率。
    作为Android开发,日常碰到的注解主要有来自JDK里的,也有Android SDK里的,也有自定义的。推荐阅读

    ButterKnife其实就是基于自定义注解的方式实现的。

    3.1 andorid-apt(Annotation Processing Tool)

    android-apt 是一个Gradle插件,协助Android Studio 处理annotation processors, 它有两个目的:

    1、允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library
    2、设置源路径,使注解处理器生成的代码能被Android Studio正确的引用
    

    使用该插件,添加如下到你的构建脚本中:

    //配置在Project下的build.gradle中 buildscript {
    repositories {
    mavenCentral()
    }
    dependencies {
    //替换成最新的 gradle版本
    classpath 'com.android.tools.build:gradle:1.3.0'
    //替换成最新android-apt版本
    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    } } //配置到Module下的build.gradle中 apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt'

    伴随着 Android Gradle 插件 2.2 版本的发布,,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。
    修改前配置如下:

    compile 'com.jakewharton:butterknife:8.0.1'
    apt 'com.jakewharton:butterknife-compiler:8.0.1'
    

    修改后配置如下:

    compile 'com.jakewharton:butterknife:8.6.0'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'
    

    整个注解处理过程包括三部分:1、注解处理器(Processor);2、注册注解处理器(AutoService);2、代码生成器(JavaPoet).

    示例:

    package com.example;
    
    import com.google.auto.service.AutoService;
    import com.squareup.javapoet.JavaFile;
    import com.squareup.javapoet.MethodSpec;
    import com.squareup.javapoet.TypeSpec;
    
    import java.io.IOException;
    import java.util.LinkedHashSet;
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.Filer;
    import javax.annotation.processing.Messager;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.Processor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.ElementKind;
    import javax.lang.model.element.Modifier;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.util.Elements;
    import javax.lang.model.util.Types;
    
    @AutoService(Processor.class)
    public class MyProcessor extends AbstractProcessor {
    
        private Types typeUtils;
        private Elements elementUtils;
        private Filer filer;
        private Messager messager;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            //提供处理注解和生成代码的工具类
            typeUtils = processingEnv.getTypeUtils();
            elementUtils = processingEnv.getElementUtils();
            filer = processingEnv.getFiler();
            messager = processingEnv.getMessager();
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            //1、处理注解;
            //2、生成代码。
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> annotataions = new LinkedHashSet<String>();
            annotataions.add(MyAnnotation.class.getCanonicalName());
            return annotataions;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    }
    

    定义完注解处理器后,还需要告诉编译器该注解处理器的信息,需在 src/main/resource/META-INF/service 目录下增加 javax.annotation.processing.Processor 文件,并将注解处理器的类名配置在该文件中。
    ![image.png-33kB][2]

    ![image.png-34.9kB][3]

    javapoet也是square开源的生成.java文件的API。

    4、ButterKnife整体流程

    image.png-37.5kBimage.png-37.5kB

    4.1 ButterKnifeProcessor

    ButterKnifeProcessor主要做了两件事:

    1、解析所有包含了ButterKnife注解的类;
    2、根据解析结果,使用JavaPoet生成对应的类。
    
    @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        //解析所有包含了ButterKnife注解的类
        Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    
        for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
          TypeElement typeElement = entry.getKey();
          BindingSet binding = entry.getValue();
            //根据解析结果,使用JavaPoet生成对应的类
          JavaFile javaFile = binding.brewJava(sdk);
          try {
            javaFile.writeTo(filer);
          } catch (IOException e) {
            error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
          }
        }
    
        return false;
      }
    

    ButterKnife支持的所有注解在getSupportedAnnotations方法里面定义。

    private Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
    
        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(BindInt.class);
        annotations.add(BindString.class);
        annotations.add(BindView.class);
        annotations.add(BindViews.class);
        annotations.addAll(LISTENERS);
    
        return annotations;
      }
      private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(//
          OnCheckedChanged.class, //
          OnClick.class, //
          OnEditorAction.class, //
          OnFocusChange.class, //
          OnItemClick.class, //
          OnItemLongClick.class, //
          OnItemSelected.class, //
          OnLongClick.class, //
          OnPageChange.class, //
          OnTextChanged.class, //
          OnTouch.class //
      );
    

    4.2 @BindView、@BindString、@OnClick

    下面以@BindView、@BindString、@OnClick三个比较常用的注解为例,分析下具体的绑定过程。
    在findAndParseTargets方法中解析所有注解,每个类对应一个BindingSet,解析过程中会把每个注解的解析结果放到BindingSet中,比如@BindString对应FieldResourceBinding类型,@BindView对应FieldViewBinding类型,解析完成后调用BindingSet的brewJava生成对应的JavaFile,即可通过JavaPoet生成.java文件。

    4.2.1@BindView:

     private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
          Set<TypeElement> erasedTargetNames) {
        
        //1、TypeElement就是该注解Element所在的类
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        
        // Start by verifying common generated code restrictions.
        //2、验证生成代码时是否可见和是否在错误的包里面
        boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
            || isBindingInWrongPackage(BindView.class, element);
    
        // Verify that the target type extends from View.
        //3、 验证目标类型是否继承自View
        TypeMirror elementType = element.asType();
        if (elementType.getKind() == TypeKind.TYPEVAR) {
          TypeVariable typeVariable = (TypeVariable) elementType;
          elementType = typeVariable.getUpperBound();
        }
        Name qualifiedName = enclosingElement.getQualifiedName();
        Name simpleName = element.getSimpleName();
        if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
          if (elementType.getKind() == TypeKind.ERROR) {
            note(element, "@%s field with unresolved type (%s) "
                    + "must elsewhere be generated as a View or interface. (%s.%s)",
                BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
          } else {
            error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
                BindView.class.getSimpleName(), qualifiedName, simpleName);
            hasError = true;
          }
        }
    
        //4、上述两步只要有错误,直接返回
        if (hasError) {
          return;
        }
    
        // Assemble information on the field.
        //5、解析注解的value
        int id = element.getAnnotation(BindView.class).value();
    
        //6、查找所在类对应的BindingSet构造类。如果有还要判断这个view的id是否已经绑定过,如果没有则新建。
        BindingSet.Builder builder = builderMap.get(enclosingElement);
        QualifiedId qualifiedId = elementToQualifiedId(element, id);
        if (builder != null) {
          String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
          if (existingBindingName != null) {
            error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
                BindView.class.getSimpleName(), id, existingBindingName,
                enclosingElement.getQualifiedName(), element.getSimpleName());
            return;
          }
        } else {
          builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
        }
    
        //7、解析注解的view。比如@BindView(R.id.tv_title) TextView tvTitle,那么name就是tvTitle,type就是TextView,required只要没有@Nullable的注解就是true。
        String name = simpleName.toString();
        TypeName type = TypeName.get(elementType);
        boolean required = isFieldRequired(element);
    
        //8、将上述信息封装成FieldViewBinding,加到builder中。
        builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
    
        // Add the type-erased version to the valid binding targets set.
        erasedTargetNames.add(enclosingElement);
      }
    

    4.2.2@BindString:

      private void parseResourceString(Element element,
          Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
        boolean hasError = false;
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    
        // Verify that the target type is String.
        if (!STRING_TYPE.equals(element.asType().toString())) {
          error(element, "@%s field type must be 'String'. (%s.%s)",
              BindString.class.getSimpleName(), enclosingElement.getQualifiedName(),
              element.getSimpleName());
          hasError = true;
        }
    
        // Verify common generated code restrictions.
        hasError |= isInaccessibleViaGeneratedCode(BindString.class, "fields", element);
        hasError |= isBindingInWrongPackage(BindString.class, element);
    
        if (hasError) {
          return;
        }
    
        // Assemble information on the field.
        String name = element.getSimpleName().toString();
        int id = element.getAnnotation(BindString.class).value();
        QualifiedId qualifiedId = elementToQualifiedId(element, id);
        BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
        builder.addResource(
            new FieldResourceBinding(getId(qualifiedId), name, FieldResourceBinding.Type.STRING));
    
        erasedTargetNames.add(enclosingElement);
      }
    

    看代码,跟绑定View的过程基本一致,唯一的区别是它最后封装成了
    FieldResourceBinding类,然后通过addResource加到builder中。

    4.2.3@OnClick:

    所有Listener的解析都在parseListenerAnnotation方法中,代码总共200多行,这里不一一分析了,下面截取关键代码做解析。
    以如下注解代码为例:

    package test;
    import butterknife.OnClick;
        public class Test {
          @OnClick(1) void doStuff() {}
       }
    
    
     ExecutableElement executableElement = (ExecutableElement) element;//(doStuff())
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();//(test.Test)
    
        Annotation annotation = element.getAnnotation(annotationClass);//(interface butterknife.OnClick)
        Method annotationValue = annotationClass.getDeclaredMethod("value");//(public abstract int[] butterknife.OnClick.value())
        
    int[] ids = (int[])annotationValue.invoke(annotation);//(1)
    String name = executableElement.getSimpleName().toString();//(doStuff)
        boolean required = isListenerRequired(executableElement);//(true)
    
        //
    

    上面是注解及其节点的一些基本属性。后面的值是调试过程中看到的值。

    下面看下OnClick注解的定义:

    @Target(METHOD)
    @Retention(CLASS)
    @ListenerClass(
        targetType = "android.view.View",
        setter = "setOnClickListener",
        type = "butterknife.internal.DebouncingOnClickListener",
        method = @ListenerMethod(
            name = "doClick",
            parameters = "android.view.View"
        )
    )
    public @interface OnClick {
      /** View IDs to which the method will be bound. */
      @IdRes int[] value() default { View.NO_ID };
    }
    

    它有一个ListenerClass的注解,用来定义目标类型、设置方法、listener类型、method(listener里面只有一个回调时使用)、callbacks(listener里面有多个回调时使用)。

    比如OnTextChanged里面的ListenerClass注解是这样的,它只实现OnTextChanged方法的回调:

    @ListenerClass(
        targetType = "android.widget.TextView",
        setter = "addTextChangedListener",
        remover = "removeTextChangedListener",
        type = "android.text.TextWatcher",
        callbacks = OnTextChanged.Callback.class
    )
    

    继续看parseListenerAnnotation方法里面的代码。

    ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class);
    
    ListenerMethod method;
    ListenerMethod[] methods = listener.method();
    

    通过解析ListenerClass,获取一个method实例。然后再根据methodParameters及其其他一些逻辑生成一个参数列表。

    MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required);
    
    builder.addMethod(getId(qualifiedId), listener, method, binding)
    

    最后把注解listener、method和binding加入到builder中,用户后续生成代码。

    4.2.4 上面大致分析了三种不同注解的解析方式,下面看下到底是如何通过BuildSet生成java类的。

    4.3 生成.java文件

    JavaFile javaFile = binding.brewJava(sdk);
    javaFile.writeTo(filer);
    

    ButterKnifeProcessor中process方法中上面的代码触发了生成代码的逻辑,最后写到文件中。

    JavaFile brewJava(int sdk) {
        return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
            .addFileComment("Generated code from Butter Knife. Do not modify!")
            .build();
      }
    

    brewJava调用createType构造具体的类。其中最重要的代码是:
    result.addMethod(createBindingConstructor(sdk));
    它负责在构造方法中绑定view,生成的代码类似(源码中SimpleActivity)如下:

    @UiThread
      public SimpleActivity_ViewBinding(final SimpleActivity target, View source) {
        this.target = target;
    
        View view;
        target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
        target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
        view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
        target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
        view2130968578 = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
          @Override
          public void doClick(View p0) {
            target.sayHello();
          }
        });
        view.setOnLongClickListener(new View.OnLongClickListener() {
          @Override
          public boolean onLongClick(View p0) {
            return target.sayGetOffMe();
          }
        });
        view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");
        target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);
        view2130968579 = view;
        ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {
          @Override
          public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
            target.onItemClick(p2);
          }
        });
        target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
        target.headerViews = Utils.listOf(
            Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), 
            Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), 
            Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));
      }
    

    下面结合createBindingConstructor方法做具体分析,直接在代码中进行注释说明。

    private MethodSpec createBindingConstructor(int sdk) {
    ========
        MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
            .addAnnotation(UI_THREAD)
            .addModifiers(PUBLIC);
    ========1、上面代码在方法SimpleActivity_ViewBinding增加UI_THREAD注解和public修饰符。
        if (hasMethodBindings()) {
          constructor.addParameter(targetTypeName, "target", FINAL);
        } else {
          constructor.addParameter(targetTypeName, "target");
        }
    ========2、上面代码增加参数final SimpleActivity target
        if (constructorNeedsView()) {
          constructor.addParameter(VIEW, "source");
        } else {
          constructor.addParameter(CONTEXT, "context");
        }
    ========3、上面代码增加参数View source
    
        if (hasUnqualifiedResourceBindings()) {
          // Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
          constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
              .addMember("value", "$S", "ResourceType")
              .build());
        }
    
        if (hasOnTouchMethodBindings()) {
          constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
              .addMember("value", "$S", "ClickableViewAccessibility")
              .build());
        }
    
    ===========
        if (parentBinding != null) {
          if (parentBinding.constructorNeedsView()) {
            constructor.addStatement("super(target, source)");
          } else if (constructorNeedsView()) {
            constructor.addStatement("super(target, source.getContext())");
          } else {
            constructor.addStatement("super(target, context)");
          }
          constructor.addCode("\n");
        }
        ========上面代码是判断有继承的情况
        
        if (hasTargetField()) {
          constructor.addStatement("this.target = target");
          constructor.addCode("\n");
        }
    ========上面代码增加this.target = target;
        if (hasViewBindings()) {
          if (hasViewLocal()) {
            // Local variable in which all views will be temporarily stored.
            constructor.addStatement("$T view", VIEW);
          }
          for (ViewBinding binding : viewBindings) {
            addViewBinding(constructor, binding);
          }
          for (FieldCollectionViewBinding binding : collectionBindings) {
            constructor.addStatement("$L", binding.render());
          }
    
          if (!resourceBindings.isEmpty()) {
            constructor.addCode("\n");
          }
        }
    ========上面代码实现view的绑定,具体代码在addViewBinding,它会根据不同的情况生成不同的代码;
        if (!resourceBindings.isEmpty()) {
          if (constructorNeedsView()) {
            constructor.addStatement("$T context = source.getContext()", CONTEXT);
          }
          if (hasResourceBindingsNeedingResource(sdk)) {
            constructor.addStatement("$T res = context.getResources()", RESOURCES);
          }
          for (ResourceBinding binding : resourceBindings) {
            constructor.addStatement("$L", binding.render(sdk));
          }
        }
    ========上面代码实现resource的绑定;
        return constructor.build();
      }
    

    addViewBinding方法里面的大致步骤是先findView,然后调用addFieldBinding(result, binding)和addMethodBindings(result, binding)实现view的绑定和listener的设置。

    5 运行期间

    运行期间,ButterKnife.bind(this)通过反射去获取对应的xxx__ViewBinding类的实例,在该类的构造方法中,完成view或者resource的查找和绑定,以及listener的设置。

    public static Unbinder bind(@NonNull Activity target) {
        View sourceView = target.getWindow().getDecorView();
        return createBinding(target, sourceView);
      }
    
     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);
        } 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);
        }
      }
    

    参考链接:
    1、https://github.com/ShowJoy-com/showjoy-blog/issues/30?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
    2、http://www.jianshu.com/p/b8b59fb80554
    3、http://blog.csdn.net/crazy1235/article/details/51876192
    4、http://blog.csdn.net/asce1885/article/details/52878076

    相关文章

      网友评论

          本文标题:ButterKnife,不简单的Annotation

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