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 despondent. If you give yourself positive suggestions, you will also become more 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: This refers to the process where the compiler translates source code into machine-readable code. In Java, it means compiling Java source code into bytecode files recognized by the JVM.
  2. Runtime: This 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 source code
    CLASS,   // Default strategy, discarded at runtime, exists only in class files
    RUNTIME  // Annotation information is recorded in class files at compile-time, retained at runtime, can be accessed via reflection
}

Besides the differences mentioned above, compile-time and runtime annotations are implemented differently. 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 during compilation. Compile-time annotations are generated through APT using annotation information to fulfill certain functions, with typical examples being ButterKnife, Dagger, and ARouter.

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 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 generated.

Element and Elements#

Element is similar to a tag 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() methods 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 ButterKnife's 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 relevant 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'
// Used 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 Element of specified Class type
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Bind.class);
        // Iterate and store the matching Elements
        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 doesn't create new objects every time. Although it also uses reflection, the testing of reflection is significantly reduced compared to runtime annotations, so its performance is better than that of runtime annotations. This is also a distinction between compile-time 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 and provides another avenue for solving problems.

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