background

In the previous article, explained annotations and compile-time annotations and other related contents, in order to more comprehensive and really understand the Android compile-time annotations in the actual use of the project, this paper adopts the implementation of the mainstream framework Butterknife injection View to fully understand compile-time annotations.

Annotated columns – Blogs

The effect

Let’s start with a picture to calm things down

The view binding of butterKnife will be implemented with an image

use

The @bindView annotation is modeled after ButterKnife, and the current MainActivity is bound by the WzgJector. Bind method. The overall use of butterknife is exactly the same as that of Butterknife

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_msg)
    TextView tvMsg;
    @BindView(R.id.tv_other)
    TextView tvOther;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WzgJector.bind(this);
        if(tvMsg! =null){
            tvMsg.setText("I've successfully initialized.");
        }

        if(tvOther! =null){
            tvOther.setText("I'm just checking in."); }}}Copy the code

implementation

The idea of implementation and Android compile-time annotations – the initial understanding of the implementation principle is roughly the same, so here does not repeat the repeated steps, focus on improving the technical points, so need to understand the basic compile-time annotations under the premise of continuing to learn

AndroidCompile-time annotations – a primer

Custom annotation

This uses Java and Android annotations. The BindView annotation starts with @target as FIELD, and the annotation BindView has an initial int as the view-ID when used

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

For those who are not familiar with Java and Android annotations, please refer to the following two articles

Java- Full explanation of annotations

Android- Notes in detail

ElementBreak down

Element

With annotations, it is necessary to have a corresponding annotation processor to handle annotations, but when processing annotations, you need to fully understand the annotation processor’s process method and the core of the compiled code, and the core of the process method is the Element object, so before explaining the annotation processor, Need to have a comprehensive understanding of Element, in order to get twice the result with half the effort.

Due to the complexity of Element’s knowledge content, the focus here is on the core content, which is sufficient for basic use

Source:

public interface Element extends AnnotatedConstruct {
    TypeMirror asType(a);

    ElementKind getKind(a);

    Set<Modifier> getModifiers(a);

    Name getSimpleName(a);

    Element getEnclosingElement(a);

    List<? extends Element> getEnclosedElements();

    boolean equals(Object var1);

    int hashCode(a);

    List<? extends AnnotationMirror> getAnnotationMirrors();

    <A extends Annotation> A getAnnotation(Class<A> var1);

    <R, P> R accept(ElementVisitor<R, P> var1, P var2);
}Copy the code

Element is a defined interface that defines the interface exposed by external calls

methods explain
asType Returns the type defined for this element
getKind Returns the type of this element: package, class, interface, method, field… , the enumerated values are as follows
getModifiers Returns the modifier for this element, as enumerated below
getSimpleName Returns a simple name for this element, such as the activity name
getEnclosingElement Returns the innermost element that encapsulates this element, or if the declaration of this element is wrapped directly in another element’s declaration; If the element is a top-level type, return its package; if the element is a package, return NULL; If the element is a generic parameter, null is returned.
getAnnotation Returns an annotation of this element for the specified type, if one exists, or NULL otherwise. Annotations can be inherited or exist directly on the element

getKindmethods

The getKind() method gets the specific type and returns an enumerated TypeKind

Source:

public enum TypeKind {  
    /** The primitive type {@code boolean}. */  
    BOOLEAN,  
    /** The primitive type {@code byte}. */  
    BYTE,  
    /** The primitive type {@code short}. */  
    SHORT,  
    /** The primitive type {@code int}. */  
    INT,  
    /** The primitive type {@code long}. */  
    LONG,  
    /** The primitive type {@code char}. */  
    CHAR,  
    /** The primitive type {@code float}. */  
    FLOAT,  
    /** The primitive type {@code double}. */  
    DOUBLE,  
    /** The pseudo-type corresponding to the keyword {@code void}. */  
    VOID,  
    /** A pseudo-type used where no actual type is appropriate. */  
    NONE,  
    /** The null type. */  
    NULL,  
    /** An array type. */  
    ARRAY,  
    /** A class or interface type. */  
    DECLARED,  
    /** A class or interface type that could not be resolved. */  
    ERROR,  
    /** A type variable. */  
    TYPEVAR,  
    /** A wildcard type argument. */  
    WILDCARD,  
    /** A pseudo-type corresponding to a package element. */  
    PACKAGE,  
    /** A method, constructor, or initializer. */  
    EXECUTABLE,  
    /** An implementation-reserved type. This is not the type you are looking for. */  
    OTHER,  
    /** A union type. */  
    UNION,  
    /** An intersection type. */  
    INTERSECTION;  
}Copy the code

ElementA subclass

Element has five direct subinterfaces, each representing a particular type of Element

Tables Are
TypeElement A class or interface program element
VariableElement A field, enum constant, method or constructor parameter, local variable, or exception parameter
ExecutableElement A method, constructor, or initializer (static or instance) of a class or interface, including annotation type elements
PackageElement A package element
TypeParameterElement Generic parameters for a generic class, interface, method, or constructor element

Each of the five subclasses has its own purpose and a variety of separate methods that can be used to force an Element object to be converted to any one of them, but only if the conditional conversion is met, otherwise an exception will be thrown.

