Butterknife is now largely unused in the project and has been replaced by ViewBinding, the familiar internals of which are custom annotations + custom annotation parsers that dynamically generate code and bind ids to our views. Today, learn about our main character, Anotation Processing, by re-writing ButterKinife.

Source address: APTDemo

Runtime annotations

Before writing the annotation handler, use runtime annotations to manipulate it. Here we will create a new library called Lib-Reflection

Then customize the annotation, we only implement the View and ID binding function, so we define here:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    int value(a);
}
Copy the code

The Type of Target indicates that this is an annotation that modifies the class and interface. This is the member variable, the view member to which the resource ID is bound.

Note Retention is RUNTIME, meaning that annotations are not only saved in the class file, but still exist after the CLASS file is loaded by the JVM.

Once annotations are defined, they can be used directly in a project;

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
      	Binding.bind(this);
       textView.setText("Ha ha ha ha."); }}Copy the code

Note that we added binding.bind (this) to help us parse the annotations, After the internal call MainActivity. TextView = (textView) MainActivity. The findViewById () to realize the bound for the view id. The Binding class is created in lib-reflect as follows:

public class Binding {
    public static void bind(Activity activity){
        // Reflection gets the annotation annotation
        for (Field field: activity.getClass().getDeclaredFields()){
            BindView bindView = field.getAnnotation(BindView.class);
            if(bindView ! =null) {try {
                    // Expand the scope
                    field.setAccessible(true);
                    field.set(activity, activity.findViewById(bindView.value()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
Copy the code

The view id is now bound to the view id, so we don’t need to manually bind it. But relying on reflection is always a performance drain, and that’s where our annotation handler comes in.

Compile-time annotations

Here, let’s create a new Java Library project called lib-Processor. Then create BindingProcessor from AbstractProcessor in this directory. This is the parser used to parse custom annotations. For this to work, however, you have to create the following directory under Resource (Google now provides an annotation to register the Processor library @autoService (processor.class) to simplify things.) :

Javax.mail. The annotation. Processing. The Processor in the text file content a line:

com.pince.lib_processor.BindingProcessor
Copy the code

Annotations (Lin-Annotations); Next, I configured annotations for the BindView annotations as compile-time annotations.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
  int value(a);
}
Copy the code

You also need to modify the build.gradle file to add:

App depends on:

implementation project(':lib')
annotationProcessor project(':lib-processor')
Copy the code

Lib – processor depends on:

implementation project(':lib-annotations')
Copy the code

Lib depends on:

// Dependencies required by the main project
api project(':lib-annotations')
Copy the code

This is done so that the compiler can use our parser to parse annotations.

The rest of the work is done in the BindingProcessor. The code to generate the corresponding bound view by reading custom annotations in the class also needs to import a library.

compile 'com. Squareup: javapoet: 1.9.0'
Copy the code

Let’s start with the last auto-generated class:

public class MainActivityBinding {
  public MainActivityBinding(MainActivity activity) {
    activity.textView = activity.findViewById(2131231093); }}Copy the code

The above content is generated by Javapoet, so let’s take a step by step analysis of how to generate our proxy class with this final result.

We need to create a constructor <? Extend Activity> is passed as an argument:

String packageStr = element.getEnclosingElement().toString();
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");
Copy the code

Then find all member variables marked with BidView annotations:

 for (Element enclosedElement: element.getEnclosedElements()){
                if (enclosedElement.getKind() == ElementKind.FIELD){
                    // Find the BindView comment
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if(bindView ! =null) {... }}}Copy the code

Then the statement code that generates findViewById:

for (Element enclosedElement: element.getEnclosedElements()){
                if (enclosedElement.getKind() == ElementKind.FIELD){
                    // Find the BindView comment
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if(bindView ! =null){
                        hasBinding = true;
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)", enclosedElement.getSimpleName(), bindView.value()); }}}Copy the code

The MainActivityBinding class is generated when the MainActivityBinding class is generated.

String packageStr = element.getEnclosingElement().toString();
ClassName className = ClassName.get(packageStr, classStr + "Binding");
TypeSpec builtClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorBuilder.build())
                    .build();
try {
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
Copy the code

Note the package name here. The package name of the generated class should be the same as the package name of the Activity to be bound to, so that the member variable modified by BindView only needs to be visible within the package. At this point we will execute./gradlew :app:compileDebugJava to automatically generate the required classes. We need to create a new Binding helper class in the lib Moodule directory:

public class Binding {
    public static void bind(Activity activity){
        try {
            Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "Binding");
            Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch(InvocationTargetException e) { e.printStackTrace(); }}}Copy the code

Here a bit of reflection is used to new out the MainActivityBinding entity, passing in the corresponding activity to perform the binding operation internally. This is where the simple ButterKnife version comes in. BindingProcessor gives the complete code below:

public class BindingProcessor extends AbstractProcessor {

    Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
      // With Filer you can create files
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
				// Get all the classes
      	//Element represents source code
   			// Element can be a class, method, variable, etc
        for (Element element: roundEnv.getRootElements()){
            String packageStr = element.getEnclosingElement().toString();
            String classStr = element.getSimpleName().toString();
            
          // Build the new class name: the original class name + Binding
            ClassName className = ClassName.get(packageStr, classStr + "Binding");
          //// To build a new class constructor
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");
            boolean hasBinding = false;
  				// There is also a getEnclosingElement enclosing an outer layer
            // Subclass element field method inner class
            for (Element enclosedElement: element.getEnclosedElements()){
              // Get only member variables
                if (enclosedElement.getKind() == ElementKind.FIELD){
                    // Find the BindView annotation
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if(bindView ! =null){
                        hasBinding = true;
                      // Add code to the constructor
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    }
                }
            }

            TypeSpec builtClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorBuilder.build())
                    .build();

            if (hasBinding){
                try {
                  // Generate a Java file
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                } catch(IOException e) { e.printStackTrace(); }}}return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        // Annotate this annotation
        returnCollections.singleton(BindView.class.getCanonicalName()); }}Copy the code

Source address: APTDemo

Note the latter

Meta-annotations are annotations used to define other annotations (we need to use meta-annotations to define our annotations when we customize them). Java.lang. annotation provides four meta-annotations: @Retention, @Target, @Inherited, and @Documented.

Yuan notes instructions
@Target Indicate where our annotations can appear. Is an ElementType enumeration
@Retention The survival time of this note
@Document Indicates that annotations can be documented by tools such as Javadoc
@Inherited Whether subclasses are allowed to inherit this annotation. Default is false

@Target

@ Target – ElementType type instructions
ElementType.TYPE Interfaces, classes, enumerations, annotations
ElementType.FIELD Constants for fields and enumerations
ElementType.METHOD methods
ElementType.PARAMETER The method parameters
ElementType.CONSTRUCTOR The constructor
ElementType.LOCAL_VARIABLE A local variable
ElementType.ANNOTATION_TYPE annotations
ElementType.PACKAGE package

@Retention

Indicates the multiple at which the annotation information needs to be saved to describe the annotation’s life cycle.

@ Retention – RetentionPolicy type instructions
RetentionPolicy.SOURCE Annotations remain in the source file only, and are discarded when a Java file is compiled into a class file
RetentionPolicy.CLASS Annotations are kept in the class file, but are discarded when the JVM loads the class file, which is the default lifecycle
RetentionPolicy.RUNTIME The solution is not only saved to the class file, but it still exists after the JVM loads the class file

@Document

@document indicates that the annotations we mark can be documented by a tool like Javadoc

@Inherited

Inherited indicates that the annotations we mark are Inherited. For example, if a parent class uses @Inherited annotations, subclasses are allowed to inherit the annotations from that parent

Annotations parsing

The Class Class uses the following methods:

/** * package name plus class name */
    public String getName(a);

    /** * Class name */
    public String getSimpleName(a);

    /** * returns the public constructor for the current class and parent class hierarchy */
    publicConstructor<? >[] getConstructors();/** * returns all the constructors of the current class (public, private, and protected) * excluding the parent class */
    publicConstructor<? >[] getDeclaredConstructors();/** * returns all public fields of the current class, including the parent */
    public Field[] getFields();

    /** * returns all declared fields of the current class, i.e. public, private, and protected, * excluding the parent class */
    public native Field[] getDeclaredFields();

    /** * returns all public methods of the current class, including the parent */
    public Method[] getMethods();

    /** * returns all methods of the current class, that is, public, private, and protected, * excluding the parent class */
    public Method[] getDeclaredMethods();

    /** * gets the method where the local or anonymous inner class was defined */
    public Method getEnclosingMethod(a);

    /** * gets the package */ for the current class
    public Package getPackage(a);

    /** * gets the package name of the current class */
    public String getPackageName$();

    /** * Gets the Type */ of the immediate superclass of the current class
    public Type getGenericSuperclass(a);

    /** * returns the interface directly implemented by the current class. Does not contain generic parameter information */
    publicClass<? >[] getInterfaces();/** * returns the current class modifier, public,private,protected */
    public int getModifiers(a);
Copy the code

Field and Method both implement AnnotatedElement interfaces, and common methods are as follows:

/** * Specifies whether the type of comment exists on this element */
    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        returngetAnnotation(annotationClass) ! =null;
    }

    /** * returns an annotation */ of the specified type that exists on this element
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);

    /** * returns all annotations that exist on the element */
    Annotation[] getAnnotations();

    /** * returns an annotation of the specified type of the element */
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
        return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
    }

    /** * returns all comments that exist directly on the element (excluding those in the parent class) */
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
        Objects.requireNonNull(annotationClass);
        // Loop over all directly-present annotations looking for a matching one
        for (Annotation annotation : getDeclaredAnnotations()) {
            if (annotationClass.equals(annotation.annotationType())) {
                // More robust to do a dynamic cast at runtime instead
                // of compile-time only.
                returnannotationClass.cast(annotation); }}return null;
    }

    /** * returns a type of comment */ that exists directly on the element's shore
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
        return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
    }

