概述
我们都听过或者用过butterknife,知道butterknife是一款View注入的框架。在android开发时省去我们重复的敲打findViewById等方法。ButterKnife源码的分析将会先行讲解注解,反射,注解处理器,再深入分析源码。
注解
说起注解,大家都很熟悉。不管是Java语言本身自带的@Override、@Override,还是热门的第三方框架butterknife的@BindView、retrofit的@Get等
Java自带的注解
普通注解-用于描述代码的注解
- @Override - 检查方法是否是重载。如果在父类或者实现的接口里没有找到原型的话,编译器就会报错。
- @Deprecated - 标记该方法已弃用。如果使用的话,编译器会提示。
- @SuppressWarnings - 使编译器忽略编译时的警告,使用如@SuppressWarnings(“unused”),忽略无用代码的提示
元注解-注解的注解
元注解-meta-annotation,即注解用来注解其他注解的注解,常用来自定义注解。
栗子:
元注解有四种,栗子里都用到,分别为meta-annotation,Document、Target、Retention、Inherited:
@Document
Document标记这个注解应该被javadoc工具记录。默认情况下,Javadoc是不包括注解的。
@Target
Target描述了这个注解的使用范围,使用方法如@Target(ElementType.TYPE),ElementType的取值有七种,如下:
- CONSTRUCTOR:用于描述构造器
- FIELD:用于描述域
- LOCAL_VARIABLE:用于描述局部变量
- METHOD:用于描述方法
- PACKAGE:用于描述包
- PARAMETER:用于描述参数
- TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retention
Retention用来描述这个注解的生命周期,即注解的”存活时间”,使用方法如@Retention(RetentionPolicy.RUNTIME),RetentionPolicy有三种,如下:
- SOURCE: 将被编译器丢弃(即源文件保留)
- CLASS: 在class文件中可用,但会被JVM丢弃(即class保留)
- RUNTIME: 在运行时有效(即运行时保留)
我们知道,Java代码会由源代码编译成class文件,然后再运行。Retention用来描述注解在这个阶段的存活时间。例如,当一个注解为RUNTIME时,表明我们可以在运行时通过反射获取到注解的内容,然后再去做相应的处理。
@Inherited
Inherited译为可继承的,如果一个使用了@Inherited 修饰的 annotation类型 被用于一个 class,则这个 annotation 将被用于该class的子类。
认识这四个元注解,我们就能定义属于自己的注解!
自定义注解
使用@interface自定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组)。可以通过default来声明参数的默认值。
值得一提的是,如果只有一个参数成员,最好把参数名称设为”value”,这样子,这样做有个好处。例如注解 A,正常调用时这样子:@A(value=”jerry”),可以简化为@A(“jerry”)。
这里我举两个栗子来进行介绍,一个是butterknife中的BindView注解,一个是项目中使用到的注解。
butterknife的BindView
|
|
BindView注解用来冗杂的findViewById操作,使用如下:
@Retention(CLASS)表明了BindView在class文件中保留,在runtime时不存在,这意味着butterknife是在编译时生成绑定View的代码,因此butterknife在运行时不会造成影响,只会影响编译时的速度,这也解释了butterknife受欢迎的原因。
@Target(FIELD)表明该注解是用来修饰域变量的,如上面栗子所示。
@IdRes int value(); @IdRes是android.support.annotation里定义的注解,表示只接受类似R.id.xxx形式(即为int的Id)的参数。
反射
反射机制允许在运行时发现和使用类的信息。Class类与java.lang.reflect一起对反射的概念进行了支持,允许在运行时利用反射达到:
- 判断任意一个对象所属的类;
- 构造任意一个类的对象;
- 判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
- 调用任意一个对象的方法
使用反射来模仿ButterKnife
这里使用反射来模仿下ButterKnife来简化这些代码。当然ButterKnife不是用反射的,它是使用注解处理器的
先定义个BindView注解:
调用方法:
在声明TextView时使用BindView注解,然后setContentView后,使用NewerKnife.bind(this),这样就可以避免繁琐的findViewById操作。
findViewById的操作都在NewerKnife.bind(this)这里面使用反射解决了。下面是实现的代码:
注解处理器-AbstractProcessor
前面说到的反射是在运行时处理注解,有些人可能担心反射对应用的性能有大的影响。其实通过注解处理器可以在编译时扫描注解,处理注解,包括生成代码等,这样就不会对应用的性能造成影响。前面我们提到的ButterKnife就是通过注解处理器来的。
Element
在Java中,将源代码看成Element,Element的子接口有ExecutableElement, PackageElement, Parameterizable, QualifiedNameable, TypeElement, TypeParameterElement, VariableElement。通过下面的栗子就可以直观的理解:
TypeMirror
TypeMirror表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。
Element代表的是源代码。举个栗子,TypeElement代表源代码中的类或接口的元素,是未经编译的。也就是说我们无法根据TypeElement获取类的信息,例如类里面的方法、域、超类等信息。而通过TypeElement.asType()得到的TypeMirror就可以知道。
AbstractProcessor介绍
AbstractProcessor有四个比较重要的方法:
init
初始化处理器,提供以下工具类
- Elements:一个用来处理Element的工具类,例如获取元素的包和注释等。
- Types:一个用来处理TypeMirror的工具类,例如获取类型的超类型等。
- Filer:顾名思义,使用Filer你可以创建文件。
- Messager:用来打印输出日志信息。
process
扫描处理注解,生成代码,必须重载。入参介绍:
|
|
如果process返回true,说明不需要后续的Processor处理它;返回false则需要。
getSupportedAnnotationTypes
返回你需要处理的注解集合,可以@SupportedAnnotationTypes代替。
getSupportedSourceVersion
指定Java版本,可以用@SupportedSourceVersion代替。
AbstractProcessor使用
|
|
|
|
万事俱备只欠东风,我们需要将AbstractProcessor注册到编译器中。由于AbstractProcessor是属于javax.annotation.processing包的,因此需要新建一个依赖的Java Library。在这个library里,在src/main/resources/META-INF/services里需要有javax.annotation.processing.Processor这个文件,里面放了注解处理器的路径,例如:
ButterKnife源码分析
上文我们详细介绍了注解处理器,这里再结合butterknife再次强调下。在编译源文件时,会分析扫描注解,当扫描到butterknife定义的@BindView、@OnClick等注解时,会使用JavaPoet来生成代码。生成后的文件会再次分析,直到没有分析到需要处理的注解位置。
JavaPoet简介
Poet译为诗人,JavaPoet可以帮助便捷地生成代码,而不是手动繁琐的拼接语句。简要介绍下比较关键的几个类:
- MethodSpec 代表一个构造函数或方法声明。
- TypeSpec 代表一个类,接口,或者枚举声明。
- FieldSpec 代表一个成员变量,一个字段声明。
ButterKnife效果
源文件:
编译生成的文件可以在build/source/apt下可以看到:
注意到源文件名是MainActivity,而生成的文件是MainActivity_ViewBinding。
在构造函数内,使用
|
|
利用Utils.findRequiredViewAsType得到的结果赋值到我们定义的TextView(tvTitle)上,这也解释了为什么tvTitle需要用public来修饰。
进一步看看findRequiredViewAsType。
|
|
castView方法是将得到的View转化成具体的子View,这里是TextView。而findRequiredView里进行了findViewById的操作。
这里我们可能会有疑问,在生成的MainActivity_ViewBinding的构造方法使用到MainActivity,而我们在使用ButterKnife时会使用ButterKnife.bind(this)将Activity传递到ButterKnife里,这之间是怎么一个过程?
进去ButterKnife.bind(this)看看。
|
|
根据activity得到DecorView,再传递到createBinding。
上面的代码里,获取到Constructor后,再运用反射生成实例,在实例里的findView操作就会被调用到。接下来看看如何获取到Constructor的。
|
|
从上面可以看到Constructor获取的过程,根据className得到className_ViewBinding,就可以得到Constructor。并且会将得到的Constructor缓存起来,避免反射的性能问题。
这样一来,ButterKnife.bind(this)传递进去的MainActivity会通过反射生成MainActivity_ViewBinding实例。在这个实例的构造函数内,进行findViewById、setOnclickListener等操作
编译过程
MainActivity在编译时会生成处理findViewById等操作的MainActivity_ViewBinding。接下来我们探索下ButterKnife偷偷在注解处理器里做了什么。
从process方法入手:
|
|
process里主要做了两件事情:
- findAndParseTargets
获得TypeElement -> BindingSet的映射关系,TypeElement指的是类或接口,在本文所举的栗子中是MainActivity。BindingSet里包含了生成代码时的一些参数。 - 运用JavaPoet框架来生成代码
生成的代码类形式为xxxx_ViewBinding
深层的具体代码就暂不分析了,从宏观上把握以下ButterKnife的内部构造。。。。。