Android之注解、APT、android-apt 和 annotationProcessor 的区别

马承
2023-12-01

APT、android-apt 和 annotationProcessor 的区别

  • APT是什么?
    apt是 Annotation Processing Tool 的缩写,顾名思义,就是注解处理工具,用于编译时对注解进行解析,自动生成代码,并编译代码生成class文件,大体就是这个过程。
  • android-apt是什么?
    android-apt是第三方开源的注解处理框架,因为一开始Android没有默认的支持注解。dagger、ButterKnife等流行的注解框架,都是用的android-apt进行的注解处理。
  • annotationProcessor是什么?
    在Android studio Gradle插件2.2版本发布后,Google开始支持注解处理,而android-apt作者宣布不再维护更新,建议使用官方提供的annotationProcessor。
  • android-apt 和 annotationProcessor异同
    相同:两者都是apt 工具,只在编译的时候执行依赖的库,但是库最终不打包到apk中。
    不同:
             1)前者只支持 javac 的编译方式,而后者支持 javac 和 jack 两种。
             
    2)Android Gradle 插件2.2版本后已经内置annotationProcessor,不需要引入,直接在build.gradle文件配置使用。
     
  • 以dagger的使用为例,
    1)添加android-apt框架的方法:

  • step a)在project的build.gradle中添加
    buildscript {
        repositories {
          mavenCentral()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:2.1.0'
            classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' //配置插件
        }
    }

    step b)在app的build.gradle中添加

    apply plugin: 'com.neenbedankt.android-apt' //应用插件

    dependencies {
     compile 'com.squareup.dagger:dagger:1.1.0'
     apt 'com.squareup.dagger:dagger-compiler:1.1.0' //注解编译器
    }


    2)添加annotationProcessor框架的方法

    step a)在project的build.gradle中添加

  • buildscript {
        dependencies {
            classpath 'com.android.tools.build:gradle:2.2.0'   // Gradle 为 2.2及以后的版本
        }
    }

    step b)在app的build.gradle中添加
    dependencies {
        compile 'com.google.dagger:dagger:2.0'
        annotationProcessor 'com.google.dagger:dagger-compiler:2.0'
    }
    使用annotationProcessor后,只需要配置这一处即可,但是不要忘记,必须要在Gradle plugin 2.2及以上才能使用。

注解的定义

注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明 。注解不会改变编译器的编译方式,也不会改变虚拟机指令执行的顺序,它更可以理解为是一种特殊的注释,本身不会起到任何作用,需要工具方法或者编译器本身读取注解的内容继而控制进行某种操作。

注解的用途

注解可以在代码编译期间帮我们完成一些复杂的准备工作。
比如Greendao,我们注解一个实体类,它要处理成好多逻辑关系类,这些逻辑类让我们自己去书写的话那将是一个庞大的代码量, extends AbstractDao等这些类。
比如BufferKnife,我们用注解将控件的属性传递给它,它将生成一些功能类去处理这些值,***_ViewBinding等这些类型的类。运行时注解的使用,比如Retrofit的@GET或者@POST等等都是运行时注解,它的注解的处理必须跟Retrofit的对象有关联,所以必须定义成运行时的。所以注解已经成为一种趋势,比如BufferKnife,EventBus,Dagger,Greendao,Arouter,Retrofit等。

元注解(meta-Annotation)

元注解有四种(首字母D-I-R-T),作用就是负责注解其他注解:

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AsynLog {
   
}

@Target

作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方) 
取值(ElementType)有: 

ElementType.CONSTRUCTOR修饰构造器
ElementType.LOCAL_VARIABLE修饰局部变量
ElementType.ANNOTATION_TYPE修饰注解
ElementType.PACKAGE修饰包
ElementType.PARAMETER修饰参数
ElementType.METHOD修饰方法
ElementType.FIELD修饰成员变量
ElementType.TYPE修饰类、接口或枚举类型

@Retention

作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效) 
取值有以下三种:

