View注解框架----ButterKnife

2018-10-13  本文已影响0人  海盗的帽子

csdn
个人博客

一.简介

ButterKnife 是 jake Wharton 的一个用于 View 注解框架,目前已经有 22000+ star , ButterKnife 的用处就是为开发者减少类似于 findViewById, setOnClickListener 等重复的代码,取代之的是通过注解对代码进行标记,让编译器自动生成需要的代码。

那么 ButterKnife 究竟怎么实现的呢?简单地说就是使用了注解,通常我们对注解的处理,又两种方式,一是通过反射处理注解,一种是通过注解处理器,而 ButterKnife 就是通过注解处理器实现对注解的处理,但是在整个框架的其他地方中还是有使用到反射的。

二.反射

1.反射的由来

java 对象在运行的时候有时因为多态的原因会产生两种类型 :

比如对于 Person p = new Student,它的编译的时类型就是 Person,而运行时类型就是 Student。对于编译时类型在编译前就可以确定,而对于运行时类型,因为只有在运行的时候才能确定,所以有时不能直接通过对象进行访问。但是有时程序需要调用的是对象在运行时的类型方法,那有该怎么办呢?
解决方式有两种:

2.反射的使用

反射就是根据一些类的信息确定一个对象属于哪个类还可以因此生成一个该类的对象。首先就是获取一个类的 Class 对象,每一个类都有一个 Class 对象,保存着这个类的信息。

(1)获取 Class 对象
(2)从Class 中获取信息
(3)通过反射生成并操作一个对象

1.创建一个对象

2.调用方法

每个 Method 有一个 invoke (Object ,Object...)方法,Object 是方法的主调就是方法的调用对象,Object... 是方法的参数

3.访问成员变量

通过 getFields/getField 可以对成员变量进行访问,set/get 方法。

在访问成员变量和调用方法的时候需要对他们的访问权限做一些处理,setAccessible(boolean flag) :

三.注解 Annotation

注解就是对部分代码进行标记,然后可以在编译,类加载,运行的时候被读取,并执行相应的处理, 从而对源文件补充一些信息。

1.基本的 Annotation

2.自定义的 Annotation

定义一个 Annotation 与接口类似,不过使用的是 @interface 而不是 interface 。一个 Annotation,可以带成员变量 且以无形参的方法来声明,其方法和返回值定义了成员变量的名字和类型。也可以设置默认值 default .

自定义的 Annotation 可以分为两类:

2.元 Annotation

元 Annotation 就是对注解进行注解,换句话说就是对自定义的注解补充一些信息。

1.@Retention

只能用于修饰 Annotation 定义,也就是在自定义一个注解的时候修饰在 @interface 上方。Retention 指定被修饰的 Annotation 可以保留多长的时间,具体策略需要 指定 value 成员变量,有三种可选的时间长度策略。

2.@Target

用于指定修饰的 Annotation 能用于修饰哪些程序元素,包含一个 value 变量,同样只能修饰一个 Annotation 定义。具体有以下策略

3.@Inherited

指定修饰的 Annotation 具有继承性,则继承的类都具有默认定义的 Annotation。

3.提取 Annotation

只有 使用 @Retention(RetentionPolicy.RUNTIME )修饰,JVM 才会在 装载 class 的时候提取 Annotation ,因为注解可以修饰类,方法等,因此注解提取的信息是一个 AnnotationElement,这是所有程序元素的(class , Method , Constructor ) 的父接口。也可以通过 getAnnotation 等直接提取出 Annotation 信息。

4.编译时注解 Annotation

前面说过注解信息的提取实在 JVM 装载 class 的时候提取的,因此 @Retention 的 参数就设置为 RetentionPolicy.RUNTIME ,但是这多少对 JVM 的性能有所消耗,那有什么优化的方法吗?答案就是使用 编译时处理 Annotation 技术,Annotation Processing Tool 简称 APT。 APT 的原理就是通过继承 继承 AbstractProcessor 类实现一个 APT 工具,并装在到编译类库中,编译的时候就会使用 APT 工具处理 Annotation, 可以根据源文件中的 Annotation 生成额外的源文件和其他文件,APT 还会编译生成的代码文件和原来的源文件将他们一起生成 class 文件,这些附属文件的内容也都与源代码的相关。而对注解的信息的处理通常就放在生成的文件中,在运行时就不用再提取,直接关联并执行处理即可。

三.ButterKnife

下面以 findViewById 和 setOnClickListener 对应的 @BindView 和 @ OnClick 为例简单介绍 ButterKnife 宏观实现。

1.定义注解

在 ButterKnife 源码中 butterknife-annotations 库主要用来存放自定义注解。可注解的类型如图所示:

image.png

@BindView 的注解:

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

BindView 有两个注解修饰,从前面的关于注解的知识就可以知道 @Retention(RUNTIME) 表示在保留注解信息到运行的时候, @Target(FIELD) 表示这个注解只能修饰成员变量。

@OnClick 的注解

/**
 * Bind a method to an {@link OnClickListener OnClickListener} on the view for each ID specified.
 * <pre><code>
 * {@literal @}OnClick(R.id.example) void onClick() {
 *   Toast.makeText(this, "Clicked!", Toast.LENGTH_SHORT).show();
 * }
 * </code></pre>
 * Any number of parameters from
 * {@link OnClickListener#onClick(android.view.View) onClick} may be used on the
 * method.
 *
 * @see OnClickListener
 */
