ButterKnife 8.4.0 source code analysis series

ButterKnife 8.4.0 source code analysis (I)

preface

This paper is based on the historical version 8.4.0 of ButterKnife.

ButterKnife uses apt annotation processing tool. Let's take a look at this part before explaining the source code.

Compile time technology (APT Technology)

Before explaining compile time technology, we need to understand the life cycle of the code.

As shown in the figure, the life cycle of code is divided into source code period, compilation period and running period.

  • Source code period when we are writing to The period of the file ending in java.
  • Compile time refers to the process of giving the source code to the compiler to compile into a computer executable file. In Java, that is, the process of compiling java code into class files.
  • Runtime refers to the running process of java code. Such as the period when our app runs in the mobile phone.

APT, short for Annotation Processing Tool, can process annotations during code compilation and generate java files to reduce manual code input. For example, in ButterKnife, we use a custom annotation processor to process the @ BindView annotation and annotation elements, and finally generate xxxactivity $$viewbinder Class file to reduce the input of manual codes such as findViewById.

Java annotation processor

Annotation Processor is a tool of javac, which is used to scan and process annotations at compile time. You can customize the annotation and register the corresponding Annotation Processor. An Annotation Processor takes java code (or compiled bytecode) as input and generates files (usually. Java files) as output. What does this mean? You can generate java code! The generated java code is generated in Java files, but you can't modify existing Java classes, such as adding methods to existing classes. These generated java files are compiled by javac together with other ordinary manually written java source code.

Let's first look at the processor API. Each processor is inherited from AbstractProcessor, as shown below:

public class MyProcessor extends AbstractProcessor {
    private Filer mFiler; //File related auxiliary classes
    private Elements mElementUtils; //Auxiliary class related to element
    private Messager mMessager; //Log related auxiliary classes

    //Used to specify the java version you use. Usually you should return sourceversion latestSupported()
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    //The auxiliary processor class, Filer, and messages can be called here
    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        elementUtils = env.getElementUtils();
        typeUtils = env.getTypeUtils();
        filer = env.getFiler();
    }


    //This method returns the set set of type stirng, which contains the annotations you need to process
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add("com.example.MyAnnotation");
        return annotataions;
    }


   //The core method is to scan for annotations first, and then generate java files
   //There are many knowledge points and details in the design of these two steps.

    @Override
    public boolean process(Set<? extends TypeElement> annoations,
            RoundEnvironment env) {
        return false;
    }
}

Register your processor

To call the processor you write like the jvm, you must first register and let him know. How to let it know? In fact, it is very simple. google provides us with a library, which can be simply annotated.

The first is dependence

compile 'com.google.auto.service:auto-service:1.0-rc2'
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
  //... Omit non critical code
}

Then just add @ AutoService(Processor.class) to your annotation processor

Basic concepts

  • Element s: a tool class used to process Elements

    Let's take a look at the Element class. In the annotation processor, we scan the java source file, and each part of the source code is a specific type of Element. In other words: Element represents the elements in the program, such as package, class and method. Each Element represents a static, language - level structure

    For example:

    package com.example;    // PackageElement package element
    
    public class Foo {        // TypeElement class element
    
        private int a;      // VariableElement variable element
        private Foo other;  // VariableElement variable element
    
        public Foo () {}    // ExecuteableElement method element
    
        public void setA (  // ExecuteableElement method element
                         int newA   // VariableElement variable element
                         ) {}
    }
    

    In one sentence: Element represents the source code, and TypeElement represents the Element type in the source code, such as class. Then, the TypeElement does not contain information about the class. You can get the name of the class from TypeElement, but you can't get the information of the class, such as the parent class. This information can be obtained through TypeMirror. You can call Element Asstype() to get the TypeMirror of an Element.

  • Types: a tool class used to handle TypeMirror

  • Filer: you can use this class to create java file

  • Messager: log related auxiliary class

So far, a basic annotation processor is written.

In the next chapter, let's look at the internal implementation of ButterKnife.

ButterKnife 8.4.0 source code analysis (II)

Analysis of @ BindView annotation processing flow in ButterKnife

schematic diagram

The following is the processing flow of the whole library. You can go back and look at it again after reading the process analysis.

The core of ButterKnife is the ButterKnifeProcessor class.

(1)init method, which is mainly used to obtain some auxiliary classes
    private Filer mFiler; //File related auxiliary classes
    private Elements mElementUtils; //Auxiliary class related to element
    private Messager mMessager; //Log related auxiliary classes

  @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
    }
  }
(2) The getsupportedsourceversion () method is the default method to get the java version you use for this processor
 @Override public SourceVersion getSupportedSourceVersion() {
    return SourceVersion.latestSupported();
  }
(3) The getsupportedannotations method has been introduced to explain which annotations should be processed by the annotation processor. The following are the annotations processed by ButterKnifeProcessor.
  private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }
(4) Next is the process method, which is the core method.
@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
 //1. Find all annotation information, form a BindingClass (what is it? We'll talk about it later) and save it in the map
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

//2. Traverse the generation of map in step 1 Java file (that is, the java file with the class name _viewbindingabove)
  for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingClass bindingClass = entry.getValue();
      JavaFile javaFile = bindingClass.brewJava();
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return true;
  }