RetentionPolicy.SOURCE只在源代码中保留,一般都是用来增加代码的理解性或者帮助代码检查之类的,比如我们的Override;
RetentionPolicy.CLASS 默认的选择,能把注解保留到编译后的字节码class文件中,仅仅到字节码文件中,运行时是无法得到的;
RetentionPolicy.RUNTIME注解不仅 能保留到class字节码文件中,还能在运行通过反射获取到,这也是我们最常用的。

 @Documented

@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。

@Inherited

 @Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。 
这些类型和它们所支持的类在java.lang.annotation包中可以找到。


如何实现自定义注解?

@interface自定义注解自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。

定义注解格式:
  public @interface 注解名 {定义体}


  注解参数的可支持数据类型:

    1.所有基本数据类型(int,float,boolean,byte,double,char,long,short) 
    2.String类型 
    3.Class类型 
    4.enum类型 
    5.Annotation类型 
    6.以上所有类型的数组

实例

有了上面的基础知识后,下面通过实例分析注解框架原理。

1、首先编写一个普通布局文件activity_main.xml 包含一个button:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="测试"/>

</LinearLayout>

如果在没有使用注解框架前我们的使用会是下面这样:

public class MainActivity extends Activity implements OnClickListener{
    private Button btn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 找到按钮
        bt = (Button) findViewById(R.id.btn);
        // 设置点击事件
        bt.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Toast.makeText(this, "测试", Toast.LENGTH_LONG).show();
    }
}

在使用了注解的写法后变成了这样:

public class MainActivity extends Activity {

    @BindView(value = R.id.btn, click = "clickview")
    private Button btn;

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

    public void clickview() {
         Log.d("MainActivity : ", "click");
    }

}

注解框架是帮我们完成了findViewById(int)和setOnClickListener(this)的过程。
代码省去了findViewById(int)和setOnClickListener(this)的这些操作,变得更简洁了。如上代码 @BindView(value = R.id.btn, click = "clickview") 中的R.id.btn就是value的值是我们需要传递的 方法名clickview可以通过反射获取方法得到。当需要的信息传递过去后,我们需要读取框架的信息然后让框架生效,这里是通过 Butterfly.bind(this); 来实现的。是不是感觉有点Butterknife的样子了,别急下面我们开始编写:

代码时刻: 
1、首先我们创建一个注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();

    String click() default "";
}


@Target(ElementType.FIELD)表示我们声明的注解是作用到字段上的,就是这里的Button 
@Retention(RetentionPolicy.RUNTIME)这个表示我们声明的注解是运行的时候有效的 
这里定义了两个属性: 
value int类型 表示控件的id 
click String类型 点击事件的方法名称

 

2、新定义一个类 Butterfly.java:
定义完上面这些你已经可以去MainActivity里面字段上面定义使用了,但是不会生效。为了让注解生效,接下来我们编写读取注解中方法的核心部分,

public class Butterfly {

