04. Handwritten ButterKnife(ButterKnife source reading)

Keywords: Java ButterKnife Android Gradle

Source address: https://github.com/renzhenming/MyButterknife

I believe that most Android developers are using ButterKnife for code generation. There are several advantages of using ButterKnife. Firstly, to improve the efficiency of development, we can generate all the View objects in the layout or set the click events of View in one click. These two functions are the most commonly used. Second, it will not affect the efficiency of app. ButterKnife generates code by compile-time annotations. Compared with XUtils annotations, ButterKnife has a great advantage in efficiency. If you look at Xutils source code, you will find that the annotations inside are implemented by reflection, and reflection will increase the running cost to a certain extent.

The author of ButterKnife, Jake Wharton of Google, can see the complete code on GitHub. https://github.com/JakeWharton/butterknife/tree/master/butterknife-annotations/src/main/java/butterknife ButterKnife has quite a lot of functions, not just our longest-used bindView set on Click and so on. Look at these annotations and you will probably know the functional points involved.

826815906.png

Today, one of the BindView annotations is intended to be implemented, and the onClick method may be implemented later to deepen the use of compile-time annotations.

When we use ButterKnife.bind () in an Activity, recompilation generates such a class. xxxx_ViewBinding,xxxx stands for the name of the current Activity. What is the role of this class? Let's simply copy it to see.

public final class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  MainActivity_ViewBinding(MainActivity target) {
    this.target = target;
    target.world = Utils.findViewById(target,2131427424);
    target.bitch = Utils.findViewById(target,2131427425);
  }

  @Override
  @CallSuper
  public final void unbind() {
    target.world = null;
    target.bitch = null;
  }
}

From this class, we can see that using ButterKnife does not need findViewById, but gives this work to the compiler to generate automatically. Then, when calling the bind method, it reflects the object of creating the automatically generated class once to realize the function of view injection. So why do we generate the class automatically when compiling, which needs the knowledge of annotations? Next, we will use the knowledge of annotations. Start writing a simple ButterKnife tool by hand

We created a butterknife-annotation and butterknife-compiler Java library (butterknife-compiler must be Java library, because the annotation generator needs to inherit AbstractProcessor, which is a class that only Java engineering can refer to). We created the annotation BindView in butterknife-annotation.

/**
 * Created by renzhenming on 2018/4/24.
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
    int value();
}

Create annotation generator in butterknife-compiler


/**
 * Created by renzhenming on 2018/4/24.
 * AbstractProcessor This class is in Java and can only be used in ava Library
 */