@Target(METHOD)
@Retention(RUNTIME)
@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 };
}

BindView 有三个注解修饰, @Retention(RUNTIME) 表示在保留注解信息到运行的时候, @Target(METHOD) 表示这个注解只能方法,@ListenerClass 就是对方法进一步描述,比如调用者类型 targetType,调用方法 setter,方法监听 type,最后执行的方法和方法参数 name ,parameters。

2.注解处理器

ButterKnifeProcessor 注解处理器是 APT 中对注解处理的关键类,在 ButterKnife 中注解处理器位于 butterknife - compiler 包中。

image.png

ButterKnifeProcessor 继承自 AbstractProcessor ,而 AbstractProcessor 实现了 Processor 接口,这个接口主要有两个重要的方法。

public interface Processor {
   
    ...
    void init(ProcessingEnvironment processingEnv);
    
    ...
    
    boolean process(Set<? extends TypeElement> annotations,
                    RoundEnvironment roundEnv);
    ...
}

init 方法 :

@Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if (sdk != null) {
      try {
        this.sdk = Integer.parseInt(sdk);
      } catch (NumberFormatException e) {
        env.getMessager()
            .printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
                + sdk
                + "'. Falling back to API 1 support.");
      }
    }

    debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
    useAndroidX = hasAndroidX(env.getElementUtils());

    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
    }
  }

这个方法主要的作用是:

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, useAndroidX);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

在 brewJava 方法中 有如下代码:

 static Builder newBuilder(TypeElement enclosingElement) {
    TypeMirror typeMirror = enclosingElement.asType();

    boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
    boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
    boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);

    TypeName targetType = TypeName.get(typeMirror);
    if (targetType instanceof ParameterizedTypeName) {
      targetType = ((ParameterizedTypeName) targetType).rawType;
    }

    String packageName = getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");   //注意这段命名

    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
  }

对于 process 方法,主要做了如下几件事:

为了验证,在 MainActicity 中使用 ButterKnife 注解,然后运行一下,最后可以在 build-intermediates-classes 下找到对应的 MainActivity_ViewBinding。

image.png
2.绑定 bind 方法

在经过前面两个步骤后,对于 ButterKnife 的使用也就是一句 bind 方法。

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

    }

直接看 bing 方法的具体实现

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

对于 bind 方法,首先就是获取 目前 Window 的一个 根视图 DecorView ,这里可以比较一下 findViewById 的实现中有这么一段

public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
        return create(activity, activity.getWindow(), callback);
    }

通过比较就可以看出,对于注解来说最后的实现还是一样的。下面接着看 createBinding

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

   
    try {
      return constructor.newInstance(target, source);
    } catch 
      ...
  }

在这里 会先去获取一个对象的构造器 ,然后调用这个构造器去实例化一个对象 。获取构造器的方法对应着 findBindingConstructorForClass

  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
   
    String clsName = cls.getName();
    try {
      Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
      
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
     ...
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }
  }

在这个方法中,可以看到这个构造器的类就是以 ++clsName + "_ViewBinding"++,为名字的,以 MainActivity 为例就是对应着前面的 MainActivity_ViewBinding 。

最后是通过构造器的 newInstance(target, source) 方法创建了一个对象,并将 Context 对象(target) 和 DecorView (source) 传过去。

以 @BindView 和 @OnClick 为例,最后看生成的 MainActivity_ViewBinding 类。

public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
    protected T target;
    private View view2131165218;

    @UiThread
    public MainActivity_ViewBinding(final T target, View source) {
        this.target = target;
        View view = Utils.findRequiredView(source, 2131165218, "field 'mButtonOne' and method 'click'");
        target.mButtonOne = (Button)Utils.castView(view, 2131165218, "field 'mButtonOne'", Button.class);
        this.view2131165218 = view;
        view.setOnClickListener(new DebouncingOnClickListener() {
            public void doClick(View p0) {
                target.click();
            }
        });
        target.mButtonTwo = (Button)Utils.findRequiredViewAsType(source, 2131165219, "field 'mButtonTwo'", Button.class);
    }

    @CallSuper
    public void unbind() {
        T target = this.target;
        if(target == null) {
            throw new IllegalStateException("Bindings already cleared.");
        } else {
            target.mButtonOne = null;
            target.mButtonTwo = null;
            this.view2131165218.setOnClickListener((OnClickListener)null);
            this.view2131165218 = null;
            this.target = null;
        }
    }
}

到这里就很清楚了,所有的 findViewById 和 setOnClickListener 等操作最后都是在这里实现了。 所有的参数都是从构造器里传进来的,进行绑定。

ButterKnife 实现的大致流程:

总的来说,ButterKnife 在注解的处理上使用的是 注解处理器,在编译的时候将注解处理好,从而减少运行的时候对虚拟机性能的消耗。

最后

为了更好地理解整个过程,通过学习

使用编译时注解简单实现类似 ButterKnife 的效果

Android 进阶 教你打造 Android 中的 IOC 框架

Android 打造编译时注解解析框架

用反射和编译时注解连两种不同的方式简单的实现了相同的效果。

github 地址

上一篇下一篇

猜你喜欢

热点阅读