    public static void bind(Activity act) {
        Class c = act.getClass();
        // 获取这个activity中的所有字段
        Field[] fields = c.getDeclaredFields();

        for (Field field : fields) {
            // 循环拿到每一个字段
           
if (field.isAnnotationPresent(BindView.class)) { // 如果这个字段有注入的注解
                // 获取注解对象
               
BindView b = field.getAnnotation(BindView.class);
                int value = b.value();
                field.setAccessible(true);  // 即使私有的也可以设置数据
                Object view = null;
                try {
                    view = act.findViewById(value);
                    // 设置字段的属性
                    field.set(act, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    Log.i("TAG", "注入属性失败:" + field.getClass().getName() + ":" + field.getName());
                }

                try {
                    if (view instanceof View) {
                        View v = (View) view;
                        String methodName = b.click(); // 获取点击事件的触发的方法名称
                        EventListener eventListener = null;
                        if (!TextUtils.isEmpty(methodName)) {
                            eventListener = new EventListener(act);
                            v.setOnClickListener(eventListener);
                            eventListener.setClickMethodName(methodName);
                        }
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

从上面我们可以看到,我们首先通过使用反射act.getClass()获取到了类的实例,然后 通过c.getDeclaredFields() 获取到了类里面的所有字段,接着for循环拿到每一个属性筛选出有 BindView.class 注解的字段, 拿到id的信息(也就是value的值)再调用findViewById(int)找到控件,利用反射赋值。 
最后设置点击事件这里是定义了一个EventListener 的类下面会给出;这里主要是创建EventListener 对象,然后保存了点击事件的方法名称,最后通过反射获取方法进行处理;这些都在EventListener 里面完成;

3、定义EventListener.java 类:

public class EventListener implements View.OnClickListener {
    private String TAG = "EventListener";
    private Object receiver = null;
    private String clickMethodName = "";

    public EventListener(Object receiver) {
        this.receiver = receiver;
    }

    public void setClickMethodName(String clickMethodName) {
        this.clickMethodName = clickMethodName;
    }

    @Override
    public void onClick(View v) {
        Method method = null;
        try {
            method = receiver.getClass().getMethod(clickMethodName);
            if (method != null) {
                method.invoke(receiver);
            }
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "未找到:" + clickMethodName + "方法");
        }
            try {
                if (method == null) {
                    method = receiver.getClass().getMethod(clickMethodName, View.class);
                    if (method != null) {
                        method.invoke(receiver, v);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "未找到带view类型参数的:" + clickMethodName + "方法");
            }
    }
}


可以看到EventListener 里面通过传进来的方法名然后通过反射receiver.getClass().getMethod() 获取到了方法。EventListener 已经实现了View.OnClickListener接口;到这里view的点击事件就实现完了。

通过上面带注解的MainActivity.java即可愉快的使用注解了。至此一个简单的注解框架就完成了,当然这里只是给出了一个view的点击事件,其他的事件也可以类似实现。因为所有代码上面都已经给出了,这里就没有提供demo。

以上就是ButterKnife的原理。

顺便说一下反射的基本用法,以帮助理解。

反射 (Reflection)

  • 定义:反射机制是指在运行状态中 
    对于任意一个类,都能知道这个类的所有属性和方法; 
    对于任何一个对象,都能够调用它的任何一个方法和属性; 

    这样动态获取新的以及动态调用对象方法的功能就叫做反射。
     
  • 一般通过反射获取一个类的信息主要有以下几个方法:
    1)获取Class
    //以String类为例
    //方法一:
    Class c = Class.forName("java.lang.String");  //这里一定是用完整的包名

    //方法二:
    Class c1=String.class;

    //方法三:
    String str = new String();
    Class c2=str.getClass();

    这里获取的c、c1、c2都是相等的。第一种写法我们会常见点。

    2)获取类的属性(成员变量)
    Field[] fields = c.getDeclaredFields();

    这里返回的是一个数组 ,包含所有的属性。获取到的每一个属性Filed,包含一系列的方法可以获取及修改他的内容。

    3)获取类的方法
    Method[] mt = c.getDeclaredMethods();  // 获取所有的方法

 

 

 

不信?好吧,带你去看看!

一、简单使用

首先编写一个 ButterKnife 简单使用的例子,方便后续的分析。先在 app的 build.gradle 中加入如下配置,完成 ButterKnife 引入:

dependencies {
    ......
    implementation 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

接下来在 Activity 中使用,界面上一个TextView一个Button,很简单就不解释了:

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_title)
    TextView title;

    @OnClick(R.id.bt_submit)
    public void submit() {
        title.setText("hello world");
    }

    private Unbinder unbinder;

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

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

二、原理分析

最后编译一下项目。直觉告诉我们应该从ButterKnife.bind(this)开始分析,因为它像是 ButterKnife 和 Activity 建立绑定关系的过程,看具体的代码:

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

sourceView代表当前界面的顶级父 View,是一个FrameLayout,继续看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);
    } 
    // 省略了相关异常处理代码
  }

首先得到要绑定的 Activity 对应的 Class,然后用根据 Class 得到一个继承了UnbinderConstructor,最后通过反射constructor.newInstance(target, source)得到Unbinder子类的一个实例,到此ButterKnife.bind(this)操作结束。这里我们重点关注findBindingConstructorForClass()方法是如何得到constructor实例的:

@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:

Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");

这里新的 Class 的 name 是 com.shh.sometest.MainActivity_ViewBinding,最后用新的 Class 创建一个 继承了Unbinder的 Constructor,并添加到BINDINGS

bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
BINDINGS.put(cls, bindingCtor);

所以最终bind()方法返回的是MainActivity_ViewBinding类的实例。既然可以返回MainActivity_ViewBinding的实例,那MainActivity_ViewBinding这个类肯定是存在的。可以在如下目录找到它(这个类是在项目编译时期由 annotationProcessor生成的,关于 annotationProcessor 后边会说到):

MainActivity_ViewBinding

 

来看看它里边都做了那些事:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131165217;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

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

