Runtime annotation is mainly implemented by reflection, while compile time annotation helps us to generate code during compilation, so compile time annotation is efficient, but it is a little more complex to implement. Runtime annotation is inefficient, but it is simple to implement.
Let's first look at how runtime annotations are implemented.
1. Runtime notes
1.1 definition notes
First, two runtime annotations are defined, in which Retention indicates that the annotation takes effect at runtime, and Target indicates the program element scope of the annotation. The following two examples, RuntimeBindView, are used to describe member variables and classes. Member variables are bound to view, and classes are bound to layout. RuntimeBindClick is used to describe methods, and the specified view is bound to the click event.
@Retention(RetentionPolicy.RUNTIME)//Effective at runtime @Target({ElementType.FIELD,ElementType.TYPE})//Describe variables and classes public @interface RuntimeBindView { int value() default View.NO_ID; } @Retention(RetentionPolicy.RUNTIME)//Effective at runtime @Target(ElementType.METHOD)//description method public @interface RuntimeBindClick { int[] value(); }
1.2 reflection implementation
The following code is an annotation function implemented by reflection, in which ClassInfo is a tool class that can parse various members and methods of the class,
Source code: https://github.com/huangbei1990/HDemo/blob/master/hutils/src/main/java/com/android/hutils/reflect/ClassInfo.java
In fact, the logic is very simple, that is, take out the specified annotation from the activity, and then call the corresponding method, such as take out the annotation of the RuntimeBindView description class, then get the return value of the annotation, and then call the setContentView of activity to set the layout id.
public static void bindId(Activity obj){ ClassInfo clsInfo = new ClassInfo(obj.getClass()); //Processing class if(obj.getClass().isAnnotationPresent(RuntimeBindView.class)) { RuntimeBindView bindView = (RuntimeBindView)clsInfo.getClassAnnotation(RuntimeBindView.class); int id = bindView.value(); clsInfo.executeMethod(clsInfo.getMethod("setContentView",int.class),obj,id); } //Process class members for(Field field : clsInfo.getFields()){ if(field.isAnnotationPresent(RuntimeBindView.class)){ RuntimeBindView bindView = field.getAnnotation(RuntimeBindView.class); int id = bindView.value(); Object view = clsInfo.executeMethod(clsInfo.getMethod("findViewById",int.class),obj,id); clsInfo.setField(field,obj,view); } } //Handle click events for (Method method : clsInfo.getMethods()) { if (method.isAnnotationPresent(RuntimeBindClick.class)) { int[] values = method.getAnnotation(RuntimeBindClick.class).value(); for (int id : values) { View view = (View) clsInfo.executeMethod(clsInfo.getMethod("findViewById", int.class), obj, id); view.setOnClickListener(v -> { try { method.invoke(obj, v); } catch (Exception e) { e.printStackTrace(); } }); } } } }
1.3 use
As shown below, write the annotations we have defined to the corresponding locations, and then call the bind function of BindApi. Very simple.
@RuntimeBindView(R.layout.first)//class public class MainActivity extends AppCompatActivity { @RuntimeBindView(R.id.jump)//member public Button jump; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); BindApi.bindId(this);//Call reflection } @RuntimeBindClick({R.id.jump,R.id.jump2})//Method public void onClick(View view){ Intent intent = new Intent(this,SecondActivity.class); startActivity(intent); } }
2. Compile time comments
Compile time annotation is to help you generate code automatically during compilation. In fact, the principle is not difficult.
2.1 definition notes
We can see that the Retention value is different when the compile time annotation is defined from the runtime annotation.
@Retention(RetentionPolicy.CLASS)//Effective at compile time @Target({ElementType.FIELD,ElementType.TYPE})//Describe variables and classes public @interface CompilerBindView { int value() default -1; } @Retention(RetentionPolicy.CLASS)//Effective at compile time @Target(ElementType.METHOD)//description method public @interface CompilerBindClick { int[] value(); }
2.2 generate code based on comments
1) Preparations
First, we need to create a new java lib library, because we need to inherit the AbstractProcessor class, which is not in Android.
Then we need to introduce two packages. Javapot is the package to help us generate code, and auto service is the package to help us automatically generate META-INF and other information, so that when we compile, we can execute our custom processor.
apply plugin: 'java-library' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) api 'com.squareup:javapoet:1.9.0' api 'com.google.auto.service:auto-service:1.0-rc2' } sourceCompatibility = "1.8" targetCompatibility = "1.8"
2) Inherit AbstractProcessor
As shown below, we need to customize a class to inherit the AbstractProcessor and copy its methods, and annotate it with AutoService.
ClassElementsInfo is a class used to store class information. This step will not be taken care of for the time being. The next step will be explained in detail.
In fact, we can see what it means from the name of the function. init initialization, getSupportedSourceVersion limit the supported jdk version, getSupportedAnnotationTypes need to process annotations. In this function, we can get the classes that we need to process annotations, and generate the corresponding code.
@AutoService(Processor.class) public class CompilerBindProcessor extends AbstractProcessor{ private Filer mFileUtils;//File related auxiliary class, responsible for generating java code private Elements mElementUtils;//Auxiliary class related to element to obtain information related to element private Map<String,ClassElementsInfo> classElementsInfoMap; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); mFileUtils = processingEnvironment.getFiler(); mElementUtils = processingEnvironment.getElementUtils(); classElementsInfoMap = new HashMap<>(); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> set = new LinkedHashSet<>(); set.add(CompilerBindClick.class.getCanonicalName()); set.add(CompilerBindView.class.getCanonicalName()); return set; } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { classElementsInfoMap.clear(); //1. Collect required information collection(roundEnvironment); //2. Generate specific code generateClass(); return true; }
3) Collect comments
First, let's take a look at the ClassElementsInfo class, which is the information we need to collect.
TypeElement is a class element, VariableElement is a member element, ExecutableElement is a method element, from which we can get various annotation information.
classSuffix is the prefix. For example, the original class is MainActivity, and the class name generated by annotation is MainActivity+classSuffix
public class ClassElementsInfo { //class public TypeElement mTypeElement; public int value; public String packageName; //Member, key is id public Map<Integer,VariableElement> mVariableElements = new HashMap<>(); //Method, key is id public Map<Integer,ExecutableElement> mExecutableElements = new HashMap<>(); //Suffix public static final String classSuffix = "proxy"; public String getProxyClassFullName() { return mTypeElement.getQualifiedName().toString() + classSuffix; } public String getClassName() { return mTypeElement.getSimpleName().toString() + classSuffix; } ...... }
Then we can start to collect annotation information,
As shown below, according to the collection of annotation types one by one, you can get annotation elements through the roundenvironment.getelementsnannotatedwith function, and then fill them into ClassElementsInfo according to the annotation element types.
Where ClassElementsInfo is stored in the Map, key is String and classPath.
private void collection(RoundEnvironment roundEnvironment){ //1. Collect compileBindView annotations Set<? extends Element> set = roundEnvironment.getElementsAnnotatedWith(CompilerBindView.class); for(Element element : set){ //1.1 notes of collection class if(element.getKind() == ElementKind.CLASS){ TypeElement typeElement = (TypeElement)element; String classPath = typeElement.getQualifiedName().toString(); String className = typeElement.getSimpleName().toString(); String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString(); CompilerBindView bindView = element.getAnnotation(CompilerBindView.class); if(bindView != null){ ClassElementsInfo info = classElementsInfoMap.get(classPath); if(info == null){ info = new ClassElementsInfo(); classElementsInfoMap.put(classPath,info); } info.packageName = packageName; info.value = bindView.value(); info.mTypeElement = typeElement; } } //1.2 collect member's comments else if(element.getKind() == ElementKind.FIELD){ VariableElement variableElement = (VariableElement) element; String classPath = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString(); CompilerBindView bindView = variableElement.getAnnotation(CompilerBindView.class); if(bindView != null){ ClassElementsInfo info = classElementsInfoMap.get(classPath); if(info == null){ info = new ClassElementsInfo(); classElementsInfoMap.put(classPath,info); } info.mVariableElements.put(bindView.value(),variableElement); } } } //2. Collect compileBindClick comments Set<? extends Element> set1 = roundEnvironment.getElementsAnnotatedWith(CompilerBindClick.class); for(Element element : set1){ if(element.getKind() == ElementKind.METHOD){ ExecutableElement executableElement = (ExecutableElement) element; String classPath = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString(); CompilerBindClick bindClick = executableElement.getAnnotation(CompilerBindClick.class); if(bindClick != null){ ClassElementsInfo info = classElementsInfoMap.get(classPath); if(info == null){ info = new ClassElementsInfo(); classElementsInfoMap.put(classPath,info); } int[] values = bindClick.value(); for(int value : values) { info.mExecutableElements.put(value,executableElement); } } } } }
4) Generate code
As shown below, using java pot to generate code is not complicated.
public class ClassElementsInfo { ...... public String generateJavaCode() { ClassName viewClass = ClassName.get("android.view","View"); ClassName clickClass = ClassName.get("android.view","View.OnClickListener"); ClassName keepClass = ClassName.get("android.support.annotation","Keep"); ClassName typeClass = ClassName.get(mTypeElement.getQualifiedName().toString().replace("."+mTypeElement.getSimpleName().toString(),""),mTypeElement.getSimpleName().toString()); //Construction method MethodSpec.Builder builder = MethodSpec.constructorBuilder() .addModifiers(Modifier.PUBLIC) .addParameter(typeClass,"host",Modifier.FINAL); if(value > 0){ builder.addStatement("host.setContentView($L)",value); } //member Iterator<Map.Entry<Integer,VariableElement>> iterator = mVariableElements.entrySet().iterator(); while(iterator.hasNext()){ Map.Entry<Integer,VariableElement> entry = iterator.next(); Integer key = entry.getKey(); VariableElement value = entry.getValue(); String name = value.getSimpleName().toString(); String type = value.asType().toString(); builder.addStatement("host.$L=($L)host.findViewById($L)",name,type,key); } //Method Iterator<Map.Entry<Integer,ExecutableElement>> iterator1 = mExecutableElements.entrySet().iterator(); while(iterator1.hasNext()){ Map.Entry<Integer,ExecutableElement> entry = iterator1.next(); Integer key = entry.getKey(); ExecutableElement value = entry.getValue(); String name = value.getSimpleName().toString(); MethodSpec onClick = MethodSpec.methodBuilder("onClick") .addAnnotation(Override.class) .addModifiers(Modifier.PUBLIC) .addParameter(viewClass,"view") .addStatement("host.$L(host.findViewById($L))",value.getSimpleName().toString(),key) .returns(void.class) .build(); //Construct anonymous inner class TypeSpec clickListener = TypeSpec.anonymousClassBuilder("") .addSuperinterface(clickClass) .addMethod(onClick) .build(); builder.addStatement("host.findViewById($L).setOnClickListener($L)",key,clickListener); } TypeSpec typeSpec = TypeSpec.classBuilder(getClassName()) .addModifiers(Modifier.PUBLIC) .addAnnotation(keepClass) .addMethod(builder.build()) .build(); JavaFile javaFile = JavaFile.builder(packageName,typeSpec).build(); return javaFile.toString(); } }
The code generated after the annotation is finally used is as follows
package com.android.hdemo; import android.support.annotation.Keep; import android.view.View; import android.view.View.OnClickListener; import java.lang.Override; @Keep public class MainActivityproxy { public MainActivityproxy(final MainActivity host) { host.setContentView(2131296284); host.jump=(android.widget.Button)host.findViewById(2131165257); host.findViewById(2131165258).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { host.onClick(host.findViewById(2131165258)); } }); host.findViewById(2131165257).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { host.onClick(host.findViewById(2131165257)); } }); } }
5) Make comments effective
After we generate the code, we need to let the original class call the generated code
public class BindHelper { static final Map<Class<?>,Constructor<?>> Bindings = new HashMap<>(); public static void inject(Activity activity){ String classFullName = activity.getClass().getName() + ClassElementsInfo.classSuffix; try{ Constructor constructor = Bindings.get(activity.getClass()); if(constructor == null){ Class proxy = Class.forName(classFullName); constructor = proxy.getDeclaredConstructor(activity.getClass()); Bindings.put(activity.getClass(),constructor); } constructor.setAccessible(true); constructor.newInstance(activity); }catch (Exception e){ e.printStackTrace(); } } }
2.3 debugging
First, add the following code to gradle.properties
android.enableSeparateAnnotationProcessing = true org.gradle.daemon=true org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8888
Then click Edit Configurations
Create a new remote
Then fill in the relevant parameters. 127.0.0.1 indicates the local machine, port is consistent with the one just filled in gradle.properties, and then click ok
Then adjust the Select Run/Debug Configuration option to the newly created Configuration, and click Build – Rebuild Project to start debugging.
2.4 use
The original class is shown below
@CompilerBindView(R.layout.first) public class MainActivity extends AppCompatActivity { @CompilerBindView(R.id.jump) public Button jump; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); BindHelper.inject(this); } @CompilerBindClick({R.id.jump,R.id.jump2}) public void onClick(View view){ Intent intent = new Intent(this,SecondActivity.class); startActivity(intent); } }
The following are the generated classes
package com.android.hdemo; import android.support.annotation.Keep; import android.view.View; import android.view.View.OnClickListener; import java.lang.Override; @Keep public class MainActivityproxy { public MainActivityproxy(final MainActivity host) { host.setContentView(2131296284); host.jump=(android.widget.Button)host.findViewById(2131165257); host.findViewById(2131165258).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { host.onClick(host.findViewById(2131165258)); } }); host.findViewById(2131165257).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { host.onClick(host.findViewById(2131165257)); } }); } }
3. summary
The annotation framework looks very tall, but it's not difficult to understand. It's all a routine.