An article teaches you how to insert stubs during Android compilation so that programs can learn to write their own code.

Preface

In recent years, compile-time piling technology has become more and more popular in Android circle. Whether it's ButterKnief, Dagger, VirtualAPK that can generate JAVA source code, or even Kotlin, a new language, all use compile-time piling technology. Learning this technology is very helpful for us to understand the principles of these frameworks. In addition, we can extract complex and repetitive code through this technology, reduce program coupling, improve code reusability and improve development efficiency. Therefore, it is necessary to understand the pile insertion technology at compile time. Before introducing this technology, let's look at the compilation process of Android code and the location of the stuffing. Don't say much. Go straight to the picture.

 

APT

APT(Annotation Processing Tool) is a compile-time annotation processor. It implements code generation at compile time by defining annotations and processors, and compiles the generated code with source code into. class files.

Representative Framework: ButterKnife, Dagger, ARouter, EventBus3, Data Binding, Android Annotation, etc.

Before introducing how to apply APT technology, let's learn something about it.

Element

1. Introduction

Element is a type that describes the static structure of. java files at compile time. It may represent a package, a class, a method, or a field. Element comparisons should use equals because the same Element may be represented by two objects during compilation. JDK provides the following five elements.

 

2.Element storage structure

The compiler uses a Dom tree similar to Html to store Element s. Let's specify it in Test.java below.

//PackageElement
package me.zhangkuo.compile;

//TypeElement
public class Test {

    //VariableElement
    private String name;

    //ExecutableElement
    private Test(){
    }

    //ExecutableElement
    public void setName(/* TypeParameterElement */ String name) {
        this.name = name;
    }
}

Test.java is described in Element tree structure as follows:

 

We can see that there is no child node TypeParameterElement in the Executable Element of setName(String name). This is because TypeParameterElement is not included in the Element tree. However, we can get it through the getTypeParameters() method of Executable Element.

In addition, I would like to introduce some useful methods in the two Element s.

public interface Element extends AnnotatedConstruct {
    //Get the parent Element
    Element getEnclosingElement();
    //Get the collection of sub-Element s
    List<? extends Element> getEnclosedElements();
}

TypeMirror

Element has an asType() method to return TypeMirror. TypeMirror represents the type in the Java programming language. These types include basic types, declarative types (class and interface types), array types, type variables, and null types. It can also represent wildcard type parameters, executable signatures and return types, and pseudotypes corresponding to packages and keywords void. We usually use TypeMirror for type judgment. The following code compares whether the type described by the element is a subclass of Activity.

/**
 * Type-related tool classes
 */
private Types typeUtils;
/**
 * Element-related tool classes
 */
private Elements elementUtils;
private static final String ACTIVITY_TYPE = "android.app.Activity";

private boolean isSubActivity(Element element){
    //Get the TypeMirror of the current element
    TypeMirror elementTypeMirror = element.asType();
    //Get Elements of Activity through Tool Class Elements and convert them to TypeMirror
    TypeMirror viewTypeMirror = elementUtils.getTypeElement(ACTIVITY_TYPE).asType();
    //Judging the relationship between the two by using the tool class type Utils
    return typeUtils.isSubtype(elementTypeMirror,viewTypeMirror)
}

3. A Simple Butter Knife

This section describes how to write an APT framework by writing a simple ButterKnife. APT should be the simplest technique for compile-time piling, which can be accomplished in three steps.

1. Define compile-time annotations.

We added a new Java Library Module named apt_api to write the annotation class BindView.

@Retention(RetentionPolicy.Class)
@Target(ElementType.FIELD)
public @interface BindView {
}

Here's a brief introduction to Retention Policy. RetentionPolicy is an enumeration with three values: SOURCE, CLASS, and RUNTIME.

  • SOURCE: Not involved in compilation, for developers to use.
  • CLASS: Participates in compilation and is not visible at runtime. Use it for the compiler.
  • RUNTIME: Participates in compilation, visible at runtime. For compilers and JVM s.

2. Define Annotation Processor.