    View view;
    target.title = Utils.findRequiredViewAsType(source, R.id.tv_title, "field 'title'", TextView.class);
    view = Utils.findRequiredView(source, R.id.bt_submit, "method 'submit'");
    view2131165217 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.submit();
      }
    });
  }

  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.title = null;

    view2131165217.setOnClickListener(null);
    view2131165217 = null;
  }
}

之前createBinding()方法中return constructor.newInstance(target, source);操作使用的就是MainActivity_ViewBinding类两个参数的构造函数。

重点看这个构造函数,首先是给target.title赋值,即MainActivity中的TextView,用到了一个findRequiredViewAsType()方法:

public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
  }

继续看findRequiredView()方法:

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()得到对应View,然后就是castView()

public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
      String name = getResourceEntryName(view, id);
      throw new IllegalStateException("View '"
          + name
          + "' with ID "
          + id
          + " for "
          + who
          + " was of the wrong type. See cause for more info.", e);
    }
  }

核心就是把findRequiredView()得到的 View 转成指定类型的 View ,如果 xml 中定义的 View 和 Activity 中通过注解绑定的 View 类型不一致,就会抛出上边方法的异常,可能很多人都遇到过。这样target.title的赋值就结束了,接下来就是直接使用findRequiredView()找到对应 id 的Button,不用进行类型转换,然后给它绑定点击事件,最终调用了在MainActivity中给Button绑定点击事件时定义的submit()方法。到这里就完成了 相关 View 的赋值以及事件绑定!

MainActivity_ViewBinding类中还有一个unbind()方法,需要在ActivityFragmentonDestory()中调用,以完成 相关 View 引用的释放以及点击事件的解绑操作!

三、注解处理器

那么,MainActivity_ViewBinding类时如何生成的呢?首先,要生成这个类就要先得到这个类必须的基础信息,这就涉及到了annotationProcessor技术,和 APT(Annotation Processing Tool)技术类似,它是一种注解处理器,项目编译时对源代码进行扫描检测找出存活时间为RetentionPolicy.CLASS的指定注解,然后对注解进行解析处理,进而得到要生成的类的必要信息,然后根据这些信息动态生成对应的 java 类,至于如何生成 java 类就涉及到了后边要说的 JavaPoet技术。

这里我们先看用注解处理器收集类信息的过程,之前我们已经在app的 build.gradle引入了 ButterKnife 的注解处理器: butterknife-compiler,其中有一个ButterKnifeProcessor类完成了注解处理器的核心逻辑。这个类约有1500行代码,先看下它的基本框架结构:

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
  
    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        String sdk = env.getOptions().get(OPTION_SDK_INT);
        ......
        debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
        elementUtils = env.getElementUtils();
        typeUtils = env.getTypeUtils();
        filer = env.getFiler();
        try {
            trees = Trees.instance(processingEnv);
        } catch (IllegalArgumentException ignored) {
        }
    }

    @Override
    public Set<String> getSupportedOptions() {
        return ImmutableSet.of(OPTION_SDK_INT, OPTION_DEBUGGABLE);
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

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

   @Override
   public SourceVersion getSupportedSourceVersion() {
       return SourceVersion.latestSupported();
   }
}