The two most central children are TypeElement and VariableElement

TypeElementBreak down

TypeElement defines a class or interface program element that corresponds to the class object in which the current annotation is located, even if this example uses MainActivity in the code

The source code is as follows:

public interface TypeElement extends Element.Parameterizable.QualifiedNameable {
    List<? extends Element> getEnclosedElements();

    NestingKind getNestingKind(a);

    Name getQualifiedName(a);

    Name getSimpleName(a);

    TypeMirror getSuperclass(a);

    List<? extends TypeMirror> getInterfaces();

    List<? extends TypeParameterElement> getTypeParameters();

    Element getEnclosingElement(a);
}Copy the code

The main methods are explained here

methods explain
getNestingKind Returns the nested type of an element of this type
getQualifiedName Returns the fully qualified name of an element of this type. More precisely, return the specification name. For local and anonymous classes without a canonical name, an empty name is returned.
getSuperclass Returns the immediate superclass of an element of this type. If this type element represents an interface or class java.lang.Object, a NoType of type NONE is returned
getInterfaces Returns the interface type implemented directly by this class or extended directly from this interface
getTypeParameters Returns the formal type parameters of elements of this type in declarative order

VariableElementBreak down

Source:

public interface VariableElement extends Element {
    Object getConstantValue(a);

    Name getSimpleName(a);

    Element getEnclosingElement(a);
}Copy the code

Here the VariableElement has the following two methods in addition to its own Element method

methods explain
getConstantValue The initialized value of the variable
getEnclosingElement Get related class information

Annotation processor

The annotation handler requires two steps:

  • 1. Collect previous information

  • 2. Generate processing classes

Now that you have a thorough understanding of Element, the annotation processor is easy to learn, starting with a simple version of BindView processing

Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        // Collect information
        for (Element element : elements) {
            /* Check the type */
            if(! (elementinstanceof VariableElement)) {
                return false;
            }
            VariableElement variableElement = (VariableElement) element;

            /* Get class information */
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            /* Absolute path of class */
            String qualifiedName = typeElement.getQualifiedName().toString();
            / * * /
            String clsName = typeElement.getSimpleName().toString();
            /* Get the package name */
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();

            BindView annotation = variableElement.getAnnotation(BindView.class);
            int id = annotation.value();

            /* Parameter name */
            String name = variableElement.getSimpleName().toString();
            /* Parameter object class */
            String type = variableElement.asType().toString();

            ClassName InterfaceName = ClassName.bestGuess("com.example.annotation.api.ViewInjector");
            ClassName host = ClassName.bestGuess(qualifiedName);

            MethodSpec main = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(host, "host")
                    .addParameter(Object.class, "object")
                    .addCode(""
                            + " if(object instanceof android.app.Activity){\n"
                            + " host." + name + "=" + type + ")(((android.app.Activity)object).findViewById(" + id + ")); \n"
                            + " }\n"
                            + "else{\n"
                            + " host." + name + "=" + type + ")(((android.view.View)object).findViewById(" + id + ")); \n"
                            + "}\n")
                    .build();

            TypeSpec helloWorld = TypeSpec.classBuilder(clsName + "ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(main)
                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
                    .build();

            try {
                / / generated com. Example. The HelloWorld. Java
                JavaFile javaFile = JavaFile.builder(packageName, helloWorld)
                        .addFileComment(" This codes are generated automatically. Do not modify!")
                        .build();
                // Generate the file
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }Copy the code

If it is a VariableElement, it is possible to obtain the package name (which must be the same as the app package name, otherwise it cannot obtain the Android class), and the @bindView annotation parameter data. Finally, all the required data is automatically compiled by Javapoet and Filer to create a Java file

The resulting generated class:

package com.wzgiceman.viewinjector;

import com.example.ViewInjector;
import java.lang.Object;
import java.lang.Override;

public class MainActivityViewInjector implements ViewInjector<MainActivity> {
  @Override
  public void inject(MainActivity host, Object object) {
     if(object instanceof android.app.Activity){
     host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
     }
    else{
     host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945)); }}}Copy the code

The above simple processor, just pure judgment an annotation, in simplified on the processing of information collection, led to the current processor can only deal with the current in the same class at the same time not an annotation here only initializes the tvMsg object, tvOther is not initialized, of course it is not in conformity with the actual demand, the following to optimize collection and treatment scheme.

To optimize the

Optimization plan is actually a step more information recording work

Create an information class object

Start by creating a class information object that contains the following attributes, where varMap records all annotation-related information in the current class

public class VariMsg {
    / * package name * /
    private String pk;
    / * * /
    private String clsName;
    /* Annotate object */
    private HashMap<Integer,VariableElement> varMap;
    }Copy the code

BindViewProcessors

1. Start a map to record the VariMsg object. Since the process method may be called several times, clear it each time

 Map<String, VariMsg> veMap = new HashMap<>();Copy the code

