Android Annotations Quick Start and Practical Analysis

Android Annotations Quick Start and Practical Analysis

First of all, what is an annotation? @Override is an annotation, and its function is:

  • Check whether the method in the parent class is correctly rewritten.
  • Mark the code, this is an overridden method.

1. It is reflected in: checking whether the method name and parameter type overridden by the subclass are correct; checking whether the method private/final/static cannot be overridden. In fact, @Override has no actual impact on the application, which can be seen from its source code.

2. Mainly shows the readability of the code.

As a well-known annotation in Android development, Override is just one manifestation of annotation. More often, annotations have the following functions:

  • Reduce the coupling of projects.
  • Automatically complete some regular codes.
  • Automatically generate Java code to reduce the workload of developers.

1. Quick reading of annotation basics

1. Meta-annotation

Meta-annotation is a basic annotation provided by Java, which is responsible for annotating other annotations. As shown in the figure above, Override is modified by @Target and @Retention, which are used to explain other annotations and are located in the sdk/sources/android-25/java/lang/annotation path.

The meta annotations are:

  • @Retention: Annotation retention life cycle
  • @Target: The scope of the annotation object.
  • @Inherited: @Inherited indicates whether the modified annotation can be inherited on the class it acts on.
  • @Documented: As the name suggests, it is documented by javadoc tool, which is generally not of concern.

@Retention

The Retention statement indicates the life cycle of the annotation, and the corresponding RetentionPolicy enumeration indicates when the annotation takes effect:

  • SOURCE: Only valid in source code and discarded during compilation, such as @Override above.
  • CLASS: Takes effect when compiling class files.
  • RUNTIME: takes effect only during runtime.

As shown in Figure X1 below, the Nullable annotation in com.android.support:support-annotations will determine at compile time whether the annotated parameter will be empty, which will be analyzed later.

@Target

Target indicates the applicable scope of the annotation, corresponding to the ElementType enumeration, which clarifies the effective scope of the annotation.

  • TYPE: class, interface, enumeration, annotation type.
  • FIELD: class members (constructors, methods, member variables).
  • METHOD: method.
  • PARAMETER: parameter.
  • CONSTRUCTOR: Constructor.
  • LOCAL_VARIABLE: local variable.
  • ANNOTATION_TYPE: Annotation.
  • PACKAGE: Package declaration.
  • TYPE_PARAMETER: Type parameter.
  • TYPE_USE: Type use declaration.

As shown in Figure X1 above, @Nullable can be used to annotate methods, parameters, class members, annotations, and package declarations. Common examples are as follows:

  1. /**
  2. * Nullable indicates
  3. * The parameter target and return value Data of the bind method can be null  
  4. */
  5. @Nullable
  6. public   static Data bind(@Nullable Context target) {
  7. //do someThing and   return  
  8. return bindXXX(target);
  9. }

@Inherited

The class to which the annotation applies cannot inherit the annotation of the parent class by default when it is inherited, unless the annotation declares @Inherited. At the same time, the annotation declared by Inherited is only valid for the class, not for the method/attribute.

As shown in the code below, the annotation class @AInherited declares Inherited, while the annotation BNotInherited does not, so they are modified:

  • The Child class inherits @AInherited from the parent class Parent, but does not inherit @BNotInherited;
  • The overridden method testOverride() does not inherit any annotations of Parent;
  • Because testNotOverride() has not been overridden, the annotation is still effective.

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Inherited
  3. public @interface AInherited {
  4. String value();
  5. }
  6. @Retention(RetentionPolicy.RUNTIME)
  7. public @interface BNotInherited {
  8. String value();
  9. }
  10.  
  11. @AInherited( "Inherited" )
  12. @BNotInherited( "NotInherited" )
  13. public class Parent {
  14.  
  15. @AInherited( "Inherited" )
  16. @BNotInherited( "NotInherited" )
  17. public void testOverride(){
  18.  
  19. }
  20. @AInherited( "Inherited" )
  21. @BNotInherited( "NotInherited" )
  22. public void testNotOverride(){
  23. }
  24. }
  25.  
  26. /**
  27. * Child inherits the AInherited annotation of Parent
  28. * BNotInherited cannot be inherited because there is no @Inherited declaration
  29. */
  30. public class Child extends Parent {
  31.  
  32. /**
  33. * The overridden testOverride does not inherit any annotations
  34. * Because Inherited does not work on methods
  35. */
  36. @Override
  37. public void testOverride() {
  38. }
  39.  
  40. /**
  41. * testNotOverride is not overridden
  42. * So the annotations AInherited and BNotInherited are still valid.
  43. */
  44. }

2. Custom annotations

2.1 Runtime Annotations

After understanding meta-annotations, let's see how to implement and use custom annotations. Here we briefly introduce the runtime annotation RUNTIME, and the compile-time annotation CLASS will be analyzed later.