    /** * returns all comments */ that exist directly on the element
    Annotation[] getDeclaredAnnotations();
Copy the code

Comments after the processor

The Annotation Processor is a javAC tool that scans and processes annotations at compile time. You can customize annotations and register the corresponding annotation handler (the custom annotation handler needs to inherit from AbstractProcessor).

Define an annotation processor that inherits from AbstractProcessor. As follows:

public class MyProcessor extends AbstractProcessor {

  /** * Each Annotation Processor must have an empty constructor. Init () is automatically called by the annotation processing tool at compile time, passing in the ProcessingEnvironment parameter, from which many useful utility classes (Element, Filer, Messager, etc.) can be retrieved */
    @Override
    public synchronized void init(ProcessingEnvironment env){}/** * specifies which annotations are registered for the custom Annotation Processor. * Annotation specifies that it must be the full package name + class name */
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) {}/ * * * is used to specify the Java version of you, general return: SourceVersion. LatestSupported () * /
    @Override
    public Set<String> getSupportedAnnotationTypes(a) {}/** * The results of the Annotation Processor scan are stored in the roundEnvironment, where you can get the Annotation content and write your operation logic. * Note: Exceptions cannot be thrown directly from process(), otherwise the program will crash
    @Override
    public SourceVersion getSupportedSourceVersion(a) {}}Copy the code

Note that the core of the processor is the process() method, and the core of the process method is the Element. An Element can be a class, a method, or a variable. In the annotation process, the compiler scans all Java files and treats each part as an Element of a specific type. Can represent package, class, interface, method, field, and many other element types.

Element subclass explain
TypeElement Class or interface element
VariableElement Fields, enum constants, method or constructor parameters, local variables, or exception parameter elements
ExecutableElement A method, constructor, or annotation type element of a class or interface
PackageElement Package elements
TypeParameterElement Generic parameters of a class, interface, method, or constructor element

The Element class uses the following methods:

    /** * returns the type defined by this element, int,long, etc
    TypeMirror asType(a);

    /** * returns the type of this element: package, class, interface, method, field */
    ElementKind getKind(a);

    /** * returns the element's modifiers :public, private, protected */
    Set<Modifier> getModifiers(a);

    /** * returns the simple name (class name) of this element */
    Name getSimpleName(a);

    /** * returns the innermost element enclosing this element. Return the enclosing element if the declaration of this element is wrapped directly in the declaration of another element; * If this element is of top-level type, its package is returned; * If this element is a package, null is returned; * If this element is a generic parameter, null. */ is returned
    Element getEnclosingElement(a);

    /** * returns the child wrapped directly by this element */
    List<? extends Element> getEnclosedElements();

    To get inherited annotations, use getAllAnnotationMirrors */
    List<? extends AnnotationMirror> getAnnotationMirrors();

    /** * returns an annotation */ of the specified type that exists on this element
    <A extends Annotation> A getAnnotation(Class<A> var1);
Copy the code

There are also four help classes that we need to know about:

  • Elements: a utility class that works with Element
  • Types: A utility class for handling TypeMirror
  • Filer: used to create files (such as class files)
  • Messager: Used for output, similar to printf function

reference

Custom annotations and parsers implement ButterKnife

Android custom annotations

Java annotation processor

JavaPoet source probe