An overview of the

Android compile time annotation framework from getting started to project practice. This series of 5 blog posts will teach you step by step how to build your own compile-time annotation framework, and then open source compile-time annotation framework based on APT.

When it comes to annotations, there are two common attitudes: hacking and low performance. Using annotations often makes it possible to do incredible things with very little code, such as frameworks like ButterKnife and Retrofit. However, runtime annotations have been criticized for causing serious performance problems due to Java reflection…

Today we’re going to talk about a dark technology that doesn’t have any impact on performance: compile-time annotations. Some people call it code generation, but there’s a little bit of a difference between doing annotations at compile time, getting the necessary information from annotations, generating code in your project, calling it at run time, and just running your handwritten code. More accurately, apt-Annotation Processing Tool

Using compile-time annotations properly can greatly improve development efficiency and avoid writing repetitive, error-prone code. Annotations can replace Java reflection most of the time at compile time, using code that can be called directly instead of reflection, which is a huge increase in runtime efficiency.

This three-part tutorial, the first in the Android Compile-time Annotation Framework series, gives you an overview of the annotation framework. We will then create our own compile-time annotation framework step by step.

  • What are annotations

  • Simple use of runtime annotations

  • A preliminary study on the source code of compile – time annotation framework ButterKnife

What are annotations

You are no doubt familiar with annotations. Here are our most common ones:

First, annotations fall into three categories:

  • Standard annotations

    Override, Deprecated, and SuppressWarnings are Java annotations that are recognized by the compiler and do not affect code execution. Their meanings are not the focus of this blog and will not be covered here.

  • Yuan the Annotation

    @Retention, @target, @Inherited, @documented, these are the annotations that define annotations. That is, we need to use them when we want to customize annotations.

  • The custom Annotation

    Custom annotations as needed. And the custom way, we’ll talk about that.

Similarly, custom annotations fall into three categories, defined by meta-annotation – @retention:

  • @Retention(RetentionPolicy.SOURCE)

    Source code annotations, commonly used as compiler tags. Such as Override, Deprecated, SuppressWarnings.

  • @Retention(RetentionPolicy.RUNTIME)

    Runtime annotations, which are identified by reflection at runtime.

  • @Retention(RetentionPolicy.CLASS)

    Compile-time annotations, which are identified and processed at compile time, are the focus of this chapter.

Simple use of runtime annotations

The essence of runtime annotations is that you mark them up in your code with annotations, and the runtime does some processing by looking for them through reflection. Runtime annotations have long been plagued by reflex inefficiency.

Here’s a Demo. The function is to set the layout file through annotations.

Previously we set up the layout file like this:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_home);
}
Copy the code

With annotations, we can set up the layout like this

@ContentView(r.layout.activity_HOME) Public Class HomeActivity extends BaseActivity {... }Copy the code

Let’s not talk about which one is better or worse, let’s just talk about the technology and not the requirements.

So how does this annotation work? That’s easy. Look down.

Create an annotation

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ContentView {
	int value();
}
Copy the code

The first line: @ Retention (RetentionPolicy. RUNTIME)

@retention is used to modify what type of annotation this is. This indicates that the annotation is a runtime annotation. This way APT knows when to process the annotation.

Line 2: @target ({elementtype.type})

The @target is used to indicate where the annotation can be used. For example: classes, methods, properties, interfaces, and so on. Elementtype. TYPE indicates that this annotation can be used to modify: Class, interface, or enum declaration. When you modify a method with a ContentView, the compiler prompts an error.

Line 3: Public @interface ContentView

Interface doesn’t mean ContentView is an interface. Just like declaring a class with the keyword class. Declare enumeration with enum. So the annotation is at sign interface. (It is worth noting that in the classification of ElementType, class, interface, Annotation and enum all belong to the same category as Type, and from the official Annotation, it seems that interface contains @interface.)

/** Class, interface (including annotation type), or enum declaration */
TYPE,
Copy the code

Line 4: int value();

The return value indicates what type of value can be stored in the annotation. For example, this is how we use it

@ContentView(R.layout.activity_home)
Copy the code

R.layout.activity_home is essentially an int id. If used this way, an error will be reported:

@ ContentView (" string ")Copy the code

The specific syntax of annotations is not detailed in this article, but will be put into the “Android Compile time Annotation Framework – Syntax Explanation”

Annotations parsing

Annotation statement is done, but how do you recognize and use this annotation?

@ContentView(r.layout.activity_HOME) Public Class HomeActivity extends BaseActivity {... }Copy the code

Annotation parsing is in BaseActivity. Let’s take a look at the BaseActivity code

public class BaseActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); For (Class c = this.getClass(); c ! = Context.class; c = c.getSuperclass()) { ContentView annotation = (ContentView) c.getAnnotation(ContentView.class); if (annotation ! = null) { try { this.setContentView(annotation.value()); } catch (RuntimeException e) { e.printStackTrace(); } return; }}}Copy the code

Step 1: Iterate through all the subclasses

Step 2: Find the class that decorates the annotation ContentView

Step 3: Get the property value of the ContentView.

Step 4: Set the layout for your Activity.

conclusion

You now have some understanding of the use of runtime annotations. We also know where the runtime annotations are sick.

You might think that *setContentView(r.layout.activity_HOME) is no different from @ContentView(R.layout.activity_home)*, and that using annotations adds to the performance problem.

But remember, this is just the simplest way to use annotations. For example, AndroidEventBus’s annotations are runtime annotations, which can be a bit of a performance issue, but can improve development efficiency.

Because the focus of this blog post is not runtime annotations, we will not parse the source code. “AndroidEventBus” is written by an older student of mine, “Haha”.

A preliminary study on the source code of compile – time annotation framework ButterKnife

ButterKnife is familiar to most of us, and with over 9000 starts, we can say goodbye to the boring findViewbyId. Here’s how it works:

Haven’t you ever wondered how it works? This is where compile-time annotations – code generation comes in.

Here’s the secret. After compiling your project, open the build directory in your project’s app directory:

Can you see some with *? ViewBinder* suffix class files. This is the code generated by ButterKnife. Let’s open it:

// Generated code from Butter Knife. Do not modify!

1.ForgetActivity? ViewBinder is packaged with our ForgetActivity:

package com.zhaoxuan.wehome.view.activity;
Copy the code

What does it mean to be in the same bag? ForgetActivity? ViewBinder can directly use property methods above the ForgetActivity protected level. Something like this:

AccountEdit = Finder. castView(View, 2131558541, "field 'accountEdit'"); //accountEdit is the control defined in ForgetActivity.Copy the code

So you can see why you get an error when using private?

2. Without going into details, let’s just take a look at what this generated code means. I’ll write the explanation in the comments.

Public void bind(final Finder Finder, final T target, Object source) {// Define a View Object reference, The object reference is reused (this is a lazy way to write it). // Whatever Finder is for now, it's an operation similar to findViewById. view = finder.findRequiredView(source, 2131558541, "field 'accountEdit'"); // Target is our ForgetActivity, AccountEdit = finder.castView(View, 2131558541, "field 'accountEdit'"); view = finder.findRequiredView(source, 2131558543, "field 'forgetBtn' and method 'forgetOnClick'"); target.forgetBtn = finder.castView(view, 2131558543, "field 'forgetBtn'"); / / set to view a click event the setOnClickListener (new butterknife. Internal. DebouncingOnClickListener () {@ Override public void DoClick (Android.view.view P0) {//forgetOnClick() is the event method we wrote in ForgetActivity. target.forgetOnClick(); }}); }Copy the code

OK, now you have a general idea of ButterKnife’s secret? Instead of writing findViewById code, we’ll automatically generate code. Now you must be wondering two questions:

1. When will the bind method be called? There’s no ForgetActivity in our code, right? ViewBinder is a weird class reference.

2. What exactly is Finder? Why can it find the view?

Take your time. Take your time.

Note: @bind’s definition

Here’s what we can read:

  1. Bind is a compile-time annotation

  2. You can only modify properties

  3. The property value is an array of ints.

After creating custom annotations, we can use APT to identify and parse these annotations, and we can use these annotations to get the value of the annotation, the type and name of the class that the annotation modifies. The name of the class the annotation belongs to, and so on.

The Finder class

From the code generated above, you must be wondering what Finder is. Finder is actually an enumeration.

FindView and getContext methods are provided with different implementations depending on the type. Now you’re finally looking at the familiar findViewById, ha ha, that’s the secret.

There are also two important Finder methods that I didn’t cover: Finder.findrequiredView and Finder.castView

FindRequiredView calls findOptionalView

FindOptionalView calls the findView method (findViewById) implemented by different enumeration classes.

FindView gets the view, and gives it to castView to do some fault tolerance.

CastView doesn’t do anything, just force and return. If there’s an exception, we do a catch, we just throw an exception and we don’t have to look at it.

ButterKnife. Bind (this) method

* Butterknife. bind(this)* This method is usually called in the BaseActivity onCreate method, and it seems that all findViewById methods are resolved by this bind method

Bind has several overloaded methods, but ends up calling the following one.

The target parameter is usually our Activity, and the source parameter is used to get the Context to find the resource. When target is activity, Finder is finder.activity.

First get the target (Activity) Class object and use it to find the generated Class, for example: ForgetActivity? ViewBinder.

And then call ForgetActivity? ViewBinder’s bind method.

This gives you a general idea of how ButterKnife works when the application is running. Here’s the highlight, the work ButterKnife does at compile time.

ButterKnifeProcessor

You may be wondering how ButterKnife recognizes annotations and generates code.

AbstractProcessor is the core class of APT, where all of the hacks are generated. AbstractProcessor only two of the most important method is the process and getSupportedAnnotationTypes.

Rewrite getSupportedAnnotationTypes method, used to represent the AbstractProcessor class to handle what annotation.

The first and most obvious is the Bind annotation.

All annotation processing is performed in process:

Use the findAndParseTargets method to retrieve the set of annotations that need to be processed. And then I iterate over it.

The JavaFileObject is the key object for our code generation, and its purpose is to write Java files. ForgetActivity? ViewBinder, a strange class file, is generated using a JavaFileObject.

Here we will focus only on the most important sentence

writer.write(bindingClass.brewJava());
Copy the code

ForgetActivity? All code in ViewBinder is spelled out using the bindingClass.brewJava method.

BindingClass brewJava method

Well, I don’t know how you feel when you see this code. Anyway, I saw this time only one word in my head: Good low ah…

I had no idea that something so dark and technologically advanced could be written like this. Line by line…

Now that I know it’s a string concatenation, I don’t want to read it, so I won’t put the whole code here.

You can see why the generated code was written in a lazy way

conclusion

When you peel back the veil of unfamiliar territory, the dark tech doesn’t seem much more than that, and even the code stitched together with strings feels lowish.

But isn’t that the beauty of learning?

All right, so to sum up.

  1. The beauty of compile-time annotations is that they generate code according to a certain strategy at compile time, avoiding duplication of code and improving development efficiency without affecting performance.

  2. There is a difference between code generation and Code insertion (Aspectj). The code insertion aspect is to insert code before and after the code runs, and the new code is triggered by the original code. While code generation is only automatically generated a set of independent code, the implementation of the code still needs to be actively invoked.

  3. APT is a very powerful set of mechanics, the only limitation of which is your creative design

  4. The principle of ButterKnife is simple, but why is there so much code for such a simple function? Because ButterKnife is an external dependency framework, it does a lot of fault tolerance and validation to ensure stable operation. So: The hardest thing about writing a framework is not technical implementation, but stability!

  5. One of ButterKnife’s great lessons is how generated code can be used to broker execution of existing code. This is something you should look into if you are working on an APT framework with proxy capabilities.

APT is like a cake in front of you, it’s up to you to eat it gracefully.

In the following chapters, I will launch several APT frameworks named after Cake.


The original address