注意:ButterKnifeProcessor类上使用了@AutoService(Processor.class)注解,来实现注解处理器的注册,注册到 javac后,在项目编译时就能执行注解处理器了。

ButterKnifeProcessor继承了AbstractProcessor抽象类,并重写以上五个方法,如果我们自定义解处理器也是类似的,看下这几个方法:

1、init()

首先 init()方法完成sdk版本的判断以及相关帮助类的初始化,帮助类主要有以下几个:

  • Elements elementUtils,注解处理器运行扫描源文件时,以获取元素(Element)相关的信息。Element 有以下几个子类:
    包(PackageElement)、类(TypeElement)、成员变量(VariableElement)、方法(ExecutableElement)
  • Types typeUtils,
  • Filer filer,用来生成 java 类文件。
  • Trees trees,

2、getSupportedAnnotationTypes()

该方法返回一个Set<String>,代表ButterKnifeProcessor要处理的注解类的名称集合,即 ButterKnife 支持的注解:butterknife-annotations

3、getSupportedSourceVersion()

返回当前系统支持的 java 版本。

4、getSupportedOptions()

返回注解处理器可处理的注解操作。

5、process()

最后,process() 方法是我们要重点分析的,在这里完成了目标类信息的收集并生成对应 java 类。
首先看注解信息的扫描收集,即通过findAndParseTargets()方法:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
        Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
        Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

        scanForRClasses(env);
        ......
        ......
        // env.getElementsAnnotatedWith(BindView.class)获取所有使用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;
    }

只保留了BindView注解信息的扫描解析过程,省略其它类似逻辑,先将扫描得到的注解相关信息保存到builderMaperasedTargetNames中,最后对这些信息进行重新整理返回一个以TypeElement为 key 、BindingSet为 value 的 Map,其中TypeElement代表使用了 ButterKnife 的类,即 Activity、Fragment等,BindingSetbutterknife-compiler中的一个自定义类,用来存储要生成类的基本信息以及注解元素的相关信息。
parseBindView()方法用来解析使用了BindView注解的元素:

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
                               Set<TypeElement> erasedTargetNames) {
        // 首先要注意,此时element是VariableElement类型的,即成员变量
        // enclosingElement是当前元素的父类元素,一般就是我们使用ButteKnife时定义的View类型成员变量所在的类,可以理解为之前例子中的MainActivity
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

        // 进行相关校验
        // 1、isInaccessibleViaGeneratedCode(),先判断当前元素的是否是private或static类型,
        // 再判断其父元素是否是一个类以及是否是private类型。
        // 2、isBindingInWrongPackage(),是否在系统相关的类中使用了ButteKnife注解
        boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
                || isBindingInWrongPackage(BindView.class, element);

        // TypeMirror表示Java编程语言中的一种类型。 类型包括基元类型,声明的类型(类和接口类型),数组类型,类型变量和空类型。 
        // 还表示了通配符类型参数,可执行文件的签名和返回类型,以及与包和关键字void相对应的伪类型。
        TypeMirror elementType = element.asType();
        // 如果当前元素是类的成员变量
        if (elementType.getKind() == TypeKind.TYPEVAR) {
            TypeVariable typeVariable = (TypeVariable) elementType;
            elementType = typeVariable.getUpperBound();
        }
        Name qualifiedName = enclosingElement.getQualifiedName();
        Name simpleName = element.getSimpleName();
        // 判断当前元素是否是 View 的子类,或者是接口,不是的话抛出异常
        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;
            }
        }
        
        if (hasError) {
            return;
        }

        // 获得元素使用BindView注解时设置的属性值,即 View 对应的xml中的id
        int id = element.getAnnotation(BindView.class).value();
        // 尝试获取父元素对应的BindingSet.Builder
        BindingSet.Builder builder = builderMap.get(enclosingElement);
        // QualifiedId记录了当前元素的包信息以及id
        QualifiedId qualifiedId = elementToQualifiedId(element, id);
        if (builder != null) {
            String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
            // 如果当前id已经被绑定,则抛出异常
            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 {
            // 创建一个新的BindingSet.Builder并返回,并且以enclosingElement 为key添加到builderMap中
            builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
        }

        String name = simpleName.toString();
        TypeName type = TypeName.get(elementType);
        // 判断当前元素是否使用了Nullable注解
        boolean required = isFieldRequired(element);
        // 创建一个FieldViewBinding,它包含了元素名、类型、是否是Nullable
        // 然后和元素id一同添加到BindingSet.Builder
        builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

        // 记录当前元素的父类元素
        erasedTargetNames.add(enclosingElement);
    }