2. Record information Use veMap to record all related information and determine whether the data is duplicated each time.

    for (Element element : elements) {
            /* Check the type */
            if(! (elementinstanceof VariableElement)) {
                return false;
            }
            VariableElement variableElement = (VariableElement) element;

            /* Get class information */
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            /* Absolute path of class */
            String qualifiedName = typeElement.getQualifiedName().toString();
            / * * /
            String clsName = typeElement.getSimpleName().toString();
            /* Get the package name */
            String packageName = processingEnv.getElementUtils().getPackageOf(typeElement).getQualifiedName().toString();

            BindView annotation = variableElement.getAnnotation(BindView.class);
            int id = annotation.value();

            VariMsg variMsg = veMap.get(qualifiedName);
            if (variMsg == null) {
                variMsg = new VariMsg(packageName, clsName);
                variMsg.getVarMap().put(id, variableElement);
                veMap.put(qualifiedName, variMsg);
            } else{ variMsg.getVarMap().put(id, variableElement); }}Copy the code

3. Generate Java class files through Javapoet. This is mainly the use of Javapoet

javapoet-GitHub


  System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        for (String key : veMap.keySet()) {
            ClassName InterfaceName = ClassName.bestGuess("com.example.ViewInjector");
            ClassName host = ClassName.bestGuess(key);
            VariMsg variMsg = veMap.get(key);

            StringBuilder builder = new StringBuilder();
            builder.append(" if(object instanceof android.app.Activity){\n");
            builder.append(code(variMsg.getVarMap(), "android.app.Activity"));
            builder.append("}\n");
            builder.append("else{\n");
            builder.append(code(variMsg.getVarMap(), "android.view.View"));
            builder.append("}\n");

            MethodSpec main = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(void.class)
                    .addAnnotation(Override.class)
                    .addParameter(host, "host")
                    .addParameter(Object.class, "object")
                    .addCode(builder.toString())
                    .build();

            TypeSpec helloWorld = TypeSpec.classBuilder(variMsg.getClsName() + "ViewInjector")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(main)
                    .addSuperinterface(ParameterizedTypeName.get(InterfaceName, host))
                    .build();

            try {
                JavaFile javaFile = JavaFile.builder(variMsg.getPk(), helloWorld)
                        .addFileComment(" This codes are generated automatically. Do not modify!")
                        .build();
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
                System.out.println("e--->" + e.getMessage());
            }
        }
        System.out.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
        return true;
    }


    /** * Generates the code method body ** from the annotation object@param map
     * @param pk
     * @return* /
    private String code(Map<Integer, VariableElement> map, String pk) {
        StringBuilder builder = new StringBuilder();
        for (Integer id : map.keySet()) {
            VariableElement variableElement = map.get(id);
            String name = variableElement.getSimpleName().toString();
            String type = variableElement.asType().toString();

            builder.append("host." + name + "=" + type + ") (((" + pk + ")object).findViewById(" + id + ")); \n");
        }
        return builder.toString();
    }Copy the code

At this point, the final version of the annotation processor has been generated. Take a look at the resulting code class

package com.wzgiceman.viewinjector;

import com.example.ViewInjector;
import java.lang.Object;
import java.lang.Override;

public class MainActivityViewInjector implements ViewInjector<MainActivity> {
  @Override
  public void inject(MainActivity host, Object object) {
     if(object instanceof android.app.Activity){
    host.tvMsg = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
    host.tvOther = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492946));
    }
    else{
    host.tvMsg = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
    host.tvOther = (android.widget.TextView)(((android.view.View)object).findViewById(2131492946)); }}}Copy the code

api

The API module mainly defines the external use method, in this case the WzgJector. Bind (this) method, the same as butterknife.bind (this) in ButterKnife;

public class MainActivity extends AppCompatActivity {

   xxxxxx
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WzgJector.bind(this);
        xxxx
    }
}Copy the code

implementation

Create an App Module

Defining interface classesViewInjector

Exposed external methods, used primarily in compiling automatically generated annotation handling classes

/** * Created by WZG on 2017/1/11. */
public interface ViewInjector<M> {
    void inject(M m, Object object);
}Copy the code

Actual processing classWzgJector

Provide two methods, one is the activity binding, one is the View or fragment binding, binding after, through reflection to get relevant annotations compilation processing class timely ViewInjector subclass Object, call Inject (M M, Object Object) method to complete the initial process.

public class WzgJector {
    public static void bind(Object activity) {
        bind(activity, activity);
    }

    public static void bind(Object host, Objectroot) { Class<? > clazz = host.getClass();String proxyClassFullName = clazz.getName() + "ViewInjector";
        try{ Class<? > proxyClazz = Class.forName(proxyClassFullName); ViewInjector viewInjector = (ViewInjector) proxyClazz.newInstance(); viewInjector.inject(host, root); }catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch(IllegalAccessException e) { e.printStackTrace(); }}}Copy the code

As you’ll see here, compile-time annotations are not completely reflective, but they improve compilation performance by avoiding multiple reflective uses of critical repetitive code.

The results of

This is basically how butterknife’s @BindView implementation works so far, but I won’t go into @onclick for time’s sake. In fact, the principle is the same, you can install the @onclick processing.

column

Annotations – Compile runtime annotations

The source code

Download the source code

advice

If you have any questions or suggestions, please join the QQ group and tell me!