APT best learning case: write a simple version of ButterKnife

foreword

Purpose: To write a simple version of ButterKnife, and learn how to implement annotations, annotationProcessor, etc. by handwriting ButterKnife.

First look at the structure of butterknife:

source address

https://github.com/LucasXu01/MyButterKnife

Use of ButterKnife

Adding build.gradledependencies:

android {
    
    
  ...
  // Butterknife requires Java 8.
  compileOptions {
    
    
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}
dependencies {
    
    
  implementation 'com.jakewharton:butterknife:10.2.3'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}

MainActivityWrite in :

public class MainActivity extends AppCompatActivity {
    
    
    @BindView(R.id.tv_content)
    TextView tvContent;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
		···
        ButterKnife.bind(this);
        tvContent.setText("修改成功!");
    }
}

According to the analysis, writing a simple ButterKnife by hand is mainly divided into two steps: annotation definition; annotation analysis ;

Handwritten Easy ButterKnife

Annotation definition

New annotation MyBindView.java:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyBindView {
    
    
    int viewId();
}

`int viewId(); is used to bind the id of which view, we need to add variables to the annotation, so that after assignment, we will know which id to bind:

    @MyBindView(viewId = R.id.tv_content)
    TextView tvContent;

Improve:

Change viewId to value. If the name is value, the compiler will automatically assign a value for us, so we need to change it slightly:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyBindView {
    
    
    int value();
}
@MyBindView(R.id.tv_content)
TextView tvContent;

Annotation analysis

ButterKnife starts parsing with the following code:

ButterKnife.bind(this);

We also create a new class, and write the binding code of the control in the bind() method:

  • Get all member variables of the Activity
  • Determine whether this member variable is annotated by MyBindView
  • If the annotation matches, assign findViewById to the member variable
public class MyButterKnife {
    
    
    public static void bind(Activity activity){
    
    
        //获取该 Activity 的全部成员变量
        for (Field field : activity.getClass().getDeclaredFields()) {
    
    
            //判断这个成员变量是否被 MyBindView 进行注解
            MyBindView myBindView = field.getAnnotation(MyBindView.class);
            if (myBindView != null) {
    
    
                try {
    
    
                    //注解符合的话,就对该成员变量进行 findViewById 赋值
                    //相当于 field = activity.findViewById(myBindView.value())
                    field.set(activity, activity.findViewById(myBindView.value()));
                } catch (IllegalAccessException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }
}

There is no problem in the implementation of the above functions, but the binding of each control depends on reflection, which consumes too much performance. One is okay, but a normal Activity has more than one View. With the increase of Views, the execution time will be longer. Therefore, we must find a new way out, that is AnnotationProcessor.

Isn't the way that consumes the least performance is to directly use findViewById to bind the View? That being the case, is there any way to generate the findViewById code during the compilation phase, and then call it directly when it is used.

We want to create a new class, the class name has a fixed form, that is 原Activity名字+Binding:

Simulate auto-generated files:

public class MainActivityBinding {
    
    
    public MainActivityBinding(MainActivity activity) {
    
    
        activity.tvContent = activity.findViewById(R.id.tv_content);
    }
}

Modified MyButterKnife:

public class MyButterKnife {
    
    
    public static void bind(Activity activity) {
    
    
        try {
    
    
            //获取"当前的activity类名+Binding"的class对象
            Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "Binding");
            //获取class对象的构造方法,该构造方法的参数为当前的activity对象
            Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
            //调用构造方法
            constructor.newInstance(activity);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }
}

So, we only have one problem left now, and that is how to dynamically generate the MainActivityBinding class.

At this time, AnnotationProcessor is really needed.

AnnotationProcessor is a tool for processing annotations, which can find annotations in source code and automatically generate code based on annotations.

Use AnnotationProcessor

Create a new module.

Android Studio --> File --> New Module --> Java or Kotlin Library --> Next --> Finish 。

Create in the new module MyBindingProcessorto inherit AbstractProcessor,

  • process(): The code that automatically generates the code is stored in it.
  • getSupportedAnnotationTypes(): Returns the supported annotation types.
@AutoService(Processor.class)
public class MyBindingProcessor extends AbstractProcessor {
    
    
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
    
      //测试输出
        System.out.println("配置成功!");  
        return false;
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
    
    
        return super.getSupportedAnnotationTypes();
    }
}

Note that this module adds auto-servicedependencies, which can be automatically configured and executed, saving the trouble of traditionally configuring the META-INF folder:

    implementation 'com.squareup:javapoet:1.12.1'
    // AS 4.3.1 ->  4.0.1 没有问题
    // As-3.4.1  +  gradle-5.1.1-all + auto-service:1.0-rc4
    compileOnly 'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'

At this point, configure the dependencies of each module, open Terminal, enter ./gradlew :app:compileDebugJava, and check if there is any output 配置成功!. If there is, the configuration is successful!

The next step is MyBindingProcessorto add specific generated code in the process method:

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
    
        //获取全部的类
        for (Element element : roundEnvironment.getRootElements()) {
    
    
            //获取类的包名
            String packageStr = element.getEnclosingElement().toString();
            //获取类的名字
            String classStr = element.getSimpleName().toString();
            //构建新的类的名字:原类名 + Binding
            ClassName className = ClassName.get(packageStr, classStr + "Binding");
            //构建新的类的构造方法
            MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");
            //判断是否要生成新的类,假如该类里面 MyBindView 注解,那么就不需要新生成
            boolean hasBuild = false;
            //获取类的元素,例如类的成员变量、方法、内部类等
            for (Element enclosedElement : element.getEnclosedElements()) {
    
    
                //仅获取成员变量
                if (enclosedElement.getKind() == ElementKind.FIELD) {
    
    
                    //判断是否被 MyBindView 注解
                    MyBindView bindView = enclosedElement.getAnnotation(MyBindView.class);
                    if (bindView != null) {
    
    
                        //设置需要生成类
                        hasBuild = true;
                        //在构造方法中加入代码
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    }
                }
            }
            //是否需要生成
            if (hasBuild) {
    
    
                try {
    
    
                    //构建新的类
                    TypeSpec builtClass = TypeSpec.classBuilder(className)
                            .addModifiers(Modifier.PUBLIC)
                            .addMethod(constructorBuilder.build())
                            .build();
                    //生成 Java 文件
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                } catch (IOException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
        return false;
    }

Run to see if there are files generated in the build directory:

Guess you like

Origin blog.csdn.net/LucasXu01/article/details/128367376