美文网首页AndroidAndroid技术知识Android开发经验谈
ButterKnife原理分析(二)注解的处理

ButterKnife原理分析(二)注解的处理

作者: Ihesong | 来源:发表于2017-12-31 23:47 被阅读55次

    上一篇我们讲解了ButterKnife的设计思想,理解了ButterKnife绑定相关源码的实现逻辑。但是它是怎么通过注解的方式生成的那些逻辑代码,这才是最让我们迫切想知道,因此在这篇,我将说说ButterKnife中注解处理的原理。本篇主要有以下内容:

    1. 注解Annotation
    2. 注解处理器AbstractProcessor
    3. AutoService注册注解处理器
    4. JavaPoet生成Java代码
    5. Element元素相关
    6. 编译时注解解析
    7. 例子项目

    注解(Annotation)

    注解(Annotation)在Java中已经是很普遍的使用了,它其实就是一种标记信息,然后程序在编译或者运行的时候可以读取这个标记信息,去执行特定的逻辑,比如@BindView(R.id.tv_text) TextView tvText,程序在编译时会读取到这个@BindView注解,解析出它的值R.id.tv_text,再根据它注解的这个tvText,就可以生成类似tvText = (TextView)findViewById(R.id.tv_text);的功能代码。
    注解按生命周期可以分为

    • RetentionPolicy.SOURCE(源码注解),只在源码中存在,在编译时会被丢弃,通常用于检查性的操作,如@Override。
    • RetentionPolicy.CLASS(编译时注解),在编译后的class文件中依然存在,通常用于编译时处理,如ButterKnife的@BindView。
    • RetentionPolicy.RUNTIME(运行时注解),不仅在编译后的class文件中存在,在被jvm虚拟机加载之后,仍然存在,通常用于运行时处理,如Retrofit的@Get。

    同时注解按使用的对象可以分为

    • ElementType.TYPE(类型注解),标记在接口、类、枚举上。
    • ElementType.FIELD(属性注解),标记在属性字段上。
    • ElementType.METHOD(方法注解),标记在方法上。
    • ElementType.PARAMETER(方法参数注解),标记在方法参数上。
    • ElementType.CONSTRUCTOR(构造方法注解),标记在构造方法上。
    • ElementType.LOCAL_VARIABLE(本地变量注解),标记在本地变量上。
    • ElementType.ANNOTATION_TYPE(注解的注解),标记在注解上。
    • ElementType.PACKAGE(包注解),标记在包上。
    • ElementType.TYPE_PARAMETER(类型参数注解,Java1.8加入),标记类型参数上。
    • ElementType.TYPE_USE(类型使用注解,Java1.8加入),标记在类的使用上。

    我们首先来认识ButterKnife的一个自定义属性注解@BindView

    /**
     * 作用于View的注解,如@BindView(R.id.text) TextView tvText
     *
     * @Retention(RetentionPolicy.CLASS) 表示生命周期到类的编译时期
     * @Target(ElementType.FIELD) 表示注解作用在字段上
     *
     * Created by Administrator on 2017/12/31 0031.
     */
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface BindView {
    
        @android.support.annotation.IdRes
        int value();
    }
    

    它可以保留在类编译之后,使用场景是作用在属性上

    关于注解的详细介绍查看Java注解基础概念总结

    注解处理器AbstractProcessor

    注解只是一种标记信息,所以需要我们自己去处理注解,注解的处理有编译时注解处理和运行时注解处理。运行时注解,我们可以通过反射获取注解信息,进而进行相应处理。而编译时注解就需要使用注解处理器(Annotation Processor)进行处理。那什么是注解处理器?

    注解处理器是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些生成的Java代码是在新生成的.java文件中,所以你不能修改已经存在的Java类,例如向已有的类中添加方法。这些生成的Java文件,会同其他普通的手动编写的Java源代码一样被javac编译。
    要实现一个注解处理器需要继承AbstractProcessor,并重写它的4个方法,同时必须要有一个无参的构造方法,以便注解工具能够对它进行初始化。

    public class ViewBindProcessor extends AbstractProcessor {  
        private Types typeUtils;
        private Elements elementUtils;
        private Filer filer;
        private Messager messager;
    
        @Override  
        public synchronized void init(ProcessingEnvironment processingEnv) {  
            super.init(processingEnv);  
            
            //提供给注解处理器使用的工具类
            typeUtils = processingEnv.getTypeUtils();
            elementUtils = processingEnv.getElementUtils();
            filer = processingEnv.getFiler();
            messager = processingEnv.getMessager();
        }  
      
        @Override  
        public Set<String> getSupportedAnnotationTypes() {  
            //添加需要处理的注解
            Set<String> annotataions = new LinkedHashSet<String>();  
            annotataions.add(MyAnnotation.class.getCanonicalName());  
            return annotataions;  
        }  
      
        @Override  
        public SourceVersion getSupportedSourceVersion() {
            //指定使用的Java版本
            return SourceVersion.latestSupported();  
        } 
        
         @Override  
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
            //这里实现注解的处理,重点方法
            return false;  
        }  
    }  
    
    • init,会被注解处理工具调用,参数ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
    • getSupportedAnnotationTypes(),指定注解处理器要处理哪些注解,返回一个字符串集合,包含要处理注解的全名。
    • getSupportedSourceVersion, 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
    • process,相当于每个处理器的main函数,在这里可以做扫描、评估和处理注解代码的操作,以及生成Java文件。

    那么init方法中ProcessingEnvironment提供的Elements,Types,Filer,Messager 等是做什么用的呢?

    • Elements:用来处理程序元素的工具类
    • Types:用来处理类型数据的工具类
    • Filer:用来给注解处理器创建文件
    • Messager:用来给注解处理器报告错误,警告,提示等消息。

    AutoService注册注解处理器

    以前要注册注解处理器是要在module的META-INF目录下新建services目录,并创建一个名为javax.annotation.processing.Processor的文件,然后在文件中写入要注册的注解处理器的全名,例如在javax.annotation.processing.Processor的文件中加上

    com.pinery.compile.ViewBindProcessor
    

    来注册ViewBindProcessor注解处理器。

    后来Google推出了通过添加AutoService注解库来实现注解处理器的注册,通过在你的注解处理器上加上@AutoService(Processor.class)注解,即可在编译时生成 META-INF/services/javax.annotation.processing.Processor 文件。
    配置AutoService需要在工程的build.gradle中添加

    JavaPoet生成Java代码

    JavaPoet是Square公司出品的生成Java源文件库,正如其名,会写Java代码的诗人,使用它的一系列API就可以很方便的生成java源代码了。

    JavaPoet中有几个常用的类:

    • MethodSpec,代表一个构造函数或方法声明。
    • TypeSpec,代表一个类,接口,或者枚举声明。
    • FieldSpec,代表一个成员变量,一个字段声明。
    • JavaFile,包含一个顶级类的Java文件。

    这是一个计算从1到100相加的方法

    public static int caculateNum() {
        int result = 0;
        for(int i = 1; i < 100; i++) {
          result = result + i;
        }
        return result;
    }
    

    我们用MethodSpec实现这个方法声明

    MethodSpec caculateMethod = MethodSpec.methodBuilder("caculateNum")
          .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
          .returns(int.class)
          .addStatement("int result = 0")
          .beginControlFlow("for(int i = $L; i < $L; i++)", 1, 100)
          .addStatement("result = result $L i", "+")
          .endControlFlow()
          .addStatement("return result")
          .build();
    
    

    可以发现,通过addModifiers添加方法修饰符,returns来定义方法的返回值类型,addStatement来添加方法中的一行语句,它会处理分号和换行,beginControlFlow和endControlFlow构成一个封闭的控制语段,适用于if,for,while等。$L相当于一个占位符,代表的是一个字面量,其他还有:

    • $S for Strings,代表一个字符串
    • $T for Types,代表一个类型,使用它会自动import导入包
    • $N for Names,代表我们自己生成的方法名或者变量名等等

      例如
    addStatement("$T.out.println($S)", System.class, "Hello World"))
    

    这是定义的一个属性

    private final String name = "Pinery";
    

    我们用MethodSpec实现这个方法声明

    FieldSpec nameField = FieldSpec.builder(String.class, "name")
        .addModifiers(Modifier.PRIVATE, Modifier.FINAL)
        .initializer("$S", "Pinery")
        .build();
    

    下面是一个类的定义

    public class MyClass{
    
        private final String name = "Pinery";
    
        public static int caculateNum() {
            int result = 0;
            for(int i = 1; i < 100; i++) {
              result = result + i;
            }
            return result;
        }
    }
    

    我们用TypeSpec实现这个方法声明

    TypeSpec helloWorld = TypeSpec.classBuilder("MyClass")
        .addModifiers(Modifier.PUBLIC)
        .addField(nameField)
        .addMethod(caculateMethod)
        .build();
    

    通过TypeSpec添加了属性实现和方法实现,其他常用的还有

    • addTypeVariable,添加泛型声明
    • addSuperinterface,添加接口实现
    • addJavadoc,添加注释
    • interfaceBuilder,生成一个接口

    等,详细使用可以参考JavaPoet官方文档

    Element元素相关

    注解处理工具扫描java源文件,源代码中的每一部分都是程序中的Element元素,如包,类,方法,字段等。每一个元素代表一个静态的,语言级别的结构。源代码其实是一种结构化的文本(例如json文本就是一种结构化的文本),因此需要对它进行解析,解析的话,解析器会解析某些信息代表某些结构,例如源代码中的类声明信息代表TypeElement类型元素,方法声明信息代表代表ExecutableElement类型元素。有了这些结构,就能完整的表示整个源代码信息了。
    Element元素分为以下类型:

    • ExecutableElement: 可执行元素,包括类或接口的方法、构造方法或初始化程序
    • PackageElement: 包元素,提供对有关包及其成员的信息的访问
    • TypeElement: 类或接口元素,提供对有关类型及其成员的信息的访问
    • TypeParameterElement: 表示一般类、接口、方法或构造方法元素的形式类型参数,类型参数声明一个 TypeVariable
    • VariableElement: 表示一个字段、enum常量、方法或构造方法参数、局部变量或异常参数。

    Element元素还有个asType()可以获取元素类型TypeMirror,TypeMirror有以下具体类型:

    • ArrayType: 表示一个数组类型。多维数组类型被表示为组件类型也是数组类型的数组类型。
    • DeclaredType: 表示某一声明类型,是一个类 (class) 类型或接口 (interface) 类型。这包括参数化的类型(比如 java.util.Set<String>)和原始类型。
    • ErrorType: 表示无法正常建模的类或接口类型。这可能是处理错误的结果。大多数对于派生于这种类型(比如其成员或其超类型)的信息的查询通常不会返回有意义的结果。
    • ExecutableType: 表示 executable 的类型。executable 是一个方法、构造方法或初始化程序。
    • NoType: 在实际类型不适合的地方使用的伪类型。NoType 的种类有:
      • VOID:对应于关键字 void。
      • PACKAGE:包元素的伪类型。
      • NONE:用于实际类型不适合的其他情况中;例如,java.lang.Object 的超类。
    • NullType: 表示 null 类型。此类表达式 null 的类型。
    • PrimitiveType: 表示一个基本类型。这些类型包括 boolean、byte、short、int、long、char、float 和 double。
    • ReferenceType: 表示一个引用类型。这些类型包括类和接口类型、数组类型、类型变量和 null 类型。
    • TypeVariable: 表示一个类型变量。类型变量可由某一类型、方法或构造方法的类型参数显式声明。
    • WildcardType: 表示通配符类型参数。

    编译时注解解析

    有了上面知识点的了解之后,下面就可以进行编译时的注解解析了,需要以下几个步骤:

    1. 定义注解
    2. 定义一个继承自AbstractProcessor的注解处理器,重写它4个方法中
    3. 使用AutoService注册自定义的注解处理器
    4. 实现process方法,在这里处理注解
    5. 处理所有注解,得到TypeElement和注解信息等信息
    6. 使用JavaPoet生成新的Java类

    在annotations的moudle中定义一个注解

    /**
     * 作用于View的注解,如@BindView(R.id.text) TextView tvText
     *
     * @Retention(RetentionPolicy.CLASS) 表示生命周期到类的编译时期
     * @Target(ElementType.FIELD) 表示注解作用在字段上
     *
     * Created by Administrator on 2017/12/31 0031.
     */
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface BindView {
    
        @android.support.annotation.IdRes
        int value();
    }
    

    在compile的moudle中定义一个ViewBindProcessor注解处理器

    /**
     * 自定义的AbstractProcessor,用于编译时处理注解
     */
    @AutoService(Processor.class) //添加AutoService注解,自动注册ViewBindProcessor注解处理器
    public class ViewBindProcessor extends AbstractProcessor{
    
        private Map<TypeElement, List<ViewBindInfo>> bindMap = new HashMap<>();
    
        //用来处理类型数据的工具类
        private Types typeUtils;
        //用来处理程序元素的工具类
        private Elements elementUtils;
        //用来给注解处理器创建文件
        private Filer filer;
        //用来给注解处理器报告错误,警告,提示等消息。
        private Messager messager;
    
        /**
         * 会被注解处理工具调用,参数ProcessingEnvironment提供了Elements,Types,Filer,Messager 等。
         * @param processingEnvironment
         */
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
    
            typeUtils = processingEnv.getTypeUtils();
            elementUtils = processingEnv.getElementUtils();
            filer = processingEnv.getFiler();
            messager = processingEnv.getMessager();
        }
    
        /**
         * 指定注解处理器是注册给那一个注解的,它是一个字符串的集合,意味着可以支持多个类型的注解,并且字符串是合法全名。
         * @return
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> annotataions = new LinkedHashSet<String>();
    
            //添加自定义的BindView注解
            annotataions.add(BindView.class.getCanonicalName());
    
            return annotataions;
        }
    
        /**
         * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported()
         * @return
         */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        /**
         * 相当于每个处理器的main函数,在这里可以做扫描、评估和处理注解代码的操作,以及生成Java文件。
         * @param set
         * @param roundEnvironment
         * @return
         */
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
            collectBindViewAnnotations(roundEnvironment);
    
            generateJavaFilesWithJavaPoet();
    
            return false;
        }
    
        /**
         * 收集BindView注解
         * @param roundEnvironment
         * @return
         */
        private boolean collectBindViewAnnotations(RoundEnvironment roundEnvironment){
            //查找所有添加了注解BindView的元素
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
            if(elements == null || elements.isEmpty()){
                return false;
            }
    
            for(Element element : elements){
                //注解BindView必须添加在属性上
                if(element.getKind() != ElementKind.FIELD){
                    error(element, "只有类的属性可以添加@%s注解", BindView.class.getCanonicalName());
                    return false;
                }
    
                //获取注解的值
                int viewId = element.getAnnotation(BindView.class).value();
                //这个元素是属性类型的元素
                VariableElement viewElement = (VariableElement) element;
                //获取直接包含属性元素的元素,即类元素
                TypeElement typeElement = (TypeElement) viewElement.getEnclosingElement();
    
                //将类型元素作为key,保存到bindMap暂存
                List<ViewBindInfo> viewBindInfoList = bindMap.get(typeElement);
                if(viewBindInfoList == null){
                    viewBindInfoList = new ArrayList<>();
                    bindMap.put(typeElement, viewBindInfoList);
                }
    
                info("注解信息:viewId=%d, name=%s, type=%s", viewId, viewElement.getSimpleName().toString(), viewElement.asType().toString());
    
                viewBindInfoList.add(new ViewBindInfo(viewId, viewElement.getSimpleName().toString(), viewElement.asType()));
            }
    
            return true;
        }
    
        /**
         * 生成注解处理之后的Java文件
         */
        private void generateJavaFilesWithJavaPoet(){
            if(bindMap.isEmpty()){
                return;
            }
    
            //针对每个类型元素,生成一个新文件,例如,针对MainActivity,生成MainActivity_ViewBind文件
            for(Map.Entry<TypeElement, List<ViewBindInfo>> entry : bindMap.entrySet()){
                TypeElement typeElement = entry.getKey();
                List<ViewBindInfo> list = entry.getValue();
    
                //获取当前类型元素所在的包名
                String pkgName = elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
    
                //获取类的全名称
                ClassName t = ClassName.bestGuess("T");
                ClassName viewBinder = ClassName.bestGuess("com.pinery.bind_lib.ViewBinder");
    
                //定义方法结构
                MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bind")
                        .addAnnotation(Override.class)//Override注解
                        .addModifiers(Modifier.PUBLIC)//public修饰符
                        .returns(void.class)//返回类型void
                        .addParameter(t, "activity")//参数类型
                        ;
                //为方法添加实现
                for(ViewBindInfo info : list){
                    methodSpecBuilder.addStatement("activity.$L = activity.findViewById($L)", info.viewName, info.viewId);
                }
    
                //定义类结构
                TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "_ViewBinder")
                        .addModifiers(Modifier.PUBLIC)//public修饰符
                        .addTypeVariable(TypeVariableName.get("T", TypeName.get(typeElement.asType())))//泛型声明
                        .addSuperinterface(ParameterizedTypeName.get(viewBinder, t))
                        .addMethod(methodSpecBuilder.build())//方法
                        .build();
    
                //定义一个Java文件结构
                JavaFile javaFile = JavaFile.builder(pkgName, typeSpec).build();
                try {
                    //写入到filer中
                    javaFile.writeTo(filer);
                }catch (Exception ex){
                    ex.printStackTrace();
                }
    
            }
    
        }
    
        /**
         * 错误提示
         * @param element
         * @param msg
         * @param args
         */
        private void error(Element element, String msg, Object... args){
            //输出错误提示
            messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
        }
    
        /**
         * 信息提示
         * @param msg
         * @param args
         */
        private void info(String msg, Object... args) {
            messager.printMessage(
                    Diagnostic.Kind.NOTE,
                    String.format(msg, args));
        }
    
    }
    

    这里会使用一个ViewBindInfo用于存储view的id, 名称,和类型的对应关系,如下:

    public class ViewBindInfo {
    
        public int viewId;
        public String viewName;
        public TypeMirror typeMirror;
    
        public ViewBindInfo(int viewId, String viewName, TypeMirror typeMirror){
            this.viewId = viewId;
            this.viewName = viewName;
            this.typeMirror = typeMirror;
        }
    
    }
    

    这样就完成了编译时注解的处理。接下来在MainActivity中使用注解

    
    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.tv_text)
        TextView tvText;
        @BindView(R.id.btn_text)
        Button btnText;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            BindHelper.bind(this);
    
            tvText.setText("Hello, BindView");
            btnText.setText("Hello, BindView");
        }
    }
    

    编译后会生成一个MainActivity_ViewBinder.java文件。我们在看看BindHelper的实现

    
    public class BindHelper {
    
        /**
         * 绑定方法
         * @param activity
         */
        public static void bind(Activity activity) {
            try {
                Class<?> viewBinderClazz = Class.forName(activity.getClass().getCanonicalName() + "_ViewBinder");
                ViewBinder viewBinder = (ViewBinder) viewBinderClazz.newInstance();
                viewBinder.bind(activity);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
    
        }
    
    }
    

    这里就会通过反射生成MainActivity_ViewBinder的对象,调用bind方法作view的绑定处理。

    例子项目

    CompileAnnotation

    相关文章

      网友评论

        本文标题:ButterKnife原理分析(二)注解的处理

        本文链接:https://www.haomeiwen.com/subject/rmdzgxtx.html