Teach you how to implement Android compile-time annotations

vivo互联网技术
中文

1. The importance of compile-time annotations in development

From the amazing ButterKnife in the early days, to the various routing frameworks led by ARouter, and to the Jetpack component that Google has vigorously promoted, more and more third-party frameworks are using the technology of compile-time annotation. It can be said Whether you want to study the principles of these third-party frameworks in depth or become an Android senior development engineer, compile-time annotations are a basic technology you have to master.

This article starts from the basic runtime annotation usage, and gradually evolves to the compile-time annotation usage, so that you really understand what scenarios the compile-time annotation should be used in, how to use it, and what are the benefits of using it.

2. Handwritten runtime notes

Similar to the following writing method, when findViewById with too many Views keeps writing many lines, handwriting is very troublesome. We first try to use runtime annotations to solve this problem and see if we can automatically handle these findViewById operations.

The first is the project structure, a lib module must be defined.

Secondly, define our annotation class:

With this annotated class, we can use it in our MainAcitivity first, although this annotation has not played any role yet.

To think about a little here, at this time we have to do is to be assigned to the corresponding field by R.id.xx comment , that is, those who view objects (tv such as the red box) you define for us For the lib project, because MainActivity depends on lib, naturally your lib cannot depend on the app project to which Main belongs. There are 2 reasons:

  • A depends on B, and B depends on A's circular dependence, which will definitely report an error;
  • Since you want to make a lib, you must not rely on the user's host otherwise how can it be called a lib?

So the question becomes, the lib project can only get Acitivty, but not the host's MainActivity. Since the host's MainActivity is not available, how do I know how many fields this activity has? Reflection is going to be used here.

public class BindingView {
 
