Write at the beginning: While reading some open source libraries recently, I found that most of them use annotations, so I had to take a closer look at the knowledge of annotations under Android
What are annotations
Java.lang. annotation, the interface annotation, was introduced in JDK5.0 and later.
Annotations are special tags in your code that can be read at compile time, class load time, run time, and perform processing accordingly. Using annotations, developers can embed additional information in source files without changing the original logic. Code analysis tools, development tools, and deployment tools can use this supplementary information to validate, process, or deploy.
The Annotation doesn’t run, it just has member variables, it has no methods. Annotations are part of a program element in the same way that modifiers such as public and final are. Annotations cannot be used as a program element.
The role of annotations
Annotations make repetitive tasks automatic, simplifying and automating the process. For example, it is used to generate Java Doc, check the format during compilation, and automatically generate code to improve the quality of software and improve the production efficiency of software.
Common notes
There are roughly four types of annotations that Android has defined, called meta-annotations
@Retention: Defines how long the Annotation is retained
RetentionPoicy.SOURCE
Annotations are only retained in the source file. When a Java file is compiled into a class file, annotations are discarded. It is used to perform some inspection operations, such as@Override
和@SuppressWarnings
RetentionPoicy.CLASS:
Annotations are kept in the class file, but are discarded when the JVM loads the class file, which is the default lifecycle; Used for pre-processing operations at compile time, such as generating auxiliary code (e.gButterKnife
)RetentionPoicy.RUNTIME:
Annotations are not only saved to the class file, but still exist after the JVM loads the class file; Used to dynamically retrieve annotation information at run time. This annotation will be used with reflection
@target: Defines the range of objects that the Annotation decorates
ElementType.CONSTRUCTOR
: describes the constructorElementType.FIELD
: Describes a domainElementType.LOCAL_VARIABLE
: Describes local variablesElementType.METHOD
: Describes a methodElementType.PACKAGE
: Describes packagesElementType.PARAMETER
: Describes parametersElementType.TYPE
: Describes classes, interfaces (including annotation types), or enum declarations
Not marked means can be modified all
@Inherited: Whether a child is allowed to inherit annotations from its parent. The default value is false
Documented Whether @documented will be saved to a Javadoc document
Custom annotations
Run-time annotations and compile-time annotations are the most commonly used custom annotations
Runtime annotations
This is illustrated with a simple example of dynamically bound controls
Start by defining a simple custom annotation,
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value() default -1;
}
Copy the code
We then use reflection to inject the controls from findViewbyId() into the variables we need when the app runs.
public class AnnotationActivity extends AppCompatActivity { @BindView(R.id.annotation_tv) private TextView mTv; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_annotation); getAllAnnotationView(); mTv.setText("Annotation"); } private void getAllAnnotationView() {Field[] fields = this.getClass().getDeclaredFields(); For (Field Field: fields) {try {// Annotations if (field.getannotations ()! = null) {/ / sure annotation types if (field. IsAnnotationPresent (BindView. Class)) {/ / allows field. The modified reflection properties setAccessible (true); BindView bindView = field.getAnnotation(BindView.class); Field. Set (this, findViewById(bindView.value()))); } } } catch (Exception e) { e.printStackTrace(); }}}}Copy the code
And then finally the mTv shows the Annotation text that we want, which looks a little bit like a ButterKnife, but remember that reflection is a performance drain,
So instead of using runtime annotations, our common control-binding library, ButterKnife, uses compile-time annotations.
Compile-time annotations
define
Before we talk about compile-time annotations, let’s mention AbstractProcessor, an annotation processor. It’s a javac tool that scans and handles annotations at compile time. You can customize annotations and register them with the Annotation handler that handles your annotations.
An annotation processor that takes Java code (or compiled bytecode) as input and generates a file (usually a.java file) as output. This.Java code generated by the annotator can be compiled by Javac just like regular.Java.
The import
AbstractProcessor can’t be called directly in Android projects because it is a javac tool. The following provides a viable import method that I try.
File–>New Module–> Java Library Create a New Java Module. Make sure it’s Java Library, not Android Library
You can then use AbstractProcessor in the corresponding library
With all the preparation done, let’s take a look at a simple annotation bound control example
Project directory
-- APP (Main project) -- App_ANNOTATION (Java Module custom annotations) -- Annotation-API (Android Module) -- APP_compiler (Java Module annotations processor logic)Copy the code
Create an annotation under the Annotation Module
@retention (retentionPolicy.class) public @retention (retentionPolicy.class) public @retention (retentionPolicy.class) public @retention (retentionPolicy.class) public @retention (retentionPolicy.class) }Copy the code
Create the annotation processor CustomProcessor under compiler Module
Public class CustomProcessor extends AbstractProcessor {private Filer mFiler; // Private Elements mElements; @override public synchronized void init(ProcessingEnvironment ProcessingEnvironment) { super.init(processingEnvironment); mElements = processingEnvironment.getElementUtils(); mFiler = processingEnvironment.getFiler(); Override public Boolean process(Set<?) @override public Boolean process(Set<? extends TypeElement> set, RoundEnvironment env) { return true; } / / custom annotation Set @ Override public Set < String > getSupportedAnnotationTypes () {Set < String > types = new LinkedHashSet < > (); types.add(BindView.class.getCanonicalName()); return types; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }}Copy the code
The core process function takes two arguments, the second of which we focus on because env represents the set of all annotations
First of all, we will briefly explain the processing process of Porcess
- Iterate over env to get a list of the elements we need
- Encapsulate the list of elements as objects for later processing (as you would normally parse JSON data)
- The JavaPoet library generates Java files from objects in the form we expect
- Iterate over env to get a list of the elements we need
for(Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)){ // todo .... Class if (element.getkind () == elementkind.class) {TypeElement TypeElement = (TypeElement); element; / / output element names System. Out. Println (typeElement. GetSimpleName ()); / / output annotation attribute value System. Out. Println (typeElement. GetAnnotation (BindView. Class). The value ()); }}Copy the code
A list of required annotations can be obtained directly from the getElementsAnnotatedWith function, with some elements added inside the function for easy use
2. Encapsulate the list of elements as objects for later processing
First, we need to make it clear that under this event that binds the control, we need the id of the control.
Create a new class called bindViewField. class to hold the attributes associated with the custom annotation BindView
BindViewField.class
public class BindViewField {
private VariableElement mFieldElement;
private int mResId;
public BindViewField(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(String.format("Only field can be annotated with @%s",
BindView.class.getSimpleName()));
}
mFieldElement = (VariableElement) element;
BindView bindView = mFieldElement.getAnnotation(BindView.class);
mResId = bindView.value();
if (mResId < 0) {
throw new IllegalArgumentException(String.format("value() in %s for field % is not valid",
BindView.class.getSimpleName(), mFieldElement.getSimpleName()));
}
}
public Name getFieldName() {
return mFieldElement.getSimpleName();
}
public int getResId() {
return mResId;
}
public TypeMirror getFieldType() {
return mFieldElement.asType();
}
}
Copy the code
The above BindViewField can only represent one custom Annotation bindView object. A class can have multiple custom annotations, so we need to create an Annotation. Class object to manage the collection of custom annotations.
AnnotatedClass.class
Public class AnnotatedClass {// class public TypeElement mClassElement; Public List<BindViewField> mFiled; Public Elements mElementUtils; public AnnotatedClass(TypeElement classElement, Elements elementUtils) { this.mClassElement = classElement; this.mElementUtils = elementUtils; this.mFiled = new ArrayList<>(); Public void addField(BindViewField field) {mFiled. Add (field); } public String getPackageName(TypeElement Type) {return mElementUtils.getPackageOf(type).getQualifiedName().toString(); } private static String getClassName(TypeElement type, String packageName) { int packageLen = packageName.length() + 1; return type.getQualifiedName().toString().substring(packageLen).replace('.', '$'); }}Copy the code
Give the complete parsing process
Private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>(); @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { mAnnotatedClassMap.clear(); try { processBindView(roundEnvironment); } catch (Exception e) { e.printStackTrace(); return true; } return true; } private void processBindView(RoundEnvironment env) { for (Element element : env.getElementsAnnotatedWith(BindView.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); BindViewField field = new BindViewField(element); annotatedClass.addField(field); System.out.print("p_element=" + element.getSimpleName() + ",p_set=" + element.getModifiers()); } } private AnnotatedClass getAnnotatedClass(Element element) { TypeElement encloseElement = (TypeElement) element.getEnclosingElement(); String fullClassName = encloseElement.getQualifiedName().toString(); AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName); if (annotatedClass == null) { annotatedClass = new AnnotatedClass(encloseElement, mElements); mAnnotatedClassMap.put(fullClassName, annotatedClass); } return annotatedClass; }Copy the code
3. Generate Java files from objects in the desired form through the JavaPoet library
FindViewById () is missing. ButterKnife’s popular library does not omit findViewById(). In the build/generated/source/apt/debug grown into a file, help to performed the findViewById () this behavior.
Similarly, we need to generate a Java file, using the JavaPoet library. Specific usage reference links
Add logic to generate Java files in the Process function
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mAnnotatedClassMap.clear();
try {
processBindView(roundEnvironment);
} catch (Exception e) {
e.printStackTrace();
return true;
}
try {
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
annotatedClass.generateFinder().writeTo(mFiler);
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
Copy the code
The core logic annotatedClass. GenerateFinder (.) writeTo (mFiler); Detailed implementation in AnnotatedClass
Public JavaFile generateFinder() {// Build inject method methodSpec.builder methodBuilder = methodspec.methodBuilder ("inject") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL) .addParameter(TypeName.OBJECT, "source") .addParameter(Utils.FINDER, "finder"); // host. Btn1 =(Button)finder.findView(source,2131427450); $N=($T)finder.findView(source,$L) ---- mFiled) { methodBuilder.addStatement("host.$N=($T)finder.findView(source,$L)", field.getFieldName() , ClassName.get(field.getFieldType()), field.getResId()); } String packageName = getPackageName(mClassElement); String className = getClassName(mClassElement, packageName); ClassName bindClassName = ClassName.get(packageName, className); TypeSpec finderClass = TypeSpec. ClassBuilder (bindclassname.simplename () + "? Injector") .addModifiers(Modifier.PUBLIC) .addSuperinterface(ParameterizedTypeName.get(Utils.INJECTOR, TypeName. Get (mClassElement. AsType ()))) // Inheritance interface.addMethod(methodBuilder.build()).build(); return JavaFile.builder(packageName, finderClass).build(); }Copy the code
At this point, most of the logic has been implemented, and the helper classes used to bind the controls have been generated through JavaPoet, except for the last step, host registration, as ButterKnife, butterknife.bind (this)
Writing the call interface
New under annotation-API
Injector interface Injector
public interface Injector<T> {
void inject(T host, Object source, Finder finder);
}
Copy the code
Host generic interface Finder(later extended to View and Fragment)
public interface Finder {
Context getContext(Object source);
View findView(Object source, int id);
}
Copy the code
The activity implements the ActivityFinder class
public class ActivityFinder implements Finder{ @Override public Context getContext(Object source) { return (Activity) source; } @Override public View findView(Object source, int id) { return ((Activity) (source)).findViewById(id); }}Copy the code
The core implementation class ButterKnife
public class ButterKnife {
private static final ActivityFinder finder = new ActivityFinder();
private static Map<String, Injector> FINDER_MAP = new HashMap<>();
public static void bind(Activity activity) {
bind(activity, activity);
}
private static void bind(Object host, Object source) {
bind(host, source, finder);
}
private static void bind(Object host, Object source, Finder finder) {
String className = host.getClass().getName();
try {
Injector injector = FINDER_MAP.get(className);
if (injector == null) {
Class<?> finderClass = Class.forName(className + "?Injector");
injector = (Injector) finderClass.newInstance();
FINDER_MAP.put(className, injector);
}
injector.inject(host, source, finder);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Copy the code
The main project was downgraded
The corresponding button can be used directly without requiring findViewById()
public class MainActivity extends AppCompatActivity { @BindView(R.id.annotation_tv) public TextView tv1; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); tv1.setText("annotation_demo"); }}Copy the code
A brief introduction to JavaPoet
Several commonly used classes
- MethodSpec represents a constructor or method declaration.
- TypeSpec represents a class, interface, or enumeration declaration.
- FieldSpec represents a member variable, a field declaration.
- JavaFile contains a Java file for a top-level class.
A commonly used placeholder
$L for variable
$S for Strings
$T for Types
$N for Names(method or variable Names we generated ourselves, etc.)
Add the content
The main processing method in the annotation Processor is the process() function, and the important one in the process() function is the RoundEnvironment parameter,
Common usage
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
//todo
}
Copy the code
Get all Element objects through the BindView annotation, and what is that Element?
Element represents a program Element, which can be a package, class, or method. All elements retrieved through annotations are treated as Element types. To be precise, it is a subclass of the Element object.
Element of the subclass
- ExecutableElementA method, constructor, or initializer (static or instance) that represents a class or interface, including annotation type elements.
Corresponding to the annotation@Target(ElementType.METHOD)
and@Target(ElementType.CONSTRUCTOR)
- PackageElementRepresents a package element that provides access to information about a package and its members. Corresponding annotation time
@Target(ElementType.PACKAGE)
- TypeElementRepresents a class or interface program element that provides access to information about a type and its members. The corresponding
@Target(ElementType.TYPE)
Note: An enumeration type is a class, while an annotation type is an interface. - TypeParameterElementA type parameter representing a generic class, interface, method, or constructor element.
The corresponding@Target(ElementType.PARAMETER)
- VariableElementRepresents a field, enum constant, method or constructor parameter, local variable, or exception parameter.
The corresponding@Target(ElementType.LOCAL_VARIABLE)
Different types of elements acquire information in different ways
Modify method annotations and executableElements
When you have an annotation defined as @target (elementType.method), it means that the annotation can only modify methods.
Get some basic information we need
// bindclick. class with @target (ElementType.METHOD) decorates for (Element Element: RoundEnv. GetElementsAnnotatedWith (BindClick. Class)) {/ / Element for direct strong ExecutableElement ExecutableElement = (ExecutableElement) element; / / the corresponding Element, by obtaining TypeElement getEnclosingElement conversion classElement = (TypeElement) Element. GetEnclosingElement (); / / when (ExecutableElement) element was established, using (PackageElement) element. GetEnclosingElement (); Will be an error. / / need to use elementUtils to obtain Elements elementUtils = processingEnv. GetElementUtils (); PackageElement packageElement = elementUtils.getPackageOf(classElement); . / / the class name String fullClassName = classElement getQualifiedName (), toString (); . / / the class name String className = classElement getSimpleName (), toString (); . / / package name String packageName = packageElement getQualifiedName (), toString (); . / / the method name String methodName = executableElement getSimpleName (), toString (); List<? extends VariableElement> methodParameters = executableElement.getParameters(); // List<String> types = new ArrayList<>(); for (VariableElement variableElement : methodParameters) { TypeMirror methodParameterType = variableElement.asType(); if (methodParameterType instanceof TypeVariable) { TypeVariable typeVariable = (TypeVariable) methodParameterType; methodParameterType = typeVariable.getUpperBound(); } / / parameter name String parameterName. = variableElement getSimpleName (). The toString (); / / parameter type String parameteKind = methodParameterType. ToString (); types.add(methodParameterType.toString()); }}Copy the code
Modify attributes, class members’ annotations, and VariableElements
for (Element element : RoundEnv. GetElementsAnnotatedWith (IdProperty. Class)) {/ / ElementType. Strong FIELD annotations can directly turn VariableElement VariableElement variableElement = (VariableElement) element; TypeElement classElement = (TypeElement) element .getEnclosingElement(); PackageElement packageElement = elementUtils.getPackageOf(classElement); . / / the class name String className = classElement getSimpleName (), toString (); . / / package name String packageName = packageElement getQualifiedName (), toString (); . / / a class member name String variableName = variableElement getSimpleName (), toString (); // Class member type TypeMirror TypeMirror = variableElement.astype (); String type = typeMirror.toString(); }Copy the code
Modify class annotations and TypeElements
for (Element element : RoundEnv. GetElementsAnnotatedWith (BindView. Class)) {/ / ElementType. Strong TYPE annotations can directly turn TypeElement TypeElement classElement = (TypeElement) element; PackageElement packageElement = (PackageElement) element .getEnclosingElement(); . / / the class name String fullClassName = classElement getQualifiedName (), toString (); . / / the class name String className = classElement getSimpleName (), toString (); . / / package name String packageName = packageElement getQualifiedName (), toString (); . / / parent class name String superClassName = classElement getSuperclass (), toString (); }Copy the code
The source address
The demo address
reference
Explore annotations in Android quick start annotations
Compile-time annotations for custom annotations
Understand the annotation processor in one hour
Javapoet – Frees you from repetitive boring code
JavaPoet source code cannot be properly imported Modifier class discussion
Android compile time Annotation Framework 5- Syntax explanation