banner
jzman

jzman

Coding、思考、自觉。
github

Detailed Explanation of Compile-Time Annotations and Implementation of ButterKnife

PS: Humans are creatures that are very receptive to self-suggestion. If you give yourself negative suggestions, you can easily become dejected; if you give yourself positive suggestions, you can also become positive.

Today, let's take a look at the knowledge related to compile-time annotations. I believe that after manual practice, you will find it easier to understand frameworks like Dagger, ARouter, and ButterKnife that use compile-time annotations, as well as their internal source code implementations. The content is as follows:

  1. Compile-time and runtime annotations
  2. Annotation Processing Tool (APT)
  3. AbstractProcessor
  4. Element and Elements
  5. Custom annotation processors
  6. Using custom annotation processors

Compile-time and Runtime Annotations#

First, let's understand the difference between compile-time and runtime:

  1. Compile-time: Refers to the process where the compiler translates source code into machine-readable code, which in Java means compiling Java source code into bytecode files recognized by the JVM.
  2. Runtime: Refers to the process where the JVM allocates memory and interprets the bytecode files.

The meta-annotation @Retention determines whether the annotation is available at compile-time or runtime, with the following configurable strategies:

public enum RetentionPolicy {
    SOURCE,  // Discarded at compile-time, exists only in the source code
    CLASS,   // Default strategy, discarded at runtime, exists only in the class file
    RUNTIME  // Annotation information is recorded in the class file at compile-time, retained at runtime, can be accessed via reflection
}

Besides the above differences, compile-time annotations and runtime annotations also differ in implementation. Compile-time annotations are generally implemented through annotation processors (APT), while runtime annotations are typically implemented through reflection.

For more information on annotations and reflection, you can refer to the following two articles:

What is APT#

APT (Annotation Processing Tool) is a tool provided by javac that can process annotations, used to scan and process annotations at compile-time. In simple terms, APT allows you to obtain information about annotations and their locations, which can be used to generate code in the compiler. Compile-time annotations are generated through APT to complete certain functionalities, with typical representatives being ButterKnife, Dagger, ARouter, etc.

AbstractProcessor#

AbstractProcessor implements Processor and is the abstract class for annotation processors. To implement an annotation processor, you need to extend AbstractProcessor. The main method descriptions are as follows:

  • init: Initializes the Processor, from which you can obtain utility classes like Elements, Types, Filer, and Messager from the ProcessingEnvironment parameter;
  • getSupportedSourceVersion: Returns the Java version being used;
  • getSupportedAnnotationTypes: Returns the names of all annotations to be processed;
  • process: Obtains all specified annotations for processing;

The process method may be executed multiple times during runtime until no other classes are produced.

Element and Elements#

Element is similar to tags in XML. In Java, Element represents program elements such as classes, members, methods, etc. Each Element represents a specific structure. For operations on Element objects, please use visitor or getKind() method for judgment and specific processing. The subclasses of Element are as follows:

  • ExecutableElement
  • PackageElement
  • Parameterizable
  • QualifiedNameable
  • TypeElement
  • TypeParameterElement
  • VariableElement

The above element structures correspond to the following code structure:

// PackageElement
package manu.com.compiler;

// TypeElement
public class ElementSample {
    // VariableElement
    private int a;
    // VariableElement
    private Object other;

    // ExecutableElement
    public ElementSample() {
    }
    // Method parameter VariableElement
    public void setA(int newA) {
    }
    // TypeParameterElement represents parameterized types used in generic parameters
}

Elements is a utility class for processing Element, providing only interfaces, with specific implementations in the Java platform.

Custom Compile-time Annotation Processor#

Below is an implementation of an annotation @Bind using APT to mimic the ButterKnife annotation @BindView. The project structure is as follows:

image

In the API module, define the annotation @Bind as follows:

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

The compiler module is a Java module that introduces Google's auto-service to generate related files under META-INFO. Javapoet is used for easier creation of Java files. The custom annotation processor BindProcessor is as follows:

// Used to generate META-INF/services/javax.annotation.processing.Processor file
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
// For creating Java files
implementation 'com.squareup:javapoet:1.12.1'
/**
 * BindProcessor
 */
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {
    private Elements mElements;
    private Filer mFiler;
    private Messager mMessager;

    // Stores the corresponding BindModel for a certain class
    private Map<TypeElement, List<BindModel>> mTypeElementMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
        print("init");
        // Initialize Processor

        mElements = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        print("getSupportedSourceVersion");
        // Return the Java version being used
        return SourceVersion.RELEASE_8;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        print("getSupportedAnnotationTypes");
        // Return the names of all annotations to be processed
        Set<String> set = new HashSet<>();
        set.add(Bind.class.getCanonicalName());
        set.add(OnClick.class.getCanonicalName());
        return set;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        print("process");
        mTypeElementMap.clear();
        // Get the Element of the specified Class type
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Bind.class);
        // Iterate and store the Elements that meet the conditions
        for (Element element : elements) {
            // Get the fully qualified class name of the Element
            TypeElement typeElement = (TypeElement) element.getEnclosingElement();
            print("process typeElement name:"+typeElement.getSimpleName());
            List<BindModel> modelList = mTypeElementMap.get(typeElement);
            if (modelList == null) {
                modelList = new ArrayList<>();
            }
            modelList.add(new BindModel(element));
            mTypeElementMap.put(typeElement, modelList);
        }

        print("process mTypeElementMap size:" + mTypeElementMap.size());

        // Java file generation
        mTypeElementMap.forEach((typeElement, bindModels) -> {
            print("process bindModels size:" + bindModels.size());
            // Get package name
            String packageName = mElements.getPackageOf(typeElement).getQualifiedName().toString();
            // Generate the file name for the Java file
            String className = typeElement.getSimpleName().toString();
            String newClassName = className + "_ViewBind";

            // MethodSpec
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.bestGuess(className), "target");
            bindModels.forEach(model -> {
                constructorBuilder.addStatement("target.$L=($L)target.findViewById($L)",
                        model.getViewFieldName(), model.getViewFieldType(), model.getResId());
            });
            // typeSpec
            TypeSpec typeSpec = TypeSpec.classBuilder(newClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(constructorBuilder.build())
                    .build();
            // JavaFile
            JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
                    .addFileComment("AUTO Create")
                    .build();

            try {
                javaFile.writeTo(mFiler);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });

        return true;
    }

    private void print(String message) {
        if (mMessager == null) return;
        mMessager.printMessage(Diagnostic.Kind.NOTE, message);
    }
}

The BindModel is a simple encapsulation of the annotation @Bind information, as follows:

/**
 * BindModel
 */
public class BindModel {
    // Member variable Element
    private VariableElement mViewFieldElement;
    // Member variable type
    private TypeMirror mViewFieldType;
    // View resource Id
    private int mResId;

    public BindModel(Element element) {
        // Validate if the Element is a member variable
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException("element is not FIELD");
        }
        // Member variable Element
        mViewFieldElement = (VariableElement) element;
        // Member variable type
        mViewFieldType = element.asType();
        // Get the value of the annotation
        Bind bind = mViewFieldElement.getAnnotation(Bind.class);
        mResId = bind.value();
    }

    public int getResId(){
        return mResId;
    }

    public String getViewFieldName(){
        return mViewFieldElement.getSimpleName().toString();
    }

    public TypeMirror getViewFieldType(){
        return mViewFieldType;
    }
}

In the bind module, create the file to be generated:

/**
 * Initialization
 */
public class BindKnife {
    public static void bind(Activity activity) {
        // Get the fully qualified class name of the activity
        String name = activity.getClass().getName();
        try {
            // Reflectively create and inject Activity
            Class<?> clazz = Class.forName(name + "_ViewBind");
            clazz.getConstructor(activity.getClass()).newInstance(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Of course, ButterKnife should cache the created objects, so it won't create new objects every time. Although it also uses reflection, the testing of reflection is significantly reduced compared to runtime annotations, so the performance is better than that of runtime annotations. This is also a distinction between compile-time annotations and runtime annotations.

Using Custom Annotation Processors#

The usage is similar to ButterKnife, as follows:

public class MainActivity extends AppCompatActivity{
   
    @Bind(R.id.tvData)
    TextView tvData;

    @Override
     public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindKnife.bind(this);
        tvData.setText("data");
    }
}

Understanding compile-time annotations may not lead you to immediately reinvent the wheel, but it is very helpful when looking at other frameworks, providing an additional avenue for problem-solving.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.