    public static void init(Activity activity) {
        Field[] fields = activity.getClass().getDeclaredFields();
        for (Field field : fields) {
            //获取 被注解
            BindView annotation = field.getAnnotation(BindView.class);
            if (annotation != null) {
                int viewId = annotation.value();
                field.setAccessible(true);
                try {
                    field.set(activity, activity.findViewById(viewId));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
 
        }
 
    }
}

Finally, we can call this method in the host's MainActivity:

At this point, some people will actually ask. This runtime annotation does not seem to be difficult. Why don't it seem to be used by many people? The problem lies in the pile of reflection methods just now. Everyone knows that reflection will bring some performance loss to the Android runtime, and the code here is a loop, which means that the code here will follow the activity of the lib that you use. interface complexity increases and becomes more and more slowly, which is a will as you gradually increase the complexity of the interface degradation process , single reflection for today's mobile phones almost does not exist any performance consumed, But the use of reflection in this for loop is still as little as possible.

Three, hand-written compile-time annotations

In order to solve this problem, it is necessary to use compile-time annotations. Now we try to use compile-time annotations to solve the above problems. As we said before, runtime annotations can use reflection to get the host's field to complete the requirements. In order to solve the performance problem of reflection, the code we actually want is this:

We can create a new MainActivityViewBinding class in the app's module:

Then calling this method in our BindingView (note that our BindingView is under the lib module) will not solve the reflection problem?

But there is a problem here is that since you are a lib, you cannot rely on the host, so in the lib Module you can't actually get the MainActivityViewBinding class, you still have to use reflection.

You can take a look at the commented out code above, why not just write the string directly? Because you are a lib library, of course you have to be dynamic, otherwise how can you use it for others? In fact, it is to get the host's class name and add a fixed suffix ViewBinding. At this time we get the Binding class, right, the rest is to call the constructor.

public class BindingView {
 
    public static void init(Activity activity) {
        try {
            Class bindingClass = Class.forName(activity.getClass().getCanonicalName() + "ViewBinding");
            Constructor constructor = bindingClass.getDeclaredConstructor(activity.getClass());
            constructor.newInstance(activity);
        } catch (ClassNotFoundException | NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Look at the code structure at this time:

Someone wants to ask here, don’t you still use reflection here, yes! Although reflection is used here, my reflection here will only be called once, no matter your activity has fewer fields, the reflection method here will only execute once. So the performance must be many times faster than the previous scheme. Next, although the code can run normally at this moment, there is still a problem. Although I can call the construction method of the class of our app host in the lib, the class of the host is still our handwritten ? Then your lib library still does not play any role that allows us to write less code.

At this time, we need our apt to appear, which is the core part of compile-time annotations. We create a Java Library, note that Java lib is not android lib, and then introduce it in the app module.

Note that the method of introduction is not imp, but the annotation processor;

Then let's modify lib_processor, first create an annotation processing class:

Then create the file resources/META-INF/services/javax.annotation.processing.Processor, here you should pay attention to the folder creation not to make a mistake.

Then this Processor can specify our annotation processor:

It's not over here, we have to tell this annotation processor to only process our BindView annotation , otherwise this annotation processor is too slow to process all annotations by default, but at this time our BindView annotation class is still in lib Inside the warehouse, obviously we have to adjust our project structure:

Let create a new Javalib at 1610751aa9f15c, just put BindView to , and then let our lib\_processor and app depend on this lib\_interface. Modify the code a little bit. At this time, we are processing at compile time, so the policy does not need to be runtime.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
public class BindingProcessor extends AbstractProcessor {
 
    Messager messager;
 
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        messager = processingEnvironment.getMessager();
        messager.printMessage(Diagnostic.Kind.NOTE, " BindingProcessor init");
        super.init(processingEnvironment);
    }
 
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
 
    //要支持哪些注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(BindView.class.getCanonicalName());
    }
}

At this point, most of our work has been processed. Look at the code structure again (the code structure here must understand why it is designed this way, otherwise you will not learn compile-time annotations).

We are now able to call the method in MainActivityViewBinding through the sdk of lib, but it is still handwritten by us in the app warehouse, which is not very smart and can't be used yet. We need to dynamically generate this class in the annotation processor. As long as this step can be completed, our SDK is basically complete.

I want to mention here that many people’s comments are stuck here, because too many articles or tutorials come up with the Javapoet code, they can’t learn it at all, or they can only copy and paste other people’s things, just a little change. No, in fact, the best way to learn here is to use StringBuffer string splicing to spell out the code we want. Through this string splicing process, we can understand the corresponding api and the idea of generating java code. Then finally use JavaPoet to optimize the code.

We can first think about what steps should be completed if we use string splicing to do this generation operation.

  • First of all, we must obtain which classes use our BindView annotations;
  • Get the fields annotated with BindView in these classes and their corresponding values;
  • Get the class names of these classes so that we can generate class names such as MainActivityViewBinding;
  • Get the package names of these classes, because the class we generated must belong to the same package as the class to which the annotation belongs, so that there will be no field access rights issues;
  • After all the above conditions are met, we can use string splicing to splice out the java code we want.

Here is the code directly, the important part can be directly read the comments, after the above step analysis, it should not be difficult to understand the code comments.

public class BindingProcessor extends AbstractProcessor {
 
    Messager messager;
    Filer filer;
    Elements elementUtils;
 
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        //主要是输出一些重要的日志使用
        messager = processingEnvironment.getMessager();
        //你就理解成最终我们写java文件 要用到的重要 输出参数即可
        filer = processingEnvironment.getFiler();
        //一些方便的utils方法
        elementUtils = processingEnvironment.getElementUtils();
        //这里要注意的是Diagnostic.Kind.ERROR 是可以让编译失败的 一些重要的参数校验可以用这个来提示用户你哪里写的不对
        messager.printMessage(Diagnostic.Kind.NOTE, " BindingProcessor init");
        super.init(processingEnvironment);
    }
 
    private void generateCodeByStringBuffer(String className, List<Element> elements) throws IOException {
 
        //你要生成的类 要和 注解的类 同属一个package 所以还要取 package的名称
        String packageName = elementUtils.getPackageOf(elements.get(0)).getQualifiedName().toString();
        StringBuffer sb = new StringBuffer();
        // 每个java类 的开头都是package sth...
        sb.append("package ");
        sb.append(packageName);
        sb.append(";\n");
 
        // public class XXXActivityViewBinding {
        final String classDefine = "public class " + className + "ViewBinding { \n";
        sb.append(classDefine);
 
        //定义构造函数的开头
        String constructorName = "public " + className + "ViewBinding(" + className + " activity){ \n";
        sb.append(constructorName);
 
        //遍历所有element 生成诸如 activity.tv=activity.findViewById(R.id.xxx) 之类的语句
        for (Element e : elements) {
            sb.append("activity." + e.getSimpleName() + "=activity.findViewById(" + e.getAnnotation(BindView.class).value() + ");\n");
        }
 
        sb.append("\n}");
        sb.append("\n }");
 
        //文件内容确定以后 直接生成即可
        JavaFileObject sourceFile = filer.createSourceFile(className + "ViewBinding");
        Writer writer = sourceFile.openWriter();
        writer.write(sb.toString());
        writer.close();
    }
 
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
 
        // key 就是使用注解的class的类名 element就是使用注解本身的元素 一个class 可以有多个使用注解的field
        Map<String, List<Element>> fieldMap = new HashMap<>();
        // 这里 获取到 所有使用了 BindView 注解的 element
        for (Element element : roundEnvironment.getElementsAnnotatedWith(BindView.class)) {
            //取到 这个注解所属的class的Name
            String className = element.getEnclosingElement().getSimpleName().toString();
            //取到值以后 判断map中 有没有 如果没有就直接put 有的话 就直接在这个value中增加一个element
            if (fieldMap.get(className) != null) {
                List<Element> elementList = fieldMap.get(className);
                elementList.add(element);
            } else {
                List<Element> elements = new ArrayList<>();
                elements.add(element);
                fieldMap.put(className, elements);
            }
        }
 
        //遍历map,开始生成辅助类
        for (Map.Entry<String, List<Element>> entry : fieldMap.entrySet()) {
            try {
                generateCodeByStringBuffer(entry.getKey(), entry.getValue());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
 
    //要支持哪些注解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(BindView.class.getCanonicalName());
    }
}

Finally look at the effect:

Although the generated code format is not pretty, but it runs ok. Here we should pay attention to the Element interface. In fact, if you can understand Element when using compile-time annotations, the subsequent work will be much simpler.

Mainly focus on these 5 subcategories of Element, for example:

package com.smart.annotationlib_2;//PackageElement |表示一个包程序元素
//  TypeElement 表示一个类或接口程序元素。
public class VivoTest {
    //VariableElement |表示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数。
    int a;
 
    //VivoTest 这个方法 :ExecutableElement|表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。
    //int b 这个函数参数: TypeParameterElement |表示一般类、接口、方法或构造方法元素的形式类型参数。
    public VivoTest(int b ) {
        this.a = b;
    }
}

Four, Javapoet generates code

With the above foundation, the process of writing string splicing to generate java code with Javapoet will not be difficult to understand.

private void generateCodeByJavapoet(String className, List<Element> elements) throws IOException {
 
    //声明构造方法
    MethodSpec.Builder constructMethodBuilder =
            MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(ClassName.bestGuess(className), "activity");
    //构造方法里面 增加语句
    for (Element e : elements) {
        constructMethodBuilder.addStatement("activity." + e.getSimpleName() + "=activity.findViewById(" + e.getAnnotation(BindView.class).value() + ");");
    }
 
    //声明类
    TypeSpec viewBindingClass =
            TypeSpec.classBuilder(className + "ViewBinding").addModifiers(Modifier.PUBLIC).addMethod(constructMethodBuilder.build()).build();
    String packageName = elementUtils.getPackageOf(elements.get(0)).getQualifiedName().toString();
     
    JavaFile build = JavaFile.builder(packageName, viewBindingClass).build();
    build.writeTo(filer);
}

I want to mention here that more and more people are using Kotlin to develop apps. You can even use https://github.com/square/kotlinpoet to directly generate Kotlin code. Those who are interested can try it.

Five, the summary of the annotations during compilation

The first is the performance aspect that everyone is concerned about. For runtime annotations, a large amount of reflection code will be generated, and the number of reflection calls will increase as the complexity of the project increases, which is a process of gradual deterioration. For compile-time annotations, the number of reflection calls is fixed. It will not change as the complexity of the project gets worse and worse. In fact, most runtime-annotated projects can be compiled Period annotations to greatly improve the performance of the framework, such as the famous Dagger, EventBus, etc., their first versions are runtime annotations, and subsequent versions are uniformly replaced with compile-time annotations.

Secondly, after reviewing the development process of our compilation period annotations, we can draw the following conclusions:

  • Compile-time annotations can only generate code, but cannot modify the code;
  • The code generated by the annotation must be called manually, it will not be called by itself;
  • For SDK writers, even with compile-time annotations, it is often inevitable to go through reflection at least once, and the function of reflection is mainly to call the code generated by your annotation processor.

Some friends may ask here, since compile-time annotations can only generate code and cannot modify the code, its effect is very limited. Why not directly use bytecode tools like ASM, Javassist, etc., which can not only generate code but also You can also modify the code, the function is more powerful. Because these bytecode tools generate classes directly, and the writing is complicated and error-prone, and it is not easy to debug. It is okay to write something similar to prevent quick clicks on a small scale, but it is actually quite inconvenient to develop third-party frameworks on a large scale. Yes, it is far less efficient than compile-time annotations.

In addition, think about it again. After the compilation-time annotations we mentioned in the previous article are written into third-party libraries for others to use, users still need to manually call the "init" method at the right time, but there are some excellent ones. The third-party library can do that even the init method does not need to be manually called by the user. It is very convenient to use. How is this done? In fact, it is not difficult. In most cases, these third-party libraries use compile-time annotations to generate code, and then cooperate with bytecode tools such as ASM to directly call the init method for you, so that you can avoid the manual call process. The core is still compile-time annotations, just a step is omitted with the bytecode tool.

Author: vivo internet client team-Wu Yue
阅读 661

vivo 互联网技术
分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。
2.2k 声望
9.2k 粉丝
0 条评论
你知道吗?

2.2k 声望
9.2k 粉丝
宣传栏