Similarly, we need to add a new Java Library Module named apt_processor.

We need to introduce two necessary dependencies: one is our new module apt_annotation, the other is Google's com.google.auto.service:auto-service:1.0-rc3 (hereinafter referred to as auto-service).

implementation project(':apt_api')
api 'com.google.auto.service:auto-service:1.0-rc3'

Add a new class ButterKnifeProcessor, inheriting AbstractProcessor.

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {   
    /**
     * Element-related tool classes
     */
    private Elements elementUtils;
    /**
     * File-related tool classes
     */
    private Filer filer;
    /**
     * Log-related tool classes
     */
    private Messager messager;
    /**
     * Type-related tool classes
     */
    private Types typeUtils;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(BindView.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_7;
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        typeUtils = processingEnv.getTypeUtils();
    }

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

Auto-service simplifies the process of defining annotation processors for us. @ AutoService is provided by auto-service to tell the compiler that the ButterKnife Processor we defined is a compile-time annotation processor. ButterKnifeProcessor is then called at compile time.

We also rewrote four methods provided by AbstractProcessor: getSupported Annotation Types, getSupported Source Version, init, process.

  • GetSupported Annotation Types indicate which annotations the processor can process. Here we return the BindView we defined earlier. In addition to rewriting methods, annotations can also be used.

    @SupportedAnnotationTypes(value = {"me.zhangkuo.apt.annotation.BindView"})
    
  • GetSupported Source Version represents the Java version that the processor can handle. Here we can use the latest version of JDK. Similarly, we can do this through annotations.

    @SupportedSourceVersion(value = SourceVersion.latestSupported())
    
  • The init method is mainly used for some preparatory work. We usually initialize several tool classes here. In this code, we start with the element-related tool class elementUtils, the log-related tool class messager, the file-related file, and the type-related tool class typeUtils. We will see that process generates code mainly through these classes.

  • Process is used to complete the specific function of program writing code. Before I go into process, let me recommend a library: javapoet . javapoet is open source by the magic square company, which provides a very human api to help developers generate. java source files. Its README.md file provides us with a wealth of examples and is our main learning tool.

    private Map<TypeElement, List<Element>> elementPackage = new HashMap<>();    
    private static final String VIEW_TYPE = "android.view.View";
    private static final String VIEW_BINDER = "me.zhangkuo.apt.ViewBinding";
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            if (set == null || set.isEmpty()) {
                return false;
            }
            elementPackage.clear();
            Set<? extends Element> bindViewElement = roundEnvironment.getElementsAnnotatedWith(BindView.class);
            //Collect data into elementPackage
            collectData(bindViewElement);
            //Generate. java code from data in elementPackage
            generateCode();
            return true;
        }
    
        private void collectData(Set<? extends Element> elements){
            Iterator<? extends Element> iterable = elements.iterator();
            while (iterable.hasNext()) {
                Element element = iterable.next();
                TypeMirror elementTypeMirror = element.asType();
                //Determine whether the type of element is View or a sub-type of View.
                TypeMirror viewTypeMirror = elementUtils.getTypeElement(VIEW_TYPE).asType();
                if (typeUtils.isSubtype(elementTypeMirror, viewTypeMirror) || typeUtils.isSameType(elementTypeMirror, viewTypeMirror)) {
                    //Find the parent element, which is considered the class where the @BindView tag field is located.
                    TypeElement parent = (TypeElement) element.getEnclosingElement();
                    //List s stored differently according to parent
                    List<Element> parentElements = elementPackage.get(parent);
                    if (parentElements == null) {
                        parentElements = new ArrayList<>();
                        elementPackage.put(parent, parentElements);
                    }
                    parentElements.add(element);
                }else{
                    throw new RuntimeException("Error handling, BindView The type should be marked as View On the field of ____________");
                }
            }
        }
    
        private void generateCode(){
            Set<Map.Entry<TypeElement,List<Element>>> entries = elementPackage.entrySet();
            Iterator<Map.Entry<TypeElement,List<Element>>> iterator = entries.iterator();
            while (iterator.hasNext()){
                Map.Entry<TypeElement,List<Element>> entry = iterator.next();
                //Class element
                TypeElement parent = entry.getKey();
                //Under the current class element, the BindView element is annotated
                List<Element> elements = entry.getValue();
                //Generating MethodSpec for bindView through JavaPoet
                MethodSpec methodSpec = generateBindViewMethod(parent,elements);
    
                String packageName = getPackage(parent).getQualifiedName().toString();
                ClassName viewBinderInterface = ClassName.get(elementUtils.getTypeElement(VIEW_BINDER));
                String className = parent.getQualifiedName().toString().substring(
                        packageName.length() + 1).replace('.', '$');
                ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
    
                try {
                  //Generate the className_ViewBinding.java file
                    JavaFile.builder(packageName, TypeSpec.classBuilder(bindingClassName)
                            .addModifiers(PUBLIC)
                            .addSuperinterface(viewBinderInterface)
                            .addMethod(methodSpec)
                            .build()
                    ).build().writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private MethodSpec generateBindViewMethod(TypeElement parent,List<Element> elementList) {
            ParameterSpec.Builder parameter = ParameterSpec.builder(TypeName.OBJECT, "target");
            MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder("bindView");
            bindViewMethod.addParameter(parameter.build());
            bindViewMethod.addModifiers(Modifier.PUBLIC);
            bindViewMethod.addStatement("$T temp = ($T)target",parent,parent);
            for (Element element :
                    elementList) {
                int id = element.getAnnotation(BindView.class).value();
                bindViewMethod.addStatement("temp.$N = temp.findViewById($L)", element.getSimpleName().toString(), id);
            }
    
            return bindViewMethod.build();
        }
    

    The code of process is relatively long, but its logic is very simple. It is mainly divided into two parts: collecting data and generating code. I've commented on all the key points and I'm not going to explain them in detail. So far, we have basically completed the compilation of annotators.

    3. Use of annotations

    Introduce our defined annotation and annotation processor in build.gradle.

      implementation project(':apt_api')
      annotationProcessor project(":apt_processor")
    

    Applied Annotations

    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.tv_content)
        TextView tvContent;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ButterKnife.inject(this);
    
            tvContent.setText("This is it. ButterKnife Principle");
        }
    }
    

    At this point, the document is over. What? You haven't talked about ButterKnife yet. Well, it's really simple. Just paste the code.

    public class ButterKnife {
        static final Map<Class<?>, Constructor<? extends ViewBinding>> BINDINGS = new LinkedHashMap<>();
    
        public static void inject(Object object) {
            if (object == null) {
                return;
            }
            try {
                Class<?> cls = object.getClass();
                Constructor<? extends ViewBinding> constructor = findBindingConstructorForClass(cls);
                ViewBinding viewBinding = constructor.newInstance();
                viewBinding.bindView(object);
            } catch (Exception e) {
    
            }
        }
    
        private static Constructor<? extends ViewBinding> findBindingConstructorForClass(Class<?> cls) throws Exception {
            Constructor<? extends ViewBinding> constructor = BINDINGS.get(cls);
            if (constructor == null) {
                String className = cls.getName();
                Class<?> bindingClass = cls.getClassLoader().loadClass(className + "_ViewBinding");
                constructor = (Constructor<? extends ViewBinding>) bindingClass.getConstructor();
                BINDINGS.put(cls, constructor);
            }
            return constructor;
        }
    }
    

[Annex] Relevant Architecture and Information

Jiaqun 878873098 collects and retrieves prior Android Advanced Architecture Information, Source Code, Notes and Video. Advanced UI, Performance Optimization, Architect Course, NDK, React Native + Weex, Flutter's all-round Android Advanced Practice Technology, there are also technical bulls in the group to discuss and solve problems.

Method of collection:

Tien Zan + Jia Qun 878873098 is available free of charge.

 

Tags: Java Android ButterKnife Google

Posted on Tue, 13 Aug 2019 20:30:03 -0700 by shikhartandon