部分代码已经写了注释,这里看下getOrCreateBindingBuilder()方法的实现:

private BindingSet.Builder getOrCreateBindingBuilder(
      Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
    // 先判断enclosingElement对应的BindingSet.Builder是否已存在
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder == null) {
      // 创建一个BindingSet.Builder
      builder = BindingSet.newBuilder(enclosingElement);
      // 添加到builderMap
      builderMap.put(enclosingElement, builder);
    }
    return builder;
  }

由于用BindingSet保存了注解相关的信息,所以继续了解下它的newBuilder()过程:

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('.', '$');
    // 即最终要生成的java类的名称
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
    // 判断父类元素是否为final类型
    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
  }

所以BindingSet主要保存了要生成的目标类的基本特征信息,以及类中使用了 ButterKnife 注解的元素的信息,这样一个BindingSet就和一个使用了ButterKnife 的类对应了起来。

四、JavaPoet

到这里要生成的目标类基本信息就收集就完成了,接下来就是生成 java 类文件了,再回到 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();
        // 得到java类源码
        JavaFile javaFile = binding.brewJava(sdk, debuggable);
        try {
            // 生成java文件
            javaFile.writeTo(filer);
        } catch (IOException e) {
            error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
        }
    }
    return false;
}

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

在继续往下分析前,先了解下 JavaPoet中一些重要的类(这些类还有许多实用的方法哦):

  • TypeSpec表示类、接口、或者枚举声明
  • ParameterSpec表示参数声明
  • MethodSpec表示构造函数、方法声明
  • FieldSpec表示成员变量,一个字段声明
  • CodeBlock表示代码块,用来拼接代码
  • JavaFile表示Java类的代码

还有几个占位符也了解下:

  • $L,for Literals 替换字符串、原语、JavaPoet中的类型
    例如beginControlFlow("for (int i = $L; i < $L; i++)", 1, 10)
    addStatement("result = result $L i", "+")
  • $S,for Strings 替换字符串,例如addStatement("return $S", "hello")
  • $T,for Types 替换类型,例如addStatement("return new $T()", Date.class)
  • $N,for Names 替换JavaPoet中的声明

有了一些基础概念后,继续看用 JavaPoet生成对应JavaFile的过程:

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

用到的JavaFile.builder()方法需要两个参数:要生成的目标类的包名,以及TypeSpec对象,即createType()方法的返回值,代表一个类、接口、枚举声明。结合文章开始的例子,这个TypeSpec对象应代表一个类声明:

private TypeSpec createType(int sdk, boolean debuggable) {
    // TypeSpec.classBuilder() 方法设置类名称
    // addModifiers() 方法设置类的修饰符为 public
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    // 如果是final类则添加final修饰符
    if (isFinal) {
      result.addModifiers(FINAL);
    }

    if (parentBinding != null) {
      result.superclass(parentBinding.bindingClassName);
    } else {
      // 让当前类Unbinder接口,之前生成的MainActivity_ViewBinding类就实现了Unbinder接口
      result.addSuperinterface(UNBINDER);
    }

    if (hasTargetField()) {
      // 添加一个类成员变量,可以对应到MainActivity_ViewBinding类中的private MainActivity target
      result.addField(targetTypeName, "target", PRIVATE);
    }
    
    // 判断目标类的类型
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      // 由于之前的例子是Activity类型的,所以会走到这里,去生成MainActivity_ViewBinding类默认的构造函数
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
      // Add a delegating constructor with a target type + view signature for reflective use.
      result.addMethod(createBindingViewDelegateConstructor());
    }
    // 生成view绑定的构造函数,可以对应到MainActivity_ViewBinding类两个参数的构造函数
    result.addMethod(createBindingConstructor(sdk, debuggable));

    if (hasViewBindings() || parentBinding == null) {
      // 生成ubinder()方法
      result.addMethod(createBindingUnbindMethod(result));
    }
    return result.build();
  }

