Writing in the front

This year, we are doing componentization, componentization development inevitably needs to use Router (Router) to complete the data interaction between components, which promotes the development of various routes such as: Ali’s ARouter and ActivityRouter and other excellent Router framework. Annotations are used to facilitate the development of these Router libraries as well as libraries like ButterKnife. The purpose of this article is to undertake a wave of literacy.

This article introduction

  • Android annotations based
  • Parsing the Annotation
  • Practice – Make your own ButterKnife

1 Annotatin basis

1.1 the basic Annotation

Java provides three basic Annotation annotations, and when you use the Annotation, you add the @ sign in front of the Annotation, and you use the Annotation as a modifier. Note: The basic annotations provided by Java are found under the java.lang package

  1. Override: Qualified Override method: This annotation is basically a command for the compiler to help us check the code (to avoid simple errors such as writing method names). When a subclass uses the Override annotation, the IDE checks to see if the function is present in the superclass at compile time. If not, the compilation will fail.
public class Animal {
    public void run() {
        //TODO
    }
}

Copy the code
Public class Monkey extends Animal {@override public void run() {public class Monkey extends Animal {@override public void run()}}Copy the code
  1. @deprecated: Indicates that the class or method is obsolete. If you modify the Animal class to use the Deprecated run method, the IDE will warn you if your subclass uses the run method.
public class Animal {
    @Deprecated
    public void run() {
        //TODO
    }
}
Copy the code

3. @suppresswarnings: Suppress compiler warnings (rarely used). When Java code is compiled, the IDE often gives a lot of warnings to developers, such as variables not being used, which greatly affects our debug efficiency. This annotation is intended to suppress these warnings. Here’s an example:

 @SuppressWarning("unused")
 public void foo() {
  String s;
 }
Copy the code

If @suppressWarning is not used to suppress compiler warnings, the above code is never used by the warning variable s. If the phrase “unused” is used, this annotation supports the same inhibition type as the following figure (from IBM Knowledge Center).

The Annotation JDK 1.2 yuan

The JDK comes out with four Meta annotations under the Java.lang. Annotation package, in addition to the basic annotations introduced in 1.1. All four of these meta annotations are used to decorate custom annotations. (Hold the rhythm, after this summary we can customize annotations)

  1. @ Retention annotation. This Annotation can only decorate an Annotation definition, which specifies how long (or period) the decorated Annotation can be retained. There are three types of time

  • Retentionpolicy.source: nothing. The compiler simply ignores comments for this policy
  • Retentionpolicy.class: Default value for custom annotations, which the compiler saves in a CLASS file. Annotations like BindView in ButterKnife work this way.
  • Retentionpolicy.runtime: The compiler saves comments for this policy in a class file, which can be retrieved by programs such as reflection.

For example, customize a BindView Annotation (it doesn’t matter if you don’t understand it, there is a perceptual understanding of it, and we’ll do custom annotations in the next section).

@retention (value = retentionPolicy.class) public @interface BindView {int id() default 0; }Copy the code

When the member variable is value, it can be omitted. @Retention(RetentionPolicy.class)

  1. @Target Annotation: This is also used to decorate a custom Annotation Annotation that specifies which program elements the custom Annotation can decorate. The annotation’s member variables are
  • The elementType. PACKAGE annotation applies to packages
  • The elementType. TYPE annotation applies to types (classes, interfaces, annotations, enumerations)
  • ElementType.ANNOTATION_TYPE Annotation acts on annotations
  • The elementType. CONSTRUCTOR annotation is applied to the CONSTRUCTOR
  • The ElementType.METHOD annotation applies to methods
  • The elementType. PARAMETER annotation is applied to method parameters
  • The elementType. FIELD annotation is applied to the attribute
  • The ElementType.LOCAL_VARIABLE annotation is applied to local variables

Similarly, a member variable named value can be omitted. Let’s enrich the custom BindView annotation used above:

@target (elementType.field) @retention (value = retentionPolicy.class) public @retention (value = retentionPolicy.class) public @retention (value = retentionPolicy.class @interface BindView { int id() default 0; }Copy the code
  1. The @Documented annotation modifies a custom annotation that can be extracted into an API document using the Javac command.
  2. Inherited annotation, this annotation modifiers the custom has inheritance. For example, the Animal class uses a custom annotation with the @inherited modifier, and the Monkey subclass also has the characteristics described by that custom annotation.

1.3 Custom Annotations

  1. Define an Annotation, using the custom BindView Annotation used above as an example. You can create a New Java file of type Annotation directly.

  1. According to their own needs, the use of 1.2 is only to customize the annotations
/** * Created by will on 2018/2/4. */ @target (elementType.field @Retention(value = RetentionPolicy.CLASS) public @interface BindView { }Copy the code
  1. Define member variables. The member variables of custom annotations are defined in the form of methods. Enrich the BindView above, because the function of this custom annotation is to bind the View in the Activity. So let’s define an ID member variable.
/** * Created by will on 2018/2/4. */ @target (elementType.field @Retention(value = RetentionPolicy.CLASS) public @interface BindView { int id(); }Copy the code
  1. Use the default keyword to specify default values for member variables. Continue to enrich the BindView code. Note put the default keyword after int id().
@documented // Documented // Documented // Documented // Documented // Documented // Documented // Documented @target (ElementType.field) public @interface BindView { int id() default 0; }Copy the code

Depending on whether or not there are member variables, we can divide annotations into two types:

  • An Annotation that does not have a member variable is called a “tag Annotation”. This Annotation gives us information based on whether or not it exists, such as an Annotation such as Override
  • There are member variables called metadata annotations. We can use tools like APT to reprocess members of this Annotation.

Note: only defining a custom Annotation has no effect, and the Annotation information needs to be extracted and processed!!

Do you want to use the BindView annotation directly in the Activity? Such as:

And then you find Crash… This leads to the next section, which uses APT to reprocess annotated code.

2. The Annotation

Now that we have custom annotations, we also need to know what we’re going to do with those annotations, and that involves parsing the annotations. Parsing annotations is usually divided into: parsing of run-time annotations, parsing of compile-time annotations; Parsing an Annotation, which is really how do you find an Annotation from your code, usually we do it by:

  • Getting an Annotation by reflection, the way that annotations are resolved at run time
  • Using apt tool to get Annotation, compile time Annotation parsing method
  • In addition, if we need to generate additional code and files, we need to leverage the JavaPoet API

2.1 Use reflection to parse annotations

Reflection is resolved in a way that usually applies to the resolution of annotations at run time. Reflection refers to the use of Class, Field, Method, Construct and other reflect objects to obtain annotations

  • Field. GetAnnotation (Annotation. Class) : Gets an Annotation
  • Field.getanannotations () : Retrieve all annotations
  • Field. IsAnnotationPresent (Annotation. Class) : the existence of the Annotation

Annotations that are usually decorated with Runtime need reflection to work with parsing

@Retention(value = RetentionPolicy.RUNTIME)

  1. Create a test custom annotation
/** * Created by will on 2018/2/4. */ @Target(elementType.field) @Retention(value = RetentionPolicy.RUNTIME) public @interface test { int id() default 0; }Copy the code
  1. Create a new Java class Animal and add the test annotation
public class Animal {
    @BindView(id = 1000)
    String a;

    @Deprecated
    public void run() {
        //TODO
    }
}
Copy the code
  1. You can use reflection to get the value of the annotation member attribute of A
private void testMethod() { Class clazz = Animal.class; Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { BindView bindView = field.getAnnotation(BindView.class); if (bindView ! = null) { int id = bindView.id(); Log.e("------", String.valueOf(id)); }}}Copy the code

2.2 Use apt tool to resolve annotations

APT is an Annotation Processing Tool. With APT, we can find the annotations in the source generation and process them accordingly

  • Generate additional source files or other files based on the annotations
  • Compile the generated source file and the original source file together to generate the class file

With APT, generating extra code at compile time has no impact on performance, just the speed of the project build

We are going to talk about the steps to use APT in Android. Developing a custom APT in Android is basically enough to learn two libraries and one class

  • The main purpose of the JavaPoet API library is to help us generate code in the form of class calls. The simple idea is to use the library to generate additional Java code. The specific API can go to Github to see, write very detailed. I’m not going to post the code here.
  • AutoService is a library developed by Google to annotate the processor class and generate meta-INF configuration information for it. It’s understandable that the IDE will compile our Annotation handler when we use this library, and we just need to add annotations to our custom Processor class

@AutoService(Processor. Class)

  • The Processor class, our custom Annotation handler needs to implement this interface. Java provides us with an abstract class that implements some of the functions of this interface. Most of the time we need to customize Annotation handler, we only need to inherit AbstractProcessor, an abstract class .

The learning of JavaPoet can be directly borrowed from the official API, and the learning cost of AutoService is low (only one line of code is needed, and the learning cost can be ignored). Let’s focus on learning the use of AbstractProcessor.

AbstractProcessor introduction

  1. Create a new AbstractProcessor to see how this method works
/** * Created by will on 2018/2/5. */ public class CustomProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { return false; } @Override public Set<String> getSupportedAnnotationTypes() { return super.getSupportedAnnotationTypes(); } @Override public SourceVersion getSupportedSourceVersion() { return super.getSupportedSourceVersion(); }}Copy the code
  • Init (ProcessingEnvironment ProcessingEnvironment): Every annotation processor class must have an empty constructor. However, there is a special init() method that is called by the annotation processing tool with the ProcessingEnviroment parameter. ProcessingEnviroment provides a number of useful tool classes Elements,Types, and Filer.
  • process(Set
    Set, RoundEnvironment RoundEnvironment): This is equivalent to main() for each processor. This is where you write your code to scan, evaluate, and process annotations, as well as generate Java files. The input parameter RoundEnviroment lets you query for annotated elements that contain specific annotations. Take the example of the custom annotation BindView mentioned earlier. Here you can look up all the activities annotated with BindView.
  • GetSupportedAnnotationTypes () : this must be specified by the developer, the method returns a Set of role is the annotation processor support treatment which annotations.
  • GetSupportedSourceVersion () : used to specify the Java version you use. Usually here return SourceVersion. LatestSupported (). However, you can also return SourceVersion.RELEASE_7 if you have good reasons to only support Java 7.
  1. AbstractProcessor Basic tool parsing: A series of tools are available from the Init method of AbstractProcessor to assist in parsing the source code
  • Elements tools

Elements utility class is obtained in the Init method of AbstractProcessor

 @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
       Elements elementUtils = processingEnv.getElementUtils();
    }
Copy the code

This utility class is used to handle source code. In the domain of the custom annotation processor, each type of Java source code is an Element, and you can refer to the official Java documentation for details

package com.example;    // PackageElement

public class Test {        // TypeElement

    private int a;      // VariableElement
    private Test other;  // VariableElement

    public Test () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}
Copy the code

For example, I have a TypeElement and want to use the Elemnts tool by getting the name of the package in which the class is located

 private String getPackageName(TypeElement type) {
        return elementUtils.getPackageOf(type).getQualifiedName().toString();
    }
Copy the code

Let’s have another chestnut, we have a TypeElement that represents Test, and we want to get all the child elements so we can write this (note, this is useful)

TypeElement testClass = ... ;  
for (Element e : testClass.getEnclosedElements()){ // iterate over children  
    Element parent = e.getEnclosingElement();  // parent == testClass
}
Copy the code
  • Types: a utility class for handling TypeMirror; A TypeElement does not contain information about the class itself. You can get the name of the class from the TypeElement, but you can’t get information about the class, such as its parent class. This information needs to be obtained with TypeMirror. You can get the TypeMirror for an element by calling elements.astype ().
  • Filer: As the name suggests, with Filer you can create files.

After reading the boring basics, let’s write a simple ButterKnife together

3. Write your own lightweight ButterKnife

1. Create a New Java project named Annotations

  1. This project is used to define all custom annotations. This section uses the knowledge base from section 1.

  1. To create a new custom annotation in the project package, we will imitate ButterKnife and add a BindView annotation here

@documented // Documented // Documented // Documented // Documented // Documented // Documented // Documented @target (ElementType.field) public @interface BindView { int id() default 0; }Copy the code

2 Create a Java project named Annotations_compiler

  1. This project is used to handle custom annotations. Let’s call this project the BindView processor. This requires the knowledge base of section 2
  2. Add a dependency between AutoService and JavaPoet in the build.gradle file
Implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2' implementation 'com. Squareup: javapoet: 1.7.0'Copy the code
  1. Create a new BindViewProcessor class that inherits from AbstractProcessor and processes the source code’s annotations (I added annotations wherever I could understand ambiguity).
/** * Created by will on 2018/2/4. */ @AutoService(Processor.class) public class BindViewProcessor extends AbstractProcessor {/** * utility class, which can get */ private Elements elementUtils from ProcessingEnvironment of init method; /** * Cache all child elements * key: parent Element class name * value: child Element */ private HashMap<String, List<Element>> cacheElements = null; Private HashMap<String, Element> cacheAllParentElements = null; /** * cacheAllParentElements * key: parent Element class name * value: parent Element */ private HashMap<String, Element> cacheAllParentElements = null; @ Override public Set < String > getSupportedAnnotationTypes () {/ / regulations to deal with return annotation type Collections.singleton(BindView.class.getCanonicalName()); } @Override public boolean process(Set<? extends TypeElement> annotations , RoundEnvironment roundEnv) {// scan all fields where BindView is annotated, because we all annotate BindView is an Activity member Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class); For (Element Element: elements) {// Filter all child elements addElementToCache(Element); } if (cacheElements == null || cacheElements.size() == 0) { return true; } for (String parentElementName : CacheElements.keyset ()) {// Determine if the parent Element is a class. Try {// Use JavaPoet to construct a method MethodSpec.Builder bindViewMethodSpec = MethodSpec.methodBuilder("bindView") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(ClassName.get(cacheAllParentElements.get(parentElementName).asType()) , "targetActivity"); List<Element> childElements = cacheElements.get(parentElementName); if (childElements ! = null && childElements.size() ! = 0) { for (Element childElement : childElements) { BindView bindView = childElement.getAnnotation(BindView.class); / / use JavaPoet to add bindViewMethodSpec method content. AddStatement (the String. Format (" targetActivity. % s = (% s) targetActivity.findViewById(%s)" , childElement.getSimpleName() , ClassName.get(childElement.asType()).toString() , bindView.id())); }} // Construct a class that starts with Bind_ TypeSpec typeElement = TypeSpec. ClassBuilder ("Bind_" + cacheAllParentElements.get(parentElementName).getSimpleName()) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(bindViewMethodSpec.build()) .build(); JavaFile JavaFile = Javafile.Builder (getPackageName((TypeElement)) cacheAllParentElements.get(parentElementName)) , typeElement).build(); javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); return true; } } return true; } private String getPackageName(TypeElement type) { return elementUtils.getPackageOf(type).getQualifiedName().toString(); } @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); elementUtils = processingEnv.getElementUtils(); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } /** * Cache the parent Element ** @param childElement */ private void addElementToCache(Element) childElement) { if (cacheElements == null) { cacheElements = new HashMap<>(); } if (cacheAllParentElements == null) { cacheAllParentElements = new HashMap<>(); } // Parent Element class name String parentElementName = null; parentElementName = ClassName.get(childElement.getEnclosingElement().asType()).toString(); if (cacheElements.containsKey(parentElementName)) { List<Element> childElements = cacheElements.get(parentElementName); childElements.add(childElement); } else { ArrayList<Element> childElements = new ArrayList<>(); childElements.add(childElement); cacheElements.put(parentElementName, childElements); cacheAllParentElements.put(parentElementName, childElement.getEnclosingElement()); }}}Copy the code

3. Create a new Android project using custom annotations

  1. Add references to both of the above projects

AnnotationProcessor provides annotationProcessor for Android to replace Android-apt. AnnotationProcessor supports both JavAC and Jack compilation methods. Android-apt only supports javac. At the same time, the author of Android-apt announced that it’s not being maintained, so I’m just using annotationProcessor here

implementation project(':annotations')
annotationProcessor project(':annotations_compiler')
Copy the code
  1. Add the @bindView annotation to the Activity View and set the ID
public class MainActivity extends AppCompatActivity { @BindView(id = R.id.tv_test) TextView tv_test; @BindView(id = R.id.tv_test1) TextView tv_test1; @BindView(id = R.id.iv_image) ImageView iv_image; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Bind_MainActivity.bindView(this); tv_test.setText("test_1"); tv_test1.setText("test_2"); iv_image.setImageDrawable(getDrawable(R.mipmap.ic_launcher)); }}Copy the code
  1. At this point, your IDE may report that Bind_MainActivity cannot be found. After Build the app/Build/generated/source/apt/debug / [your package name] / annotation/path returned to generate apt output files.

Other questions

  1. If you find that there is no apt output after build, it is because your processor has a Bug. This is where you need to debug your processor. How to debug, please follow this blog post
  2. For the problem of android-APT switching to official annotationProcessor, please switch android-APT switching to official annotationProcessor
  3. To add ing…

Finally, attach the source code of demo

Refer to the article

  • APT Processor (AbstractProcessor)
  • The Annotation annotations
  • Android APT (compile-time code generation) best practices

About Me

contact way value
mail [email protected]
wechat W2006292
github github.com/weixinjie
blog Juejin. Im/user / 308708…