In general, there are two ways to use annotations in Android. One is based on reflection, that is, during the run of the program to obtain class information for reflection calls; The other is to use annotation processing, which generates a lot of code at compile time and then calls that code at run time to achieve the target functionality.

In this article, we’ll take a look at Java annotations, and then look at the two approaches in action.

1. Java annotation review

1. Basic knowledge of Java annotations

Annotations in Java are divided into standard annotations and meta-annotations. Standard annotations are predefined annotations that Java provides us with, and there are four of them: @Override, @Deprecated, @Suppresswarnnings, and @Safevarags. Meta annotations are used to provide user-defined annotations. There are five (as of Java8) : @Target, @Retention, @Documented, @inherited, and @repeatable.

First, however, let’s look at the specification of a basic annotation definition. Here we have a custom annotation called UseCase, which shows that we use several of the meta-annotations mentioned above:

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={METHOD, FIELD})
    public @interface UseCase {
        public int id(a);
        public String description(a) default "default value";
    }
Copy the code

This is the definition of a normal annotation. From the above, we can also conclude that there are several points to pay attention to when defining annotations:

  1. use@interfaceDeclare and specify the name of the annotation;
  2. The definition of an annotation is similar to that of a method in an interface, but note that the two are fundamentally different;
  3. Can be achieved bydefaultSpecifies a default value for the specified element, or if the user does not specify a value for it, the default value is used.

2. RMB notes

Ok, having looked at a basic annotation definition, let’s take a look at the meaning of the Java meta-annotation used above.

@Target

@target specifies the type of object that an annotation can decorate. Since @target itself is an annotation, you can see its definition in the source code. The argument to this annotation is an array of ElementType types, so that means our custom annotation can be applied to multiple types of objects whose types are defined by ElementType. ElementType is an enumeration with the following enumerated values:

  • TYPE: indicates the declaration of a class, interface, or enum
  • FIELD: Indicates the domain declaration, including the enum instance
  • METHOD: Indicates the METHOD declaration
  • PARAMETER: indicates the PARAMETER declaration
  • CONSTRUCTOR: CONSTRUCTOR declaration
  • LOCAL_VARIABLE: local variable declaration
  • ANNOTATION_TYPE: annotation declaration
  • PACKAGE: PACKAGE declaration
  • TYPE_PARAMETER: type parameter declaration
  • TYPE_USE: indicates the use type

So, for example, based on the above, we can wait until our custom annotation @usecase applies only to methods and fields.

@Retention

Specifies the retention policy for annotations. For example, there are annotations that you write on methods when you use them in your own code, but when you decompile them, they are not there. Some annotations still exist after being decompiled because different parameters were specified when the annotation was used.

Like @target, this annotation uses an enumeration to specify the type of the value, except that it can specify only one value, as shown in the source code. Here it uses the RetentionPolicy enumeration, and its values have the following meanings:

  • SOURCE: Annotations will be discarded by the compiler
  • CLASS: Annotations are used in CLASS files but are discarded by the JVM
  • RUNTIME: The VM keeps annotations at RUNTIME, so information about annotations can be read through reflection

When we use annotations in Android, one is used at RUNTIME, so we use RUNTIME; The other is used at compile time, so we use CLASS.

@Documented, @inherited and @repeatable

The functions of these three meta-annotations are relatively simple and easy to understand, so we can give them together here:

  • @DocumentedIndicates that this annotation will be included in javadoc;
  • @InheritedAn annotation indicating that a child class is allowed to inherit from its parent;
  • @RepeatableIs a new annotation in Java8 to indicate that a specified annotation can be repeatedly applied to a specified object.

We’ve reviewed annotations in Java so you know a little bit about them, so let’s take a look at two ways they can be used in real development.

2. Two ways to use annotations

When I started writing a database for my open source project Mark Notes, I considered using annotations to specify field information for database objects, and using this information to concatenate SQL statements to create database tables. At that time, I also wanted to use reflection to dynamically assign values to each field, but I gave up this scheme due to the poor performance of reflection. However, we can solve our problem perfectly by using annotation processing, which dynamically generates a bunch of code at compile time and calls these methods when the actual value is assigned. These two scenarios are the two ways we’ll use annotations today.

2.1 Use annotations based on reflection

To demonstrate how reflective annotations can be used, let’s write a small Java program that defines two annotations, one for a method and one for a field, and then we use these two annotations to define a class. We want to dynamically print out the annotated method and field information in our code.

Here we define two annotations, the @column annotation that applies to fields and the @important annotation that applies to methods:

    @Target(value = {ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Column {
        String name(a);
    }

    @Target(value = {ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface WrappedMethod {
        // empty
    }
Copy the code

We then define a Person class and annotate some of its methods and fields with annotations:

    private static class Person {

        @Column(name = "id")
        private int id;

        @Column(name = "first_name")
        private String firstName;

        @Column(name = "last_name")
        private String lastName;

        private int temp;

        @WrappedMethod(a)public String getInfo(a) {
            return id + ":" + firstName + "" + lastName;
        }

        public String method(a) {
            return "Nothing"; }}Copy the code

We then use the Person class to get information about the fields and methods of the class and output the annotated parts:

    public static void main(String... args) { Class<? > c = Person.class; Method[] methods = c.getDeclaredMethods();for (Method method : methods) {
            if(method.getAnnotation(WrappedMethod.class) ! =null) {
                System.out.print(method.getName() + "");
            }
        }
        System.out.println();
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            Column column = field.getAnnotation(Column.class);
            if(column ! =null) {
                System.out.print(column.name() + "-" + field.getName() + ","); }}}Copy the code

Output results:

getInfo
id-id, first_name-firstName, last_name-lastName, 
Copy the code

In the execution result of the code above, we can see that after using the annotation and reflection, we successfully print out the annotated fields. Here we need to first get the Class type of the specified Class, then use reflection to get all of its method and field information and walk through it, determining whether the method and field used the specified type of annotation by judging the result of their getAnnotation() method.

The above code solves some problems, but in the meantime, there are a few things we need to note:

  1. What if the specified method or field names are confused? For scenarios where names can be customized, we can add a parameter to the annotation specifying a name for the field or method;
  2. There’s a lot of reflection. Does that affect performance? The use of annotations is certainly not high performance, but if annotations are not used that frequently, the above method does not incur a significant performance cost, as operations such as SQL concatenation may only need to be performed once. However, the fundamental solution is to use the second way of using annotations!

2.2 Using annotations based on annotationProcessor

If you’ve used an injection framework like ButterKnife before, do you remember to add the following dependencies when referencing it in Gradle:

    annotationProcessor 'com. Jakewharton: butterknife - compiler: 8.8.1'
Copy the code

So annotationProcessor is the annotationProcessor that we’re talking about here. Essentially it will call butterknife.bind (this) at compile time; When butterknife.bind (this) is called; The annotated methods and controls are actually bound. That is, it’s still calling findViewById(), but it’s hidden from you and you don’t have to do it, that’s all.

Let’s use the annotation processing capabilities to create a simple library similar to ButterKnife. Before we do that, though, there are a few things we need to do — a few things that need to be explained. Javapoet and AbstractProcessor.

Javapoet & AbstractProcessor

Javapoet is a Java API for generating.java files. Developed by Square, you can learn the basics of how to use it on its Github home page. The advantage is that it encapsulates the concatenation of methods, class files, code, etc., so we don’t have to concatenate a piece of code in the form of strings. Compared to using strings directly, it can also generate code and import the corresponding reference directly, which can be said to be a very convenient and fast library.

AbstractProcessor is the core class used to generate class files. It is an abstract class, and usually we only need to override 4 of its methods. Here are the methods and their definitions:

  1. init: is called before code is generated, and arguments can be taken from itProcessingEnvironmentGet a lot of useful utility classes;
  2. process: A Java method used to generate code, which can be used from argumentsRoundEnvironmentGets information about the object that uses the specified annotation and wraps it into aElementType return;
  3. getSupportedAnnotationTypes: Annotations to specify which handler is applicable;
  4. getSupportedSourceVersion: specifies the version of Java you are using.

None of these methods, except Process, must be overridden. The getSupportedAnnotationTypes and getSupportedSourceVersion can use @ SupportedAnnotationTypes and @ SupportedSourceVersion But it’s not recommended. Since the previous annotation takes a string as an argument, it can be troublesome if you use obfuscation, and the later annotations can only use enumerations, which is less flexible.

Another important point we need to note is that we need to register it to use it after inheriting from AbstractProcessor and implementing our own processor. One way of doing this is under with Java directory to create a resources folder, and create the meta-inf/service folder, and then create a named javax.mail. The annotation. Processing. Processor file, And write in it the full path of our processor. Another way to do this is to use Google’s @AutoService annotation. All you need to do is add a line of @AutoService(processor.class) to your Processor. If, of course, you need to introduce dependencies into your project:

    compile 'com. Google. Auto. Services: auto - service: 1.0 rc2'
Copy the code

This will generate the above file in the same directory, but we don’t need to do it. You can find the generated files by looking at the files produced by Buidl.

The final results of MyKnife

Before customizing, let’s take a look at the final execution result of the program. Perhaps it will help to understand how the process works. The end result of our program execution is that, at compile time, a class is generated under the same level of package as the class that uses our tool. As shown in the figure below:

MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity: MyKnifeActivity The class Injector, which is defined as follows:

    public class MyKnifeActivity?Injector implements Injector<MyKnifeActivity> {
      @Override
      public void inject(final MyKnifeActivity host, Object source, Finder finder) {
        host.textView=(TextView)finder.findView(source, 2131230952);
        View.OnClickListener listener;
        listener = new View.OnClickListener() {
          @Override
          public void onClick(View view) { host.OnClick(); }}; finder.findView(source,2131230762).setOnClickListener(listener); }}Copy the code

Because the class we’re applying MyKnife to is MyKnifeActivity, we’re generating MyKnifeActivity, right? Class of Injector. From the above code, you can see that it actually calls Finder methods to assign a value to our control textView, and then uses the control’s setOnClickListener() method to assign a value to the click event. The Finder here is an object that we encapsulate to get the control from the specified source, essentially calling the findViewById() method of the specified source.

Then, similar to ButterKnife, we need to call the bind() method on the Activity’s onCreate() when using our tool. Here’s what this method does:

    public static void bind(Object host, Object source, Finder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAPPER.get(className);
            if (injector == null) { Class<? > finderClass = Class.forName(className +"? Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAPPER.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch(InstantiationException e) { e.printStackTrace(); }}Copy the code

As you can see from the code above, the call to bind() will attempt to retrieve the specified class name from FINDER_MAPPER. For Injector. So, if the class we applied bind() to is MyKnifeActivity, then the class we get here is going to be MyKnifeActivity, right? Injector. Then, we perform our injection operation above when we call the inject method to complete the assignment to the control and click event. FINDER_MAPPER is a hash table that caches the specified Injector. So, as you can see from the above, reflection is used for value binding, so you need to deal with obfuscation when applying the framework.

OK, having seen the final result of the program, let’s look at how to generate the above class file.

Definition of APIS and annotations

First, we need to define annotations to provide the user with bindings to the event and control,

    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.CLASS)
    public @interface BindView {
        int id(a);
    }

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.CLASS)
    public @interface OnClick {
        int[] ids();
    }
Copy the code

In the code above, you can see that we use elementType. FIELD and elementType. METHOD to specify that they apply to fields and methods, respectively, and then use retentionPolicy.class to indicate that they are not retained until the program runs.

Then we need to define MyKnife, which provides a bind() method defined as follows:

    public static void bind(Object host, Object source, Finder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAPPER.get(className);
            if (injector == null) { Class<? > finderClass = Class.forName(className +"? Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAPPER.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch(InstantiationException e) { e.printStackTrace(); }}Copy the code

Host is the class that calls the bound method, such as Activity, etc. Source is the data source used to get the value of the binding. It is generally understood that the control is assigned from source to the field in host. The last parameter Finder is an interface that encapsulates the method that gets data. There are two default implementations, an ActivityFinder and a ViewFinder, for finding controls from an Activity and View, respectively.

We’ve already seen what the bind() method does, which uses reflection to get an Injector based on the class name and then calls its inject() method to do the injection. The Injector here is an interface, we won’t write code to implement it, we’ll have the compiler generate its implementation class directly at compile time.

The code generation process

When we introduced Javapoet and AbstractProcessor, we mentioned Element, which encapsulates information about the object (method, field, class, etc.) to which an annotation is applied. We can take this information from the Element and wrap it into an object that we can call. Hence the BindViewField and OnClickMethod classes. They describe information about objects that use the @BindView annotation and the @onClick annotation, respectively. In addition, there is an AnnotatedClass that describes information about the entire class that uses annotations and defines a List

and a List

to store information about the fields and methods in that class to which annotations are applied, respectively.

Several fields related to generating files and retrieving object information for annotations are retrieved from AbstractProcessor. As shown in the following code, we can get Elements, Filer, and Messager from the ProcessingEnvironment of the Init () method of AbstractProcessor. Elements is similar to a utility class used to retrieve information about an annotation object from Element. Filer is used to support the creation of new files through the annotation handler; Messager provides a way for comment handlers to report error messages, warnings, and other notifications.

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elements = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
    }
Copy the code

Then in the RoundEnvironment parameter in the Process () method of AbstractProcessor, we can get the Element information corresponding to the specified annotation. The code looks like this:

    private Map<String, AnnotatedClass> map = new HashMap<>();

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        map.clear();
        try {
            // One for each of the two annotations we define
            processBindView(roundEnvironment);
            processOnClick(roundEnvironment);
        } catch (IllegalArgumentException e) {
            return true;
        }

        try {
            // Generate class files for cached classes that use annotations
            for(AnnotatedClass annotatedClass : map.values()) { annotatedClass.generateFinder().writeTo(filer); }}catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

    // Get the @bindView annotation from RoundEnvironment
    private void processBindView(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField field = newBindViewField(element); annotatedClass.addField(field); }}// Get the @onclick annotation from RoundEnvironment
    private void processOnClick(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            OnClickMethod method = newOnClickMethod(element); annotatedClass.addMethod(method); }}// Get the information about the class that uses the annotation. First try to get it from the cache. If there is no one in the cache, instantiate one and put it in the cache
    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = encloseElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = map.get(fullClassName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(encloseElement, elements);
            map.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }
Copy the code

The logic of the above code is that when the process() method is called, the two annotations are processed separately, depending on the RoundEnvironment passed in. The information for both annotations is parsed into List

and List

, and the information for the entire class that uses the annotation is placed in the AnnotatedClass. To improve the efficiency of the program, a cache is used to store class information. . Finally, we call the annotatedClass generateFinder () to obtain a JavaFile, and calls it the writeTo (filer) method to generate class files.

The above code focuses on parsing the information about the class that is using annotations, but to generate a class file based on the class information, we also need to look at the generateFinder() method for AnnotatedClass, which looks like this. Here we use the previously mentioned Javapoet to help us generate class files:

    public JavaFile generateFinder(a) {
        // Use this to define the inject method signature
        MethodSpec.Builder builder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(typeElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtils.FINDER, "finder");
        // Bind the @bindView annotation to the inject method
        for (BindViewField field : bindViewFields) {
            builder.addStatement("host.$N=($T)finder.findView(source, $L)",
                    field.getFieldName(),
                    ClassName.get(field.getFieldType()),
                    field.getViewId());
        }
        // Bind the @onclick annotation in the inject method
        if (onClickMethods.size() > 0) {
            builder.addStatement("$T listener", TypeUtils.ONCLICK_LISTENER);
        }
        for (OnClickMethod method : onClickMethods) {
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(TypeUtils.ONCLICK_LISTENER)
                    .addMethod(MethodSpec.methodBuilder("onClick")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(TypeName.VOID)
                            .addParameter(TypeUtils.ANDROID_VIEW, "view")
                            .addStatement("host.$N()", method.getMethodName())
                            .build())
                    .build();
            builder.addStatement("listener = $L", listener);
            for (int id : method.getIds()) {
                builder.addStatement("finder.findView(source, $L).setOnClickListener(listener)", id); }}// This is used to get information about the package in which the class is to be generated
        String packageName = getPackageName(typeElement);
        String className = getClassName(typeElement, packageName);
        ClassName bindClassName = ClassName.get(packageName, className);

        // For the final assembly of the class we want to output
        TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "? Injector")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtils.INJECTOR, TypeName.get(typeElement.asType())))
                .addMethod(builder.build())
                .build();
        return JavaFile.builder(packageName, finderClass).build();
    }
Copy the code

This is the method we used to eventually generate the class file, using Javapoet. If you’re not familiar with it, check out Github to see how it works.

This completes the definition of the method.

Using MyKnife

With our defined MyKnife, we just need to introduce our package in Gradle:

    implementation project(':knife-api')
    implementation project(':knife-annotation')
    annotationProcessor project(':knife-compiler')
Copy the code

Maybe you’ve seen some places where you want to use android-apt to introduce annotation processors, but the annotationProcessor here does exactly the same thing. AnnotationProcessor is recommended here because it is simpler, requires no additional configuration, and is the official recommended way to use it.

Then, we just need to use them in our code:

public class MyKnifeActivity extends CommonActivity<ActivityMyKnifeBinding> {

    @BindView(id = R.id.tv)
    public TextView textView;

    @OnClick(ids = {R.id.btn})
    public void OnClick(a) {
        ToastUtils.makeToast("OnClick");
    }

    @Override
    protected int getLayoutResId(a) {
        return R.layout.activity_my_knife;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
        MyKnife.bind(this);
        textView.setText("This is MyKnife demo!"); }}Copy the code

Here are a few things to note:

  1. Methods and fields that use annotations need to be at leastprotectedBecause we are using a direct reference and the generated file is the same as the above class package, at least package-level access should be guaranteed;
  2. This is because the id in the R file is final only when the Module is used as an application. When it’s a library, it’s not final.

conclusion

Here we summarize the steps required to use annotations in the second way:

  1. First, we need to think about how to define annotations according to our needs.
  2. Then we need to implement AbstractProcessor, override the methods, register, and complete the generation of the class file in the Process method.

2.3 Replace enumerations with annotations

The third common use of annotations is to replace enumerations. Because enumerations have an extra memory footprint compared to normal strings or integers, they need to be optimized for memory-intensive projects such as Android. Of course, we can use string or integer constants instead of enumerations, but this way the arguments can take any string or integer value. If we want to be able to limit the range of parameters passed in like an enumeration, we need to use an enumeration!

For example, we need to limit the camera’s flash parameters by specifying each parameter as an integer variable. We then use a method to take arguments of an integer type and an annotation to require that the specified integer type be within the scope of the integer type we declared above. We can define it this way,

First, we define a Camera class to store the flash enumeration values and annotations,

public final class Camera {

    public static final int FLASH_AUTO                      = 0;
    public static final int FLASH_ON                        = 1;
    public static final int FLASH_OFF                       = 2;
    public static final int FLASH_TORCH                     = 3;
    public static final int FLASH_RED_EYE                   = 4;

    @IntDef({FLASH_ON, FLASH_OFF, FLASH_AUTO, FLASH_TORCH, FLASH_RED_EYE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface FlashMode {
    }
}
Copy the code

As shown above, we define the enumeration value and its annotations. Then, we can use that annotation like this,

public final class Configuration implements Parcelable {

    @Camera.FlashMode
    private int flashMode = Camera.FLASH_AUTO;

    public void setFlashMode(@Camera.FlashMode int flashMode) {
        this.flashMode = flashMode; }}Copy the code

This way, the IDE will automatically prompt when we pass in parameters that are not in the range specified by our custom enumeration @intDef.

3, summarize

These are two of the more common uses of annotations. The first is through reflection, because the efficiency of reflection itself is relatively low, so it is more suitable for the scene with less emission; The second approach, which is achieved through code generated by the compiler during compilation, is still likely to use reflection, but is much more efficient because it does not require running through every method and field of the class.

The above.

Get the source code: Android-References