@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

    private Filer mFiler;
    private Elements mElementUtils;

    /**
     * init()The method is called by the annotation processing tool and the Processing Enviroment parameter is entered.
     * ProcessingEnviroment Provide many useful tool classes Elements, Types, and Filer
     * @param processingEnv Provides an environment for processor s to access tool frameworks
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
    }

    /**
     * Specify the Java version to be used, usually SourceVersion.latestSupported() is returned here, and SourceVersion.RELEASE_6 is returned by default.
     * @return  Java version used
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * Here you must specify which annotation processor is registered for. Note that its return value is a collection of strings containing the legal full name of the annotation type that the processor wants to process
     * @return  The set of annotation types supported by the annotator, if not, returns an empty set
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        //Imitate Butternife source code
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation: getSupportAnnotations()){
             types.add(annotation.getCanonicalName());
        }
        return types;
    }

    private Set<Class<? extends Annotation>> getSupportAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(BindView.class);
        return annotations;
    }
    /**
     * This is equivalent to the main() function of each processor, where you write your code for scanning, evaluating, and processing annotations, as well as generating Java files.
     * Input parameter RoundEnviroment allows you to query annotated elements that contain specific annotations
     * @param set   Annotation types for request processing
     * @param roundEnvironment  Current and previous information environments
     * @return  If true is returned, these annotations are declared and do not require subsequent Processor s to process them;
     *          If false is returned, these annotations are undeclared and may require subsequent Processor s to process them
     *
     *          The log information here can only be seen in gradle console and not in Android logcat. it is to be noted that
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
          ...........................
    }
}

Note the configuration of the library build.gradle for the annotation generator

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    //Use of AutoService annotations
    compile 'com.google.auto.service:auto-service:1.0-rc3'
    //Automatic generation of class correlation
    compile 'com.squareup:javapoet:1.7.0'
    compile project(':butterknife-annotation')
}
//Error Resolution: Error detection for unmapped characters encoding GBK
tasks.withType(JavaCompile){
    options.encoding='UTF-8'
}
//android studio is not fully compatible with Java 8, so specify a compiled version bit of 1.7
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

Setting dependencies in the app configuration file build.gradle of our main project

compile project(':butterknife-annotation')
compile project(':butterknife-compiler')

At this point, compile the project and execute it into ButterKnifeProcessor's process method. If you can't see the print, check two points.
First: whether the object file (bind-bound activity) has been modified compared with the previous compilation, and if the code has not changed, it will not be recompiled
Second: Because it's a log printed in Java library, you can't see it in Android studio's logcat. You need to go to Gradle Console to see it.

When the process method is executed, the next step is to write the code that automatically generates the class and add the following code to the process method

//--------------------------------------------------------------------------------------------------------------------------------

        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        //LinkedHashMap output and input in the same order, first input first output
        Map<Element,List<Element>> elementsMap = new LinkedHashMap<>();
        for (Element element : elements) {
            //All fields related to annotations are taken here, including fields in various classes, that is to say, in the
            //At compile time, all field s in the project that involve this annotation are returned in this Set.
            //We need to sort it manually.
            System.out.println("-----------------------"+element.getSimpleName());
            //The enclosing Element obtained is the class name of the field class
            Element enclosingElement = element.getEnclosingElement();
            System.out.println("------------enclosingElement-----------"+enclosingElement.getSimpleName());
            //Store all field s in a class into a collection with the class name bit key value
            List<Element> bindViewElements = elementsMap.get(enclosingElement);
            if (bindViewElements == null){
                bindViewElements = new ArrayList<>();
                elementsMap.put(enclosingElement,bindViewElements);
            }
            bindViewElements.add(element);
        }

        //------------------------------------------------------------------------------------------------------------------------------

        for (Map.Entry<Element,List<Element>> entry:elementsMap.entrySet()){
            Element enclosingElement = entry.getKey();
            List<Element> bindViewElements = entry.getValue();

            ClassName unbinderClassName = ClassName.get("com.rzm.butterknife","Unbinder");
            System.out.println("------------Unbinder-----------"+unbinderClassName.simpleName());
            //String to get the class name
            String activityName = enclosingElement.getSimpleName().toString();
            ClassName activityClassName = ClassName.bestGuess(activityName);
            //Assemble this line of code: public final class xxx_ViewBinding implements Unbinder
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"_ViewBinding")
                    //Add public final before the class name
                    .addModifiers(Modifier.FINAL,Modifier.PUBLIC)
                    //Implementation Interface for Adding Classes
                    .addSuperinterface(unbinderClassName)
                    //Add a member variable whose name target is modeled after butterknife
                    .addField(activityClassName,"target",Modifier.PRIVATE);

            //The Method of Implementing Unbinder
            //The CallSuper annotation is not available directly like Override, and it needs to be done in this way.
            ClassName callSuperClass = ClassName.get("android.support.annotation","CallSuper");
            MethodSpec.Builder unbindMethod = MethodSpec.methodBuilder("unbind")//Consistent with the method name in the Unbinder you created
                    .addAnnotation(Override.class)
                    .addAnnotation(callSuperClass)
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC);

            //Adding constructors
            MethodSpec.Builder constructMethodBuilder = MethodSpec.constructorBuilder()
                    .addParameter(activityClassName,"target");
            constructMethodBuilder.addStatement("this.target = target");
            for (Element bindViewElement : bindViewElements) {
                String fieldName = bindViewElement.getSimpleName().toString();

                //Adding initialization code to the constructor
                ClassName utilsClassName = ClassName.get("com.rzm.butterknife", "Utils");
                BindView annotation = bindViewElement.getAnnotation(BindView.class);
                int resId = annotation.value();
                constructMethodBuilder.addStatement("target.$L = $T.findViewById(target,$L)",fieldName,utilsClassName,resId);

                //Add the code target.textView1 = null to the unbind method.
                //You can't use addCode because it doesn't add semicolons and newlines after every line of code
                unbindMethod.addStatement("target.$L = null",fieldName);
            }
            classBuilder.addMethod(constructMethodBuilder.build());


            classBuilder.addMethod(unbindMethod.build());

            //Start generating
            try {

                //Get the package name
                String packageName = mElementUtils.getPackageOf(enclosingElement)
                        .getQualifiedName().toString();

                JavaFile.builder(packageName,classBuilder.build())
                        //Annotations to add classes
                        .addFileComment("butterknife Automatic Generation")
                        .build().writeTo(mFiler);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return false;

Create a butterknife Android library, create the class ButterKnife, implement the bind method, and inject the automatically generated class generation into view through reflection

public class ButterKnife {

    public static Unbinder bind(Activity activity){
        try {
            Class<? extends Unbinder> clazz = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName() + "_ViewBinding");
            //Constructor

            Constructor<? extends Unbinder> unbinderConstuctor = clazz.getDeclaredConstructor(activity.getClass());
            Unbinder unbinder = unbinderConstuctor.newInstance(activity);
            return unbinder;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return Unbinder.EMPTY;
    }
}

Unbinder
···
public interface Unbinder {

//Imitate butterknife source code

@UiThread
void unbind();

Unbinder EMPTY = new Unbinder() {
    @Override
    public void unbind() {

    }
};

}
···
Utils

public class Utils {
    public static <T extends View> T findViewById(Activity activity,int viewId){
        return (T)activity.findViewById(viewId);
    }
}

Test the usage

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.text1)
    TextView world;

    @BindView(R.id.text2)
    TextView bitch;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        world.setText("aaaaa");
    }
}

If you recompile, you can see the automatically generated classes


828223296.png

Run successfully, so we don't need to write findViewById to get the view object.

Posted by dstonek on Sat, 19 Jan 2019 21:21:12 -0800