Find and parse each annotation - findAndParseTargets

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);
    // Process each element modified by @ BindView annotation
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
    // we don't SuperficialValidation.validateElement(element)
    // so that an unresolved View type can be generated by later processing rounds
    try {
        parseBindView(element, targetClassMap, erasedTargetNames);
     } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
   //....


      // Process each annotation that corresponds to a listener.
    for (Class<? extends Annotation> listener : LISTENERS) {
      findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
    }
   
    return targetClassMap;

}

First, let's take a look at the parameter RoundEnvironment, a tool that can be used by the processor to query annotation information. Of course, it includes all the annotations you registered in getSupportedAnnotationTypes.

We only analyze one representative BindView annotation here. Everything else is the same, even the code is the same.

Then we continue to look at parseBindView(); This method

  private void parseBindView(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

     //1. Check the legitimacy of the user's use
    // Start by verifying common generated code restrictions.
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Identify whether the type of element inherits from View
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
      TypeVariable typeVariable = (TypeVariable) elementType;
      elementType = typeVariable.getUpperBound();
    }
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
      if (elementType.getKind() == TypeKind.ERROR) {
        note(element, "@%s field with unresolved type (%s) "
                + "must elsewhere be generated as a View or interface. (%s.%s)",
            BindView.class.getSimpleName(), elementType, enclosingElement.getQualifiedName(),
            element.getSimpleName());
      } else {
        error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
            BindView.class.getSimpleName(), enclosingElement.getQualifiedName(),
            element.getSimpleName());
        hasError = true;
      }
    }

    // Illegal direct return
    if (hasError) {
      return;
    }


    //2. Get the id value of the BindView annotation on the variable
    int id = element.getAnnotation(BindView.class).value();

    // 3. Get BindingClass. If there is a caching mechanism, create it if there is no one. It will be analyzed in detail below

    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass != null) {
      ViewBindings viewBindings = bindingClass.getViewBinding(getId(id));
      if (viewBindings != null && viewBindings.getFieldBinding() != null) {
        FieldViewBinding existingBinding = viewBindings.getFieldBinding();
        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
            BindView.class.getSimpleName(), id, existingBinding.getName(),
            enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
      }
    } else {
      bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
    }
   //4 generate FieldViewBinding entity 

    String name = element.getSimpleName().toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    FieldViewBinding binding = new FieldViewBinding(name, type, required);
     //5. Add the FieldViewBinding object to the collection of bindingClass member variables (in fact, here is to get ViewBindings through Id first, and then add FieldViewBinding to ViewBindings of bindingClass. Click to see it again)
    bindingClass.addField(getId(id), binding);

    // Add the type-erased version to the valid binding         targets set.
    erasedTargetNames.add(enclosingElement);
  }

element.getEnclosingElement(); What is it? Is the parent node. That's what we said above. In fact, it is to obtain the class where the @ BindView annotation is located.

The basic steps are the above five steps

1. Check the legitimacy of users' use

 boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);
 private boolean isInaccessibleViaGeneratedCode(Class<? extends Annotation> annotationClass,
      String targetThing, Element element) {
    boolean hasError = false;
    // The parent node is the class of @ BindView annotation
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

 //Judge the modifier. If it contains private or static, an exception will be thrown.
    // Verify method modifiers.
    Set<Modifier> modifiers = element.getModifiers();
    if (modifiers.contains(PRIVATE) || modifiers.contains(STATIC)) {
      error(element, "@%s %s must not be private or static. (%s.%s)",
          annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

        //Judge whether the parent node is of class type. If not, an exception will be thrown
        //In other words, the use of BindView must be in a class
    // Verify containing type.
    if (enclosingElement.getKind() != CLASS) {
      error(enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
          annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

     //Judge whether the parent node is a private class, and throw an exception
    // Verify containing class visibility is not private.
    if (enclosingElement.getModifiers().contains(PRIVATE)) {
      error(enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
          annotationClass.getSimpleName(), targetThing, enclosingElement.getQualifiedName(),
          element.getSimpleName());
      hasError = true;
    }

    return hasError;
  }

Comments are encountered in the above code, which means that we can't use the bindview annotation when using it

Class cannot be private Modifier, which can be default or public 
  //in adapter
   private  static final class ViewHolder {
   //....
   }
   //Member variables cannot be private modifiers, but can be default or public 
   @BindView(R.id.word)
  private  TextView word; 

Next, there is another method, isBindingInWrongPackage.
The name just came out. It's probably that it can't be used in the sdk of android and java. If your package name starts with android or java, an exception will be thrown.

 private boolean isBindingInWrongPackage(Class<? extends Annotation> annotationClass,
      Element element) {
      // Get parent node
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    String qualifiedName = enclosingElement.getQualifiedName().toString();

    if (qualifiedName.startsWith("android.")) {
      error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
          annotationClass.getSimpleName(), qualifiedName);
      return true;
    }
    if (qualifiedName.startsWith("java.")) {
      error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
          annotationClass.getSimpleName(), qualifiedName);
      return true;
    }

    return false;
  } 

2. Get the id value of the BindView annotation on the variable

such as

@BindView(R.id.tvTitle)
TextView title;

The value of title.tv id is obtained here.

Well, that's the end of this chapter. Next, let's look at the next three steps: 3, 4 and 5.

The contents of the remaining chapters (III), (IV) and (V) will place a link checkout it out on my github home page:
https://github.com/tomridder/ButterKnife-8.4.0-

Keywords: Java Android source code

Added by Txtlocal on Fri, 04 Feb 2022 14:41:07 +0200