这里重点看下createBindingConstructor()的过程,代码较多,只保留部分以方便分析:

private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);
    if (hasMethodBindings()) {
      // 给构造函数添加一个修饰符为final、targetTypeName,名称为target的参数,即final MainActivity target
      constructor.addParameter(targetTypeName, "target", FINAL);
    } else {
      constructor.addParameter(targetTypeName, "target");
    }
    if (constructorNeedsView()) {
      // 给构造函数添加一个View类型的source参数
      constructor.addParameter(VIEW, "source");
    } else {
      constructor.addParameter(CONTEXT, "context");
    }
    ......
    if (hasTargetField()) {
      // 给构造函数添加一行this.target = target;的声明代码
      constructor.addStatement("this.target = target");
      constructor.addCode("\n");
    }

    if (hasViewBindings()) {
      if (hasViewLocal()) {
        // 给构造函数添加一行View view;的声明代码
        constructor.addStatement("$T view", VIEW);
      }
      for (ViewBinding binding : viewBindings) {
        // 根据id查找view并完成赋值
        addViewBinding(constructor, binding, debuggable);
      }
      ......
    }
    ......
    return constructor.build();
  }

那么是如何根据id查找view呢?答案就在addViewBinding()方法,以下解释同样类比MainActivity_ViewBinding两个参数的构造函数:

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
    if (binding.isSingleFieldBinding()) {
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      // CodeBlock 表示代码块,用来完成view查找、赋值语句的拼接
      // 相当于target.title = 
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());
      // 由于例子中title是TextView,不是View所以requiresCast为true
      boolean requiresCast = requiresCast(fieldBinding.getType());
      // debuggable为true、fieldBinding.isRequired()为true,则以下if条件不成立
      if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
        if (requiresCast) {
          builder.add("($T) ", fieldBinding.getType());
        }
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        // 继续给代码块添加Utils.find
        builder.add("$T.find", UTILS);
        // 根据上边的分析可知,会给代码块添加RequiredView
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          // 给代码块添加AsType
          builder.add("AsType");
        }
        // 给代码块添加(source, R.id.tv_title
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          // 继续添加view的相关描述,例如field 'title'
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          继续添加view的Class类型,例如TextView.class
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        // 添加右括号,到这里就完成了view的查找与赋值
        // 例如target.title = Utils.findRequiredViewAsType(source, R.id.tv_title, "field 'title'", TextView.class);
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }

结合对createType()流程的分析,我们基本了解了 MainActivity_ViewBinding 类中构造函数的构建过程、以及 title(之前例子的TextView)的查找赋值的代码是如何构建出来的,这样就把注解处理器中 process()方法中BindView注解的处理流程就跑通了。随然这只是一小部分的分析,但并不妨碍我们理解其它注解背后的工作流程。

五、小结

可以看出 ButterKnife 整个过程是在项目编译阶段完成的,主要用到了 annotationProcessor 和 JavaPoet 技术,使用时通过生成的辅助类完成操作,并不是在项目运行时通过注解加反射实现的,所以并不会影响项目运行时的性能,可能仅在项目编译时有略微的影响。




参考来源:
https://www.jianshu.com/p/39fc66aa3297
https://blog.csdn.net/mackkill/article/details/65448625 
https://blog.csdn.net/qinhai1989/article/details/82593684 

 类似资料: