手写ButterKnife来搞明白Android注解处理器_android 注解处理器原理_丶笑看退场的博客-程序员宝宝

技术标签: android  jetpack  数据库  Android笔记  

Butterknife现在在项目中基本没用到了,逐渐被ViewBinding所代替,而我们所熟知它的内部原理是通过自定义注解+自定义注解解析器来动态生成代码并为我们的view绑定id的。今天就通过重新手写ButterKinife来搞明白我们今天的主角–Anotation Processing(注解处理器)。

源码地址APTDemo

运行时注解

在写注解处理器之前,先用运行时注解来操作下。这里我们先新建一个library取名lib-reflection

然后自定义注解,我们只实现了View与id的绑定功能,所以我们这里定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    
    int value();
}

Target的Type说明这是一个用于修饰类和接口的注解,它也就是成员变量,即需要绑定资源id的view成员。

同时这个注解Retention是RUNTIME,表示注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。

注解定义好后就可以直接在项目中使用了;

public class MainActivity extends AppCompatActivity {
    
 
    @BindView(R.id.textView)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
      	Binding.bind(this);
       textView.setText("哈哈哈哈");
    }
}

注意这里我们加了个Binding.bind(this),就是它来帮助我们做注解的解析,之后在内部调用MainActivity.textView=(TextView)MainActivity.findViewById()来实现为view绑定id的。还是在刚才的目录lib-reflect下创建了Binding类,具体代码如下:

public class Binding {
    
    public static void bind(Activity activity){
    
        //反射获取注解注释
        for (Field field: activity.getClass().getDeclaredFields()){
    
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null){
    
                try {
    
                    //扩大范围
                    field.setAccessible(true);
                    field.set(activity, activity.findViewById(bindView.value()));
                } catch (IllegalAccessException e) {
    
                    e.printStackTrace();
                }
            }
        }
    }
}

再运行就可以绑定了view的id了,不再需要我们手动的绑定id了。但是依靠反射始终是消耗性能的,这时候就用到我们的注解处理器了。

编译时注解

这里,我们新创建个Java Library的项目,就叫lib-processor吧。然后再这个目录下创建BindingProcessor,继承自AbstractProcessor。这个就是用于解析自定义注解的解析器了。不过要想让它生效还必须在resource下新建如下目录(现在google给提供了一个注册处理器的库@AutoService(Processor.class)的注解来简化我们的操作。):

image.png

javax.annotation.processing.Processor的文本文件里面内容就一行:

com.pince.lib_processor.BindingProcessor

接下来将之前定义的BindView注解改为编译时注解,放进新创建的lin-annotations目录下:

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

还需要修改build.gradle文件,加入:

app依赖:

implementation project(':lib')
annotationProcessor project(':lib-processor')

lib-processor依赖:

implementation project(':lib-annotations')

lib依赖:

//主项目需要的依赖
api project(':lib-annotations')

这么做就是为了让编译器使用我们的解析器用于解析注解。

后面的的工作都是在BindingProcessor操作了。通过读取类中的自定义注解,生成相应的绑定视图的代码,还需要引入一个库。

compile 'com.squareup:javapoet:1.9.0'

先展示下最后自动生成的类:

public class MainActivityBinding {
    
  public MainActivityBinding(MainActivity activity) {
    
    activity.textView = activity.findViewById(2131231093);
  }
}

上面的内容就是由javapoet生成的,下面就按照上面这个最终效果来一步一步分析要怎么生成我们的代理类。

看下面我们需要创建一个构造函数<? extend Activity>作为参数传入:

String packageStr = element.getEnclosingElement().toString();
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(ClassName.get(packageStr, classStr), "activity");

然后查找所有标注了BidView注解的成员变量:

 for (Element enclosedElement: element.getEnclosedElements()){
    
                if (enclosedElement.getKind() == ElementKind.FIELD){
    
                    //寻找BindView注释
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if (bindView != null){
    
                        ....
                    }
                }
            }

再然后是生成findViewById的具体语句代码:

for (Element enclosedElement: element.getEnclosedElements()){
    
                if (enclosedElement.getKind() == ElementKind.FIELD){
    
                    //寻找BindView注释
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if (bindView != null){
    
                        hasBinding = true;
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    }
                }
            }

做好了上面步骤,主要的代码也写完了,最后就是生成这个MainActivityBinding这个类:

String packageStr = element.getEnclosingElement().toString();
ClassName className = ClassName.get(packageStr, classStr + "Binding");
TypeSpec builtClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorBuilder.build())
                    .build();
try {
    
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                } catch (IOException e) {
    
                    e.printStackTrace();
                }

注意这里的包名,生成的类的包名尽量与需要绑定的Activity所在的包名一致,这样BindView修饰的成员变量只需是包内可见就行。到了这里我们再执行./gradlew :app:compileDebugJava编译就可以自动生成我们所需要的类了。
到了这一步还没完,我们还需要在lib moodule目录下创建新的Binding帮助类:

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

在这里利用了一点反射new出了MainActivityBinding实体,传入相应的activity在内部进行绑定操作。到这里简单的ButterKnife版本就实现了。下面BindingProcessor给出完整代码:

public class BindingProcessor extends AbstractProcessor {
    

    Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
    
        super.init(processingEnv);
      //使用Filer你可以创建文件
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    
				//获取到全部的类
      	//Element代表的是源代码
   			// Element可以是类、方法、变量等
        for (Element element: roundEnv.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");
            boolean hasBinding = false;
  				//还有个getEnclosingElement  单数 被包住的外层
            //子类里的元素  字段 方法 内部类
            for (Element enclosedElement: element.getEnclosedElements()){
    
              //仅获取成员变量
                if (enclosedElement.getKind() == ElementKind.FIELD){
    
                    //寻找BindView注解
                    BindView bindView = enclosedElement.getAnnotation(BindView.class);
                    if (bindView != null){
    
                        hasBinding = true;
                      //在构造方法中加入代码
                        constructorBuilder.addStatement("activity.$N = activity.findViewById($L)",
                                enclosedElement.getSimpleName(), bindView.value());
                    }
                }
            }

            TypeSpec builtClass = TypeSpec.classBuilder(className)
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(constructorBuilder.build())
                    .build();

            if (hasBinding){
    
                try {
    
                  //生成 Java 文件
                    JavaFile.builder(packageStr, builtClass)
                            .build().writeTo(filer);
                } catch (IOException e) {
    
                    e.printStackTrace();
                }
            }
        }

        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
    
        //对这个注解进行注解处理
        return Collections.singleton(BindView.class.getCanonicalName());
    }
}

源码地址APTDemo

注解后话

元注解是用来定义其他注解的注解(在自定义注解的时候,需要使用到元注解来定义我们的注解)。java.lang.annotation提供了四种元注解:@Retention@Target@Inherited@Documented

元注解 说明
@Target 表明我们注解可以出现的地方。是一个ElementType枚举
@Retention 这个注解的的存活时间
@Document 表明注解可以被javadoc此类的工具文档化
@Inherited 是否允许子类继承该注解,默认为false

@Target

@Target-ElementType类型 说明
ElementType.TYPE 接口、类、枚举、注解
ElementType.FIELD 字段、枚举的常量
ElementType.METHOD 方法
ElementType.PARAMETER 方法参数
ElementType.CONSTRUCTOR 构造函数
ElementType.LOCAL_VARIABLE 局部变量
ElementType.ANNOTATION_TYPE 注解
ElementType.PACKAGE

@Retention

表示需要在什么几倍保存该注释信息,用于描述注解的生命周期。

@Retention-RetentionPolicy类型 说明
RetentionPolicy.SOURCE 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃
RetentionPolicy.CLASS 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
RetentionPolicy.RUNTIME 解不仅被保存到class文件中,jvm加载class文件之后,仍然存在

@Document

@Document表明我们标记的注解可以被javadoc此类的工具文档化

@Inherited

@Inherited表明我们标记的注解是被继承的。比如,如果一个父类使用了@Inherited修饰的注解,则允许子类继承该父类的注解

注解解析

Class类里面常用方法如下:

/**
     * 包名加类名
     */
    public String getName();

    /**
     * 类名
     */
    public String getSimpleName();

    /**
     * 返回当前类和父类层次的public构造方法
     */
    public Constructor<?>[] getConstructors();

    /**
     * 返回当前类所有的构造方法(public、private和protected)
     * 不包括父类
     */
    public Constructor<?>[] getDeclaredConstructors();

    /**
     * 返回当前类所有public的字段,包括父类
     */
    public Field[] getFields();

    /**
     * 返回当前类所有申明的字段,即包括public、private和protected,
     * 不包括父类
     */
    public native Field[] getDeclaredFields();

    /**
     * 返回当前类所有public的方法,包括父类
     */
    public Method[] getMethods();

    /**
     * 返回当前类所有的方法,即包括public、private和protected,
     * 不包括父类
     */
    public Method[] getDeclaredMethods();

    /**
     * 获取局部或匿名内部类在定义时所在的方法
     */
    public Method getEnclosingMethod();

    /**
     * 获取当前类的包
     */
    public Package getPackage();

    /**
     * 获取当前类的包名
     */
    public String getPackageName$();

    /**
     * 获取当前类的直接超类的 Type
     */
    public Type getGenericSuperclass();

    /**
     * 返回当前类直接实现的接口.不包含泛型参数信息
     */
    public Class<?>[] getInterfaces();

    /**
     * 返回当前类的修饰符,public,private,protected
     */
    public int getModifiers();

Field和Method都实现了AnnotatedElement接口,常用方法如下:

/**
     * 指定类型的注释是否存在于此元素上
     */
    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
    
        return getAnnotation(annotationClass) != null;
    }

    /**
     * 返回该元素上存在的指定类型的注解
     */
    <T extends Annotation> T getAnnotation(Class<T> annotationClass);

    /**
     * 返回该元素上存在的所有注解
     */
    Annotation[] getAnnotations();

    /**
     * 返回该元素指定类型的注解
     */
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
    
        return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
    }

    /**
     * 返回直接存在与该元素上的所有注释(父类里面的不算)
     */
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {
    
        Objects.requireNonNull(annotationClass);
        // Loop over all directly-present annotations looking for a matching one
        for (Annotation annotation : getDeclaredAnnotations()) {
    
            if (annotationClass.equals(annotation.annotationType())) {
    
                // More robust to do a dynamic cast at runtime instead
                // of compile-time only.
                return annotationClass.cast(annotation);
            }
        }
        return null;
    }

    /**
     * 返回直接存在该元素岸上某类型的注释
     */
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
    
        return AnnotatedElements.getDirectOrIndirectAnnotationsByType(this, annotationClass);
    }

    /**
     * 返回直接存在与该元素上的所有注释
     */
    Annotation[] getDeclaredAnnotations();

注解处理器后话

注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册相应的注解处理器(自定义的注解处理器需继承自AbstractProcessor)。

定义一个注解处理器,需要继承自AbstractProcessor。如下所示:

public class MyProcessor extends AbstractProcessor {
    

  /**
     * 每个Annotation Processor必须有一个空的构造函数。
     * 编译期间,init()会自动被注解处理工具调用,并传入ProcessingEnvironment参数,
     * 通过该参数可以获取到很多有用的工具类(Element,Filer,Messager等)
     */
    @Override
    public synchronized void init(ProcessingEnvironment env){
     }

  /**
     * 用于指定自定义注解处理器(Annotation Processor)是注册给哪些注解的(Annotation),
     * 注解(Annotation)指定必须是完整的包名+类名
     */
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) {
     }

  /**
     * 用于指定你的java版本,一般返回:SourceVersion.latestSupported()
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
     }

  /**
     * Annotation Processor扫描出的结果会存储进roundEnvironment中,可以在这里获取到注解内容,编写你的操作逻辑。
     * 注意:process()函数中不能直接进行异常抛出,否则程序会异常崩溃
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
     }

}

注处理器的核心是process()方法,而process方法的核心是Element元素。Element里的元素可以是类、方法或是变量。在操作注解的过程中,编译器会扫描所有的Java文件,并将每一个部分看做是特定类型的的Element。可以代表包、类、接口、方法、字段等多种元素种类。

Element子类 解释
TypeElement 类或接口元素
VariableElement 字段、enum常量、方法或构造方法参数、局部变量或异常参数元素
ExecutableElement 类或接口的方法、构造方法,或者注解类型元素
PackageElement 包元素
TypeParameterElement 类、接口、方法或构造方法元素的泛型参数

Element类常用方法如下:

    /**
     * 返回此元素定义的类型,int,long这些
     */
    TypeMirror asType();

    /**
     * 返回此元素的种类:包、类、接口、方法、字段
     */
    ElementKind getKind();

    /**
     * 返回此元素的修饰符:public、private、protected
     */
    Set<Modifier> getModifiers();

    /**
     * 返回此元素的简单名称(类名)
     */
    Name getSimpleName();

    /**
     * 返回封装此元素的最里层元素。
     * 如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素;
     * 如果此元素是顶层类型,则返回它的包;
     * 如果此元素是一个包,则返回 null;
     * 如果此元素是一个泛型参数,则返回 null.
     */
    Element getEnclosingElement();

    /**
     * 返回此元素直接封装的子元素
     */
    List<? extends Element> getEnclosedElements();

    /**
     * 返回直接存在于此元素上的注解
     * 要获得继承的注解,可使用 getAllAnnotationMirrors
     */
    List<? extends AnnotationMirror> getAnnotationMirrors();

    /**
     * 返回此元素上存在的指定类型的注解
     */
    <A extends Annotation> A getAnnotation(Class<A> var1);

还有四个帮助类也是需要我们了解的:

  • Elements:一个用来处理Element的工具类
  • Types:一个用来处理TypeMirror的工具类
  • Filer:用于创建文件(比如创建class文件)
  • Messager:用于输出,类似printf函数

参考

自定义注解和解析器实现ButterKnife

Android 自定义注解(Annotation)

Java注解处理器

JavaPoet源码初探

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/Lebron_xia/article/details/121448194

智能推荐

window中使用bat启动springboot项目,并解决乱码问题_bat启动springbootjar包_秋9的博客-程序员宝宝

springboot项目打包jar,编写bat启动jar脚本,并解决bat控制台中文乱码问题

HPU 3639--Hawk-and-Chicken【SCC缩点反向建图 && 求传递的最大值】_hawk and chicken_阿阿阿阿_欢的博客-程序员宝宝

Hawk-and-ChickenTime Limit: 6000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 2409    Accepted Submission(s): 712Problem DescriptionKids in kinder

不要再学java了,别人都在说你是傻子_Java架构-大仙的博客-程序员宝宝

就算说学Java的都是傻子,那也是拿着高薪,傲娇地鄙视着那些啥也不学或者没有深入的了解java的人。自己的问题,永远不要推给行业,再不好的行业,也会有牛的人,那个人为什么不是你呢?为何说学Java的都是傻子?第一点,问题的出处说出这个问题的人应该是一个刚入门的新手,估计是刚看完java相应的知识不久,我这里想说的是java只是一门语言,是一门基础性的语言,重点在基础两个字,现在来说应用的相对较广,知识点就那么多,但在应用上就会变得很多,有很多东西可能你还没学到,算法?框架?就像是高数,知识点就那.

JBoss Rule Engine - Drools (1)_itkbase的博客-程序员宝宝

[緣起]上次和 JBoss Ben Wang 閒聊時, 因為不知道 JBoss Rule Engine - Drools 而被嘲笑了一下, 可能從未實際接觸過 Rule Engine 的領域, 所以就只能馬上查閱資料惡補...[RuleEngine]當今技術前端應用應該就屬 ajax 最火紅, 加上 MVC Framework 的整合, 應該就可以快速處理許多系統.而後端技術應用就屬 Hibe

Mybatis使用报错org.apache.ibatis.exceptions.PersistenceException:_at org.apache.ibatis.exceptions.exceptionfactory.w_30醒悟的码农的博客-程序员宝宝

使用Mybatis中使用Mapper动态代理方式测试报以下错误:org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: org.apache.ibatis.reflection.ReflectionException: Error instantiating class ...

运行braker_java31的博客-程序员宝宝

#1.输入数据# genome:# genome.fasta## rnaseq data:# brevis.rna.1.fastq.gz# brevis.rna.2.fastq.gz## homolog.fasta#2.bam文件准备hisat2-build -p 20 genome.fasta brevishisat2 -p ...

随便推点

Golang中的defer关键字的用法、原理以及它的坑_会写代码的鱼的博客-程序员宝宝

延迟函数调用(deferred function call)是golang中很有特点的一个功能,通过defer修饰的函数调用会在函数退出的时候才被真正调用,它可以用来进行资源释放等收尾工作。一个普通的函数调用被defer关键字修饰以后,就构成了一个延迟函数调用,和协程调用类似,被延迟的函数调用的所有返回值都会全部被舍弃。延迟函数调用的用法首先我们看看延迟函数调用的用法,它的用法其实很简单!...

sqldbx连接db2数据库_sqldbx 连接db2_qq_42033422的博客-程序员宝宝

点击文件–新建连接弹出如下图–选择IBM DB2 LUW然后点选择你自己要链接的服务器,双击即可输入用户名密码即可连接

requests 模块的 requests.session() 功能_python编程的博客-程序员宝宝

之前使用 requests 模块的时候,是直接 requests.get() 或者 requests.post() 发送GET请求或POST请求;当然也是可以带上 cookies 和 headers 的,但这都是一次性请求,你这次带着cookies信息,后面的请求还得带。这时候 requests.session() 就派上用场了,它可以自动处理cookies,做状态保持。使用示例:# 先实例化一个对象session = requests.session()# 后面用法和直接使用request

C语言执行时,程序控制台输出窗口 一闪而过的问题!_c语言输出结果一闪而过__子沐_的博客-程序员宝宝

有时候,我们在写C时,执行会遇到控制台窗口一闪而过的情况。首先,我们来看一段代码:#include&amp;lt;stdio.h&amp;gt;void main(){ printf(&quot;--------------hello world!---------&quot;);}这段代码运行就会出现上述情况。如何解决呢?解决办法:法一、getchar();在主函数的最后加上getchar()法二、system(&quot;pau...

运行python程序最常用、最重要的方法_如何让ython运行地超快的10个方法,新手必学|python基础教程|python入门|python教程..._weixin_39605455的博客-程序员宝宝

但也不要过于担心 python 的速度,这并不一定是一个非此即彼的命题。经过适当优化,Python 应用程序可以以惊人的速度运行 —— 也许还不能达到 Java 或 C 语言的速度,但是对于 Web 应用程序、数据分析、管理和自动化工具以及大多数其他用途来说,速度已经足够快了。快到你可能会忘记了你是在用应用程序性能换取开发人员的生产力。优化 Python 性能不能单从一个角度上看。而是应用所有可用...

Webpack 入门指迷--转载(题叶)_weixin_30383279的博客-程序员宝宝

最近看到这个东西,一头雾水。看了一些资料了解了Webpack概念,大体是webpack 是一个模块绑定器,主要目的是在浏览器上绑定 JavaScript 文件。看到题叶写的一篇介绍,写的很好,转载连接http://segmentfault.com/a/1190000002551952转载于:https://www.cnblogs.com/kuailingmin/p/4420427.h...

推荐文章

热门文章

相关标签