1, The importance of compile time annotations in development
Xianghan novel network https://www.1599.infoFrom the amazing ButterKnife in the early days, to various routing frameworks led by ARouter later, to the Jetpack component vigorously promoted by Google, more and more third-party frameworks are using the technology of compile time annotation. It can be said that whether you want to deeply study the principles of these third-party frameworks or become a senior Android Development Engineer, Compile time annotation is a basic technology you have to master.
This article starts with the basic run-time annotation usage and gradually evolves to the compilation time annotation usage, so that you can really understand what scenario, how to use and what benefits the compilation time annotation should have.
2, Handwritten runtime notes
Similar to the following writing method, when the View keeps writing many lines of findViewById, it is very troublesome to write. First, we try to solve this problem with runtime annotation to see if we can automatically handle these findViewById operations.
The first is the engineering structure. You must define a lib module.
Secondly, define our annotation class:
With this annotated class, we can use it first in our mainactivity, although this annotation has not played any role at this time.
Let's think about it here. At this time, we need to assign R.id.xx to the corresponding field through annotation, that is, the view objects you define (such as TV in the red box). For our lib project, because MainActivity depends on lib, naturally you can't rely on the app project to which Main belongs. Here are two reasons:
-
A depends on B, and B's circular dependence on a will certainly report an error;
-
Since you want to build a lib, you must not rely on the user's host. Otherwise, how can you call lib?
So the problem becomes that the lib project can only get the activty, but not the host's MainActivity. Since I can't get the host's MainActivity, how can I know how many field s this activity has? Reflection is used here.
public class BindingView { public static void init(Activity activity) { Field[] fields = activity.getClass().getDeclaredFields(); for (Field field : fields) { //Get annotated BindView annotation = field.getAnnotation(BindView.class); if (annotation != null) { int viewId = annotation.value(); field.setAccessible(true); try { field.set(activity, activity.findViewById(viewId)); } catch (IllegalAccessException e) { e.printStackTrace(); } } } } }
Finally, we call this method in the host's MainActivity:
In fact, someone has to ask here. This runtime annotation doesn't look difficult. Why doesn't it seem to be used by many people? The problem lies in the stack of methods just reflected. Reflection is known to cause some performance loss to the Android runtime, and the code here is a cycle, that is, the code here will become slower and slower with the increase of the interface complexity of your lib Activity. This is a process that will deteriorate gradually with the increase of the interface complexity, Single reflection has almost no performance consumption for today's mobile phones, but the use of reflection in this for loop is still as little as possible.
3, Handwritten compile time notes
To solve this problem, compile time annotations are used. Now let's try to solve the above problem with compile time annotations. As we said earlier, runtime annotations can use reflection to get the host field to complete the requirements. In order to solve the performance problem of reflection, the code we actually want is as follows:
We can create a new MainActivityViewBinding class in the module of the app:
Then call this method in our BindingView (note that our BindingView is under lib module) to solve the reflection problem?
However, there is a problem here. Since you are a lib, you can't rely on the host, so you can't get the class MainActivityViewBinding in the lib Module. You still have to use reflection.
You can take a look at the code commented out above. Why not write the string directly? Because you are a lib library, of course you have to be dynamic, otherwise how can you use it for others? In fact, you can get the class name of the host and add a fixed suffix ViewBinding. At this time, we will get the Binding class, right? The rest is to call the constructor.
public class BindingView { public static void init(Activity activity) { try { Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "ViewBinding"); Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass()); constructor.newInstance(activity); } catch (ClassNotFoundException | NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
Here is the code structure:
Someone here wants to ask, don't you still use reflection here, right! Although reflection is used here, the reflection here will only be called once. No matter how many field s your activity has, the reflection method here will only be executed once. Therefore, the performance must be many times faster than the previous scheme. Then, although the code can run normally, there is another problem. Although I can call the method of constructing the class of our app host in lib, is the class of the host still written by us? Your lib library still doesn't play any role in making us write less code.
At this time, we need our apt to come out, which is the core of compile time annotation. We create a Java Library. Note that Java lib is not android lib, and then introduce it in app module.
Note that the introduction method is not imp, but annotation processor;
Then let's modify lib_processor, first create an annotation processing class:
Then create the file resources / meta-inf / services / javax annotation. processing. Processor, be careful not to write the wrong folder here.
Then specify our annotation Processor for this Processor:
It's not over yet. We have to tell the annotation processor to only process our BindView annotations. Otherwise, the annotation processor will be too slow to process all annotations by default. However, at this time, our BindView annotation class is still in the lib warehouse. Obviously, we need to adjust our engineering structure:
Let's create a new Javalib, just put BindView, and then let our Lib_ Both processor and app depend on this lib_interface. Slightly modify the code. At this time, we are processing at compile time, and the Policy is not runtime.
@Retention(RetentionPolicy.SOURCE) @Target(ElementType.FIELD) public @interface BindView { int value(); }
public class BindingProcessor extends AbstractProcessor { Messager messager; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { messager = processingEnvironment.getMessager(); messager.printMessage(Diagnostic.Kind.NOTE, " BindingProcessor init"); super.init(processingEnvironment); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { return false; } //What annotations do you want to support @Override public Set<String> getSupportedAnnotationTypes() { return Collections.singleton(BindView.class.getCanonicalName()); } }
So far, most of our work has been handled. Take another look at the code structure (the code structure here must understand why it is designed like this, otherwise you won't learn compile time annotation).
We have been able to call the MainActivityViewBinding method through the lib SDK, but it is still handwritten by us in the app warehouse, which is not very intelligent and can not be used. We need to dynamically generate this class in the annotation processor. As long as we can complete this step, our SDK will be basically completed.
It should be mentioned here that many people are stuck here because too many articles or tutorials come up with the set of code of JavaPoet. They can't learn at all, or they can only copy and paste other people's things. They won't change it a little, In fact, the best way to learn here is to spell the code we want by StringBuffer string splicing. Through this string splicing process, we can understand the corresponding api and the idea of generating java code, and then use javapool to optimize the code.
We can first think about the steps to be completed if we use string splicing to generate this class.
-
First, get which classes use our BindView annotation;
-
Get the field s in these classes that use the BindView annotation and their corresponding values;
-
Get the class names of these classes so that we can generate class names such as MainActivityViewBinding;
-
Get the package names of these classes, because the class we generate should belong to the same package as the class to which the annotation belongs, so that there will be no problem with field access permission;
-
After the above conditions are met, we can splice the java code we want by string splicing.
Here you can go directly to the code. You can directly look at the comments for important parts. It should not be difficult to understand the code comments after the above step analysis.
public class BindingProcessor extends AbstractProcessor { Messager messager; Filer filer; Elements elementUtils; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { //It is mainly used to output some important logs messager = processingEnvironment.getMessager(); //You can understand it as the important output parameters we need to use to write java files filer = processingEnvironment.getFiler(); //Some convenient utils methods elementUtils = processingEnvironment.getElementUtils(); //Note here that diagnostic Kind. Error is some important parameter verification that can make the compilation fail. You can use this to prompt the user where you wrote wrong messager.printMessage(Diagnostic.Kind.NOTE, " BindingProcessor init"); super.init(processingEnvironment); } private void generateCodeByStringBuffer(String className, List<Element> elements) throws IOException { //The class you want to generate should belong to the same package as the annotated class, so you should also take the name of the package String packageName = elementUtils.getPackageOf(elements.get(0)).getQualifiedName().toString(); StringBuffer sb = new StringBuffer(); // Every java class starts with package sth sb.append("package "); sb.append(packageName); sb.append("; "); // public class XXXActivityViewBinding { final String classDefine = "public class " + className + "ViewBinding { "; sb.append(classDefine); //Defines the beginning of the constructor String constructorName = "public " + className + "ViewBinding(" + className + " activity){ "; sb.append(constructorName); //Traverse all element s to generate such as activity tv=activity. Statements such as findviewbyid (r.id.xxx) for (Element e : elements) { sb.append("activity." + e.getSimpleName() + "=activity.findViewById(" + e.getAnnotation(BindView.class).value() + "); "); } sb.append(" }"); sb.append(" }"); //After the file content is determined, it can be generated directly JavaFileObject sourceFile = filer.createSourceFile(className + "ViewBinding"); Writer writer = sourceFile.openWriter(); writer.write(sb.toString()); writer.close(); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { // key is the class name of the class using the annotation. Element is the element using the annotation itself. A class can have multiple field s using the annotation Map<String, List<Element>> fieldMap = new HashMap<>(); // Here we get all the element s that use the BindView annotation for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) { //Get the Name of the class to which this annotation belongs String className = element.getEnclosingElement().getSimpleName().toString(); //After obtaining the value, judge whether there is any in put in the map. If not, directly add an element to the value if (fieldMap.get(className) != null) { List<Element> elementList = fieldMap.get(className); elementList.add(element); } else { List<Element> elements = new ArrayList<>(); elements.add(element); fieldMap.put(className, elements); } } //Traverse the map and start generating auxiliary classes for (Map.Entry<String, List<Element>> entry : fieldMap.entrySet()) { try { generateCodeByStringBuffer(entry.getKey(), entry.getValue()); } catch (IOException e) { e.printStackTrace(); } } return false; } //What annotations do you want to support @Override public Set<String> getSupportedAnnotationTypes() { return Collections.singleton(BindView.class.getCanonicalName()); } }
Finally, let's see the effect:
Although the generated code format is not very good-looking, it runs ok. Note the Element interface here. In fact, if you can understand the Element when using compile time annotation, the subsequent work will be much simpler.
Focus on the five subclasses of Element, for example:
package com.smart.annotationlib_2;//PackageElement | represents a package element // TypeElement represents a class or interface program element. public class VivoTest { //VariableElement | represents a field, enum constant, method or constructor parameter, local variable or exception parameter. int a; //VivoTest this method: ExecutableElement | represents the method, constructor or initializer (static or instance) of a class or interface, including annotation type elements. //int b this function parameter: TypeParameterElement | represents the formal type parameter of a general class, interface, method or constructor element. public VivoTest(int b ) { this.a = b; } }
4, Javapoet generated code
With the above foundation, the process of string splicing to generate java code will not be difficult to understand.
private void generateCodeByJavapoet(String className, List<Element> elements) throws IOException { //Declaration constructor MethodSpec.Builder constructMethodBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(ClassName.bestGuess(className), "activity"); //Add a statement in the constructor for (Element e : elements) { constructMethodBuilder.addStatement("activity." + e.getSimpleName() + "=activity.findViewById(" + e.getAnnotation(BindView.class).value() + ");"); } //Declaration class TypeSpec viewBindingClass = TypeSpec.classBuilder(className + "ViewBinding").addModifiers(Modifier.PUBLIC).addMethod(constructMethodBuilder.build()).build(); String packageName = elementUtils.getPackageOf(elements.get(0)).getQualifiedName().toString(); JavaFile build = JavaFile.builder(packageName, viewBindingClass).build(); build.writeTo(filer); }
It should be mentioned here that now more and more people use Kotlin language to develop app s, and you can even use it https://github.com/square/kotlinpoet To directly generate Kotlin code. If you are interested, you can try.
5, Summary of compile time annotations
The first is the performance that we pay attention to. For runtime annotations, a large number of reflective code will be generated, and the number of reflective calls will become more and more with the increase of project complexity, which is a gradual deterioration process. For compile time annotations, the number of reflective calls is fixed, It will not become worse and worse with the increase of project complexity. In fact, for most projects with runtime annotations, the performance of the framework can be greatly improved through compile time annotations, such as the famous Dagger and EventBus. Their first version is runtime annotations, and subsequent versions are uniformly replaced with compile time annotations.
Secondly, after reviewing the previous development process of compilation annotation, we can draw the following conclusions:
-
Compile time annotations can only generate code, but cannot modify code;
-
The code generated by annotation must be called manually, and it will not be called by itself;
-
For SDK writers, even compile time annotations often have to go through at least one reflection, and the main function of reflection is to call the code generated by your annotation processor.
Some friends may ask that since compile time annotations can only generate code and can't modify code, the role is very limited. Why not directly use bytecode tools such as ASM and Javassist. These tools can not only generate code, but also modify code, with more powerful functions. Because these bytecode tools directly generate class es, and the writing method is complex, error prone, and not easy to debug. It's OK to write things like preventing fast clicks on a small scale. In fact, it's inconvenient to develop a third-party framework on a large scale, which is far less efficient than annotation during compilation.
In addition, think about it again. After the compilation time annotation mentioned above is made into a third-party library for others to use, the user still needs to call the "init" method manually at the right time. However, some excellent third-party libraries do not need the user to call the init method manually, which is very convenient to use, How did this happen? In fact, it is not difficult. In most cases, after these third-party libraries generate code with compile time annotations, they directly help you call the init method with bytecode tools such as ASM, so that you can avoid the process of manual call. The core is still compile time annotation, but one step is omitted with bytecode tool.
Author: vivo Internet client team - Wu Yue