How Android writes projects based on compile-time annotations

Source: Internet
Author: User

This article has been published in CSDN "Programmer" magazine.

This article has authorized the public number: Yang (hongyangandroid) in the public platform original debut.

Reprint please indicate the source:
http://blog.csdn.net/lmj623565791/article/details/51931859;
This article is from: "Zhang Hongyang's Blog"

I. Overview

In Android application development, we often choose to use some annotation-based frameworks in order to improve development efficiency, but because of the loss of operational efficiency due to reflection, we prefer the framework of compile-time annotations, for example:

    • Butterknife frees us to write the initialization of the view and the injected code of the event.
    • EVENTBUS3 allows us to make communication between the two.
    • Fragmentargs easily adds parameter information to the fragment and provides a way to create it.
    • Parcelablegenerator enables automatic conversion of arbitrary objects to parcelable types for easy object transfer.

There are a lot of similar libraries, and most of these libraries are designed to automatically help us with the parts of our daily coding that need to be duplicated (for example, every activity view needs to be initialized, and every object that implements Parcelable the interface needs to write many code that is written in a fixed notation).

This is not to say that the above framework must not use reflection, in fact, some of the above framework inside or some of the implementation is dependent on reflection, but very few and generally do the cache processing, so relatively, the efficiency of the impact is very small.

However, when using such projects, sometimes errors can be difficult to debug, the main reason is that many users do not understand the framework of its internal principles, so encountered problems will consume a lot of time to troubleshoot.

So, in the case of sentiment, at compile time, when the frame is so hot, we have reason to learn:

    • How to write an opportunity compile-time annotated project

First of all, it is to understand the principle, so that when we encounter problems with a similar framework, we can find the right way to troubleshoot problems, second, if we have a good idea, we found that some code needs to be created repeatedly, we can also write a framework to facilitate their daily coding, improve coding efficiency At last, it is the improvement of its own technology.

Note: The following uses the IDE Android Studio .

This article provides a clue to the framework for writing a view injection, detailing the steps to write such a framework.

II. Preparation prior to preparation

When writing such a framework, it is generally necessary to create multiple module, such as the example that will be implemented in this article:

    • Ioc-annotation for storing annotations, etc., Java modules
    • Ioc-compiler for writing annotation processors, Java modules
    • IOC-API is used to provide the user with the API, this example is the Andriod module
    • Ioc-sample example, this example is the Andriod module

So in addition to the example thought, generally to establish 3 Module,module name you can consider yourself, the above gives a simple reference. Of course, if the conditions allow, some developers like to store annotations and API two module to merge into a module.

For the dependency between module, because the annotation processor needs to rely on the relevant annotations, so:

Ioc-compiler Dependent ioc-annotation

We use annotations and related APIs in the process of using them.

So Ioc-sample relies on ioc-api;ioc-api to rely on ioc-annotation

Third, the realization of the annotation module

Note module, mainly used to store some annotation classes, this example is to imitate butterknife implementation of view injection, so this example requires only one annotation class:

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

The retention policy we set is class and annotations are used on field. Here we need to pass in an ID when we use it and set it directly as value.

When you write, you need several annotation classes to analyze yourself, and you can set the correct settings @Target as well @Retention .

Iv. implementation of the annotation processor

Once you've defined the annotations, you can write the annotation processor, which is a bit complicated, but also a chapter.

This module, we typically rely on the annotation module, as well as the ability to use a auto-service library

build.gradleDepends on the following conditions:

dependencies {    compile ‘com.google.auto.service:auto-service:1.0-rc2‘    compile project (‘:ioc-annotation‘)}

auto-serviceLibrary can help us to generate META-INF information such as.

(1) Basic code

Note Processor is generally inherited AbstractProcessor , just now we say rule-based, because part of the code is basically fixed, as follows:

@AutoService(Processor.class) Public  class iocprocessor extends abstractprocessor{    PrivateFiler mfileutils;PrivateElements melementutils;PrivateMessager Mmessager;@Override     Public synchronized void Init(Processingenvironment processingenv) {Super. Init (PROCESSINGENV);        Mfileutils = Processingenv.getfiler ();        Melementutils = Processingenv.getelementutils ();    Mmessager = Processingenv.getmessager (); }@Override     PublicSet<string>Getsupportedannotationtypes() {set<string> annotationtypes =NewLinkedhashset<string> (); Annotationtypes.add (BindView.class.getCanonicalName ());returnAnnotationtypes; }@Override     PublicSourceVersiongetsupportedsourceversion(){returnSourceversion.latestsupported (); }@Override     Public Boolean Process(set<? extends typeelement> annotations, roundenvironment roundenv) {    }

After implementation AbstractProcessor , the process() method must be implemented and is the core part of the code we are writing, which is described later.

We will generally implement getSupportedAnnotationTypes() and getSupportedSourceVersion() two methods, both of which return a supported annotation type, a return to the supported source version, referring to the above code, the wording is basically fixed.

In addition, we will also choose the replication init() method, which passes a parameter processingEnv , can help us to initialize some of the auxiliary classes:

    • Filer mfileutils; A file-related helper class that generates Javasourcecode.
    • Elements melementutils; Auxiliary classes related to elements help us to get some information about the elements.
    • Messager Mmessager; The auxiliary class associated with the log.

Here is a simple mention Elemnet , we have a simple understanding of its several sub-categories, according to the following comments, there should be a simple cognition.

Element   - VariableElement //一般代表成员变量  - ExecutableElement //一般代表类中的方法  - TypeElement //一般代表代表类  - PackageElement //一般代表Package
(2) Implementation of process

The implementation in the process, compared with a little more complex, generally you can think of two big steps:

    • Collect information
    • Generate the proxy class (this article calls the class called the proxy class at compile time)

What do you mean by collecting information? is to get the corresponding element according to your annotation statement, and then get the information we need, which must be prepared for later generation JavaFileObject .

For example, we will generate a proxy class for each class, for example MainActivity we will generate one MainActivity$$ViewInjector . So if the annotations are declared in multiple classes, there are multiple classes, and here's what you need:

    • A class object that represents all the information generated by the proxy class for a particular class, in this caseProxyInfo
    • A collection that holds the above class object (which then iterates through the build proxy class), in this case Map<String, ProxyInfo> , key is the full path of the class.

The description here is a little vague, it doesn't matter, a combination of code is good to understand.

A. Gathering information
Privatemap<string, proxyinfo> mproxymap =NewHashmap<string, proxyinfo> ();@Override Public Boolean Process(set<? extends typeelement> annotations, roundenvironment roundenv)    {mproxymap.clear (); set<? Extends element> elements = Roundenv.getelementsannotatedwith (Bindview.class);//I. Collection of information     for(Element element:elements) {//Check element type        if(!checkannotationusevalid (Element)) {return false; }//field TypeVariableelement variableelement = (variableelement) element;//class TypeTypeelement typeelement = (typeelement) variableelement.getenclosingelement ();//typeelementString qualifiedname = Typeelement.getqualifiedname (). toString (); Proxyinfo proxyinfo = Mproxymap.get (QualifiedName);if(Proxyinfo = =NULL) {Proxyinfo =NewProxyinfo (Melementutils, typeelement);        Mproxymap.put (QualifiedName, proxyinfo); } BindView annotation = Variableelement.getannotation (Bindview.class);intid = annotation.value ();    ProxyInfo.mInjectElements.put (ID, variableelement); }return true;}

First we call mProxyMap.clear(); , because the process can be called multiple times, avoiding the generation of duplicate proxy classes and avoiding the existence of exceptions to the class name of the generated class.

Then, by roundEnv.getElementsAnnotatedWith getting us through @BindView the annotated element, here the return value, according to our expectation should be the VariableElement collection, because we use it on the member variable.

Next for loop our element, first check whether the type is VariableElement .

Then get the corresponding class information TypeElement , and then generate the ProxyInfo object, here through a mProxyMap check, key is the qualifiedName full path of the class, if no generation will be to generate a new, ProxyInfo and the class is one by one corresponding.

Next, we will add the corresponding and @BindView declared to the class VariableElement , and ProxyInfo key is the ID we filled out when we declared it, that is, the ID of the view.

This completes the information collection, after collecting the complete information, should be able to generate the proxy class.

B. Generating a proxy class
 @Override  public  boolean  process  (set<? extends typeelement> annotations, roundenvironment roundenv) {//...  Omit the code that collects the information, and Try,catch related  for  (String key:mProxyMap.keySet ()) {Proxyinfo        Proxyinfo = Mproxymap.get (key); Javafileobject sourcefile = Mfileutils.createsourcefile (Proxyinfo.getproxyclassfullname (), proxyInfo.getTy            Peelement ());            Writer writer = Sourcefile.openwriter ();            Writer.write (Proxyinfo.generatejavacode ());            Writer.flush ();    Writer.close (); } return  true ;}  

You can see that the code for the build proxy class is very brief, mostly traversing us mProxyMap , and then getting each one ProxyInfo , and finally mFileUtils.createSourceFile creating the file object, the class name proxyInfo.getProxyClassFullName() , and the contents of the write proxyInfo.generateJavaCode() .

It seems that the method of generating Java code is inside the Proxyinfo.

C. Generating Java code

Here we focus primarily on the way in which Java code is generated.
Here are the main ways to generate Java code:

#ProxyInfo//key to Id,value for the corresponding member variable PublicMap<integer, variableelement> minjectelements =NewHashmap<integer, variableelement> (); PublicStringGeneratejavacode() {StringBuilder builder =NewStringBuilder (); Builder.append ("Package"+ mpackagename). Append ("; \ n"); Builder.append ("Import com.zhy.ioc.*;\n"); Builder.append ("public class"). Append (Mproxyclassname). Append ("Implements"+ SUFFIX +"<"+ mtypeelement.getqualifiedname () +">"); Builder.append ("\n{\n");    Generatemethod (builder); Builder.append ("\n}\n");returnBuilder.tostring ();}Private void Generatemethod(StringBuilder builder) {Builder.append ("public void inject ("+mtypeelement.getqualifiedname () +"host, Object object)"); Builder.append ("\n{\n"); for(intId:mInjectElements.keySet ()) {Variableelement variableelement = minjectelements.get (ID);        String name = Variableelement.getsimplename (). toString ();        String type = Variableelement.astype (). toString (); Builder.append ("if (object instanceof android.app.Activity)"); Builder.append ("\n{\n"); Builder.append ("host."+name). Append (" = "); Builder.append ("("+type+") (((android.app.Activity) object). Findviewbyid ("+id+"));"); Builder.append ("\n}\n"). Append ("Else"). Append ("\n{\n"); Builder.append ("host."+name). Append (" = "); Builder.append ("("+type+") (((Android.view.View) object). Findviewbyid ("+id+"));"); Builder.append ("\n}\n"); } builder.append ("\n}\n");}

Here is mainly by the information gathered, splicing the completion of the proxy class object, looks like a headache, but I gave a generated code, compared to see a lot.

 PackageCom.zhy.ioc_sample;Importcom.zhy.ioc.*; Public  class mainactivity$$viewinjector implements Viewinjector<  COM. Zhy. ioc_sample. mainactivity>{    @Override     Public void Inject(com.zhy.sample.MainActivity host, Object object) {if(Objectinstanceofandroid.app.Activity) {HOST.MTV = (Android.widget.TextView) ((android.app.Activity) object). Findviewbyid (2131492945)); }Else{HOST.MTV = (Android.widget.TextView) (((Android.view.View) object). Findviewbyid (2131492945)); }    }}

It would be much better to look at the code above and actually die based on the member variables that were collected ( @BindView declared), and then generate the Java code based on the requirements that we specifically want to implement.

Note here that the generated code implements an interface that ViewInjector<T> is intended to unify the types of all proxy class objects, and then we need the strongly-proxied class object as the interface type, calling its method, and the interface being generic, mainly passing in the actual class object, for example MainActivity , Because we are actually accessing the code in the generated proxy class, the 实际类.成员变量 member variables that use compile-time annotations generally do not allow modifiers to be private decorated (some allow, but need to provide getter,setter access methods).

The Java code is written in a fully spliced way, and you can use some open source libraries to generate code through the Java API, for example:

    • Javapoet.

A Java API for generating. Java source files.

Here we complete the generation of proxy classes, where any annotation processor is written in a way that basically follows the steps of gathering information and generating proxy classes.

Five, the implementation of API module

With the proxy class, we typically provide APIs for users to access, for example, the access entry for this example is

//Activity中//Fragment中,获取ViewHolder中 Ioc.inject(this, view);

Imitating Butterknife, the first parameter is the host object and the second argument is the object that is actually called findViewById ; of course, in Actiivty, two parameters are the same.

    • How does the API generally be written?

In fact, as long as you understand the principle, this API will do two things:

    • Look for the proxy class we generated based on the incoming host: for example MainActivity->MainActity$$ViewInjector .
    • Strong to a unified interface, invoking the methods provided by the interface.

These two things should not be complicated, the first thing is to splice the proxy class name, and then reflect the generated object, the second thing strong turn call.

 Public  class Ioc{     Public Static void Inject(Activity activity)    {Inject (activity, activity); } Public Static void Inject(Object host, Object root)        {class<?> clazz = Host.getclass (); String proxyclassfullname = clazz.getname () +"$ $ViewInjector";//Omit Try,catch related codeclass<?> Proxyclazz = Class.forName (proxyclassfullname);        Viewinjector viewinjector = (com.zhy.ioc.ViewInjector) proxyclazz.newinstance ();    Viewinjector.inject (Host,root); }} Public  interface viewinjector<T>{    voidInject (t-t, Object object);}

The code is simple, stitching the full path of the proxy class, and then newInstance invoking the inject method of the proxy class by generating an instance and then a strong turn.

In general, the generated proxy class will be cached, such as the use of Map storage, no regeneration, here we do not do.

This completes the writing of a compile-time annotation framework.

Vi. Summary

This paper describes how to write a project based on compile-time annotations, the main steps are: the partition of the project structure, the realization of the annotation module, the writing of the annotation processor, and the compilation of the API module published externally. Learning through text should be able to understand how the framework works based on compile-time annotations and how to write such a framework.

Welcome to follow my Weibo:
http://weibo.com/u/3165018720

Group No.: 497438697, welcome into the group

Public Number: Hongyangandroid
(Welcome to pay attention, do not miss every dry, support contributions)

How Android writes projects based on compile-time annotations

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.