First, create an annotation conforming to: public @interface annotation name {method parameter}, such as the following @getViewTo annotation:

  1. @Target({ElementType.FIELD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface getViewTo {
  4. int value() default -1;
  5. }

Then, as shown below, we describe the annotations in the Activity's member variables mTv and mBtn. When the App is running, the controls obtained by findViewbyId are injected into mTv and mBtn through reflection.

Does it look familiar? It has a bit of ButterKnife flavor? Of course, ButterKnife is much more advanced than this. After all, more reflection affects efficiency. However, we understand that we can inject and create objects through annotations, which can save code to a certain extent.

  1. public class MainActivity extends AppCompatActivity {
  2.  
  3. @getViewTo(R.id.textview)
  4. private TextView mTv;
  5.  
  6. @getViewTo(R.id.button)
  7. private Button mBtn;
  8.  
  9. @Override
  10. protected void onCreate(Bundle savedInstanceState) {
  11. super.onCreate(savedInstanceState);
  12. setContentView(R.layout.activity_main);
  13.  
  14. //Generate View through annotation ;
  15. getAllAnnotationView();
  16. }
  17.  
  18. /**
  19. * Parse the annotation and get the control
  20. */
  21. private void getAllAnnotationView() {
  22. //Get member variables
  23. Field[] fields = this.getClass().getDeclaredFields();
  24.  
  25. for (Field field : fields) {
  26. try {
  27. //Judgement annotation
  28. if (field.getAnnotations() != null ) {
  29. // Determine the annotation type
  30. if (field.isAnnotationPresent(GetViewTo.class)) {
  31. //Allow modification of reflection properties
  32. field.setAccessible( true );
  33. GetViewTo getViewTo = field.getAnnotation(GetViewTo.class);
  34. //findViewById finds the annotated id and injects it into the member variable
  35. field.set (this, findViewById(getViewTo.value()));
  36. }
  37. }
  38. } catch (Exception e) {
  39. }
  40. }
  41. }
  42.  
  43. }

2.2 Compile-time annotations

Runtime annotation RUNTIME As shown in 2.1 above, most of the time, reflection is used at runtime to achieve the desired effect, which greatly affects efficiency. If each View injection of BufferKnife is impossible, how can it be achieved? In fact, ButterKnife uses the compile-time annotation CLASS, as shown in Figure X2.2 below, which is ButterKnife's @BindView annotation. It is a compile-time annotation that generates corresponding Java code at compile time to achieve injection.

When it comes to compile-time annotations, we have to talk about the annotation processor AbstractProcessor. If you have noticed, third-party annotation-related class libraries, such as bufferKnike and ARouter, generally have a Module named Compiler, as shown in Figure X2.3. These are generally annotation processors used to process corresponding annotations at compile time.

Annotation Processor is a tool of javac, which is used to scan and process annotations at compile time. You can customize annotations and register corresponding annotation processors to process your annotation logic.

As shown below, implement a custom annotation processor, rewrite at least four methods, and register your custom Processor. For details, refer to the CustomProcessor code below.

  • @AutoService(Processor.class), the automatic registration annotation provided by Google, generates the format file required to register the Processor for you (com.google.auto related packages).
  • init(ProcessingEnvironment env), initialize the processor, generally get the tool class we need here.
  • getSupportedAnnotationTypes(), specifies the annotation for which the annotation processor is registered, and returns the specified supported annotation class set.
  • getSupportedSourceVersion() , specify the java version.
  • process(), the processor actually processes the logic entry.

  1. @AutoService(Processor.class)
  2. public class CustomProcessor extends AbstractProcessor {
  3.  
  4. /**
  5. * Initialization of annotation processor
  6. * Generally, we get the tool classes we need here
  7. * @param processingEnvironment provides tool classes Elements, Types and Filer
  8. */
  9. @Override
  10. public synchronized void init(ProcessingEnvironment env){
  11. super.init(env);
  12. //Element represents the elements of the program, such as packages, classes, and methods.
  13. mElementUtils = env.getElementUtils();
  14.  
  15. //Tool class for processing TypeMirror, used to obtain class information
  16. mTypeUtils = env.getTypeUtils();
  17.  
  18. //Filer can create files
  19. mFiler = env.getFiler();
  20.  
  21. //Error handling tool
  22. mMessages = env.getMessager();
  23. }
  24.  
  25. /**
  26. * The processor actually processes the logic entry
  27. * @param set  
  28. * @param roundEnvironment Collection of all annotations
  29. * @return   
  30. */
  31. @Override
  32. public boolean process( Set <? extends TypeElement> annoations,
  33. RoundEnvironment env) {
  34. //do someThing
  35. }
  36.  
  37. //Specify which annotation the annotation processor is registered for, and return the specified supported annotation class set.
  38. @Override
  39. public   Set <String> getSupportedAnnotationTypes() {
  40. Set <String> sets = new LinkedHashSet<String>();
  41.  
  42. //For most classes, there is no difference between getName and getCanonicalNam.
  43. //But it is different for array or inner classes.
  44. //getName returns a form such as [[Ljava.lang.String,
  45. //getCanonicalName returns a form similar to our declaration.
  46. sets(BindView.class.getCanonicalName());
  47.  
  48. return sets;
  49. }
  50.  
  51. //Specify the Java version, generally return the *** version
  52. @Override
  53. public SourceVersion getSupportedSourceVersion() {
  54. return SourceVersion.latestSupported();
  55. }
  56.  
  57. }

First, let's sort out the general processor processing logic:

  1. Traverse and get the list of elements that need to be parsed in the source code.
  2. Determines whether the element is visible and meets the requirements.
  3. Organize the data structure to get the output class parameters.
  4. Input to generate java file.
  5. Error handling.

Then, let's understand a concept: Element, because it is the basis for us to obtain annotations.

During the Processor process, all Java source codes are scanned. Each part of the code is a specific type of Element, which is like a hierarchical structure of XML, such as classes, variables, methods, etc. Each Element represents a static, language-level component, as shown in the code below.

  1. package android.demo; // PackageElement  
  2. // TypeElement
  3. public class DemoClass {  
  4. // VariableElement
  5. private boolean mVariableType;  
  6. // VariableElement
  7. private VariableClassE m VariableClassE;  
  8. // ExecuteableElement
  9. public DemoClass () {
  10. }  
  11. // ExecuteableElement
  12. public void resolveData (Demo data //TypeElement ) {
  13. }
  14. }

Element represents the source code, while TypeElement represents the type element in the source code, such as a class. However, TypeElement does not contain information about the class itself. You can get the name of the class from TypeElement, but you cannot get information about the class, such as its parent class. This information needs to be obtained through TypeMirror. You can get the TypeMirror of an element by calling elements.asType().

1. Knowing Element, we can get all the scanned elements through RoundEnvironment in process, as shown in Figure X2.4. Through env.getElementsAnnotatedWith, we can get the list of elements annotated by @BindView, where validateElement verifies whether the element is available.

2. Because env.getElementsAnnotatedWith returns a list of all elements annotated with @BindView, sometimes we need to make some additional judgments, such as checking whether these elements are a class:

  1. @Override
  2. public boolean process( Set <? extends TypeElement> an, RoundEnvironment env) {
  3. for (Element e : env.getElementsAnnotatedWith(BindView.class)) {
  4. // Check if the element is a class
  5. if (ae.getKind() != ElementKind.CLASS) {
  6. ...
  7. }
  8. }
  9. ...
  10. }

3. javapoet (com.squareup:javapoet) is an open source library that generates java files according to specified parameters. If you are interested in learning about javapoet, you can take a look at javapoet - it frees you from repetitive and boring code. In the processor, after creating a JavaFile according to the parameters, you can use javaFile.writeTo(filer) through Filer to generate the java file you need.

4. Error handling: In the processor, we cannot directly throw an exception, because throwing an exception in process() will cause the JVM running the annotation processor to crash, resulting in very confusing stack trace information. Therefore, the annotation processor has a Messager class, which can normally output error information through messagger.printMessage( Diagnostic.Kind.ERROR, StringMessage, element).

At this point, your annotation processor has completed all the logic. It can be seen that compile-time annotations actually generate Java files during compilation, and then inject the generated Java files into the source code. At runtime, they do not affect efficiency and resources like runtime annotations.

Summarize

Let's use ButterKnife's process and give a simple example to summarize.

  1. @BindView generates XXXActivity$$ViewBinder.java according to Acitvity during compilation.
  2. ButterKnife.bind(this); called in Activity obtains ViewBinder through the class name of this plus $$ViewBinder, associates it with the Java file produced by the compiler processor, caches it in the map, and then calls ViewBinder.bind().
  3. In the bind method of ViewBinder, use the id and the encapsulation method in the butterknife.internal.Utils tool class of ButterKnife to inject the findViewById() control into the parameters of the Activity.

OK, through the above process, have you connected the generation and use of compile-time annotations? If you have any questions, please leave a message to discuss.

<<:  Apple acquires French image recognition company, may embed technology into iPhone

>>:  How to maximize HTML5 performance

Recommend

World Space Week | The world's first artificial satellite - Sputnik 1

On October 4, 1957, the world's first artific...

3 tips to increase followers on Zhihu for free!

Many people understand that I often change my ide...

Shanghai college entrance examination postponed for one month

On May 7, the Shanghai Municipal Epidemic Preventi...

Weijing TV violent review: not as powerful as an idol

In the field of Internet TV, Whale can be said to...

Product growth strategy methodology!

Product strategy is not based on features, but on...