ButterKnife
开发过 Android 的肯定都知道曾经有这么一个库,它能够让你不用再写 findViewById
这样的代码,这就是大名鼎鼎的 ButterKnife(https://github.com/JakeWharton/butterknife)。虽然现在这个库已经不再维护,最后的版本 10.2.3 也停留在了2020年8月份,但这个库当时公布时,其影响力还是不容小觑的。
这个库是怎么个用法我们待会再看,先看 ButterKnife 的依赖方式。与引用其他的库不同的是,使用 ButterKnife 需要使用如下的依赖配置:
dependencies {implementation 'com.jakewharton:butterknife:10.2.3'annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}
一般来说,我们在项目中添加依赖时,一般会用到 implementation
或 api
,那么这里的 annotationProcessor
是啥呢?为啥用 ButterKnife 需要使用这种方式的依赖呢?
这个问题~~问得好,那我们就先来回答这个问题。
Gradle 的五种依赖配置
目前 Gradle 版本支持的依赖配置有:implementation
、api
、compileOnly
、runtimeOnly
和 annotationProcessor
,此外依赖配置还可以加一些配置项,例如AndroidTestImplementation
、debugApi
等等。而其中常用的依赖配置是 implementation
,api
和 compileOnly
。
关于这几个依赖配置的区别可以看下图:
通过个图我们看到,使用 annotationProcessor
是注解处理器的依赖配置,如果在项目中使用注解处理器,那么就需要通过 annotationProcessor
来进行依赖。
那么问题就来了,注解处理器有什么用了,ButterKnife 除了要依赖一个库,为什么还必须要依赖一个注解处理器呢?
这又是一个好问题,不过在此处我先通俗的简单解释一下什么是注解处理器,也就是APT,即 Annotation Processing Tool。
简单解释 APT
正常 java 文件的编译流程
众所周知,生成 class 文件都是通过 javac 命令,例如我们编译 Hello.java
文件:
package lic.first;public class Hello {public static void main(String[] args){System.out.println("Hello APT");}
}
编译这个 Hello.java
文件,我们只需要使用如下的命令:
javac -d out lic/first/Hello.java
便可以在 out 文件夹中生成 Hello.class
,通过使用 java 命令,我们可以看到 这个 Hello.class
是可以运行的:
java -cp out lic.first.Hello
Hello APT
这是一个 java 文件的正常编译流程,也是没有添加注解处理器时的 java 代码编译流程。
那么,如果添加了注解处理器,编译流程是怎么样的呢?这里我先放上一张图,然后再用示例来解释。
加入 APT 之后的编译流程
通过图上可以看出,javac 在编译时如果碰到注解处理器,就会先根据注解处理器生成一些新的 java 代码,然后再进行编译。
现在,我们修改一下上面的例子做一下演示。
创建自定义注解
先添加一个 Java 的 Annotation
,这个注解是自定义的:
package lic.first.anno;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface FirstAnno {int value();
}
然后咱们再修改一下 Hello.java
文件:
package lic.first;import lic.first.anno.FirstAnno;public class Hello {@FirstAnno(9527)public static void main(String[] args){System.out.println("Hello APT");}
}
后面我们将通过这个 FirstAnno
的自定义注解来生成一个 java 文件。现在,自定义的注解已经准备好了,也使用到了代码中,那么现在万事俱备只欠东风,这个东风就是告知 javac 如何生成 java 文件的类。现在,我们把上面的两个文件先放一放,看一下如何生成一段 java 代码,即注解处理器。
创建注解处理器
注解处理器是一个继承自 AbstractProcessor
的类,下面我们就创建它。
package lic.first.apt;import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.TypeElement;public class FirstAPT extends AbstractProcessor {@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {return true;}
}
有人可能不懂为什么要继承自 AbstractProcessor
,但这其实就是 Java 的一个设计,就像你写 MainActivity
也需要继承自 Activity
一样。AbstractProcessor
是一个抽象类,它实现了 javax.annotation.processing.Processor
接口,专门用于在编译期处理注解,可以把它想象为 javac 的一个插件。
简单介绍 AbstractProcessor
现在就介绍这个类里面的几个核心的方法:
void init(ProcessingEnvironment processingEnv)
看名字就知道这是个初始化方法,这个方法可以让我们的处理器进行初始化,通过参数 ProcessingEnvironment
可以来获取一些帮助处理注解的工具类。例如:
// Element操作类,用来处理Element的工具
Elements elementUtils = processingEnv.getElementUtils();
// 类信息工具类,用来处理TypeMirror的工具
Types typeUtils = processingEnv.getTypeUtils();
// 日志工具类,因为在process()中不能抛出一个异常,那会使运行注解处理器的JVM崩溃。所以Messager提供给注解处理器一个报告错误、警告以及提示信息的途径,用来写一些信息给使用此注解器的第三方开发者看
Messager messager = processingEnv.getMessager();
// 文件工具类,常用来读取或者写资源文件
Filer filer = environment.getFiler();
Set<String> getSupportedAnnotationTypes()
此方法用来指定需要处理的注解集合,返回的集合元素需要是注解全路径(包名+类名)。如果没有覆写这个方法,在进行 javac 编译时,会有如下警告:
//未覆写时 No SupportedAnnotationTypes annotation found on lic.first.apt.FirstAPT, returning an empty set.
SourceVersion getSupportedSourceVersion()
此方法用来指定当前正在使用的 Java 版本,一般返回 SourceVersion.latestSupported()
表示最新的 java 版本即可。如果没有覆写这个方法,在进行 javac 编译时,会有如下警告:
//警告: No SupportedSourceVersion annotation found on lic.first.apt.FirstAPT, returning RELEASE_6.
//警告: 来自批注处理程序 'lic.first.apt.FirstAPT' 的受支持 source 版本 'RELEASE_6' 低于 -source '22'
boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
此方法是注解处理器的核心方法,注解的处理和生成代码或者配置资源都是在这个方法中完成。
Java官方文档给出的注解处理过程的定义:注解处理过程是一个有序的循环过程。在每次循环中,一个处理器可能被要求去处理那些在上一次循环中产生的源文件和类文件中的注解。
每次循环都会调用process
方法,process
方法提供了两个参数,第一个是我们请求处理注解类型的集合(也就是我们通过重写getSupportedAnnotationTypes
方法所指定的注解类型),第二个是有关当前和上一次循环的信息的环境。返回值表示这些注解是否由此 Processor
声明,如果返回 true
,则这些注解已声明并且不要求后续 Processor
处理它们;如果返回 false
,则这些注解未声明并且可能要求后续 Processor
处理它们。
在了解了 AbstractProcessor
类的几个核心的方法后,我们就可以开始编写自己的 APT 了。
第一个注解处理器
写第一个注解处理器时,我先不打算在注解处理器中添加有实际功能的代码,步子跨得大了容易扯着蛋,一步一来,不着急。下面是咱们的第一个注解处理器:
package lic.first.apt;import java.util.Collections;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;import lic.first.anno.FirstAnno;public class FirstAPT extends AbstractProcessor {@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " init");super.init(processingEnv);}@Overridepublic SourceVersion getSupportedSourceVersion() {processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " getSupportedSourceVersion");return SourceVersion.RELEASE_22;}@Overridepublic Set<String> getSupportedAnnotationTypes() {processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " getSupportedAnnotationTypes");return Collections.singleton(FirstAnno.class.getName());}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " process annotations = " + annotations);return true;}
}
可以看到这个注解处理器中只有 Log
输入,这些日志会在我们执行 javac 命令的时候显示出来,下面我们就编译和使用这个注解,看看 javac 会给我们输出什么东西。
编译注解处理器
我们的第一个注解处理器其实就是一个 java 文件,编译它跟编译其他的 java 文件没有什么区别:
javac -d out lic/first/apt/FirstAPT.java
这里由于 FirstAPT
是依赖于 FirstAnno
的,因此 FirstAnno.java
这个类也被 javac 隐式编译了。
隐式编译(implicit compilation)是指在编译某个Java源文件时,编译器自动发现并编译该文件所依赖的其他Java源文件,而无需显式地指定这些依赖文件。
使用注解处理器
现在注解处理器已经准备好了,那么现在就可以使用它来编译 Hello.java
了:
javac -d out lic/first/Hello.java -processor lic.first.apt.FirstAPT --processor-path out注: lic.first.apt.FirstAPT@71623278 init
注: lic.first.apt.FirstAPT@71623278 getSupportedSourceVersion
注: lic.first.apt.FirstAPT@71623278 getSupportedAnnotationTypes
注: lic.first.apt.FirstAPT@71623278 process annotations = [lic.first.anno.FirstAnno]
注: lic.first.apt.FirstAPT@71623278 process annotations = []
警告: 批注处理不适用于隐式编译的文件。使用 -implicit 指定用于隐式编译的策略。
1 个警告
看到没有,我们的第一个APT被 javac 执行了,init
方法用于初始化被调用了一次,getSupportedSourceVersion
和 getSupportedAnnotationTypes
方法也都被调用,process
被调用两次,说明这次编译时在处理注解时产生了两个循环。最后还有一个警告,这是因为 Hello.java
依赖于 FirstAnno
,这导致了 FirstAnno
被隐式编译,而 javac 在处理隐式编译的文件时,不会调用注解处理器。
总而言之,就是我们第一个APT运行成功。再看一下 javac 的命令 -processor
指定了要注解处理器的程序,--processor-path
指定了注解处理器存放的位置。如果想看编译时的更多信息,还可以加上 -verbose
选项。
APT编译流程之总结
这也就是加入 APT 之后的编译流程。简单来说就是分两段,第一段先编译 APT;第二段编译使用注解的代码,编译时把 APT 添加到 javac 的选项里。再回过头来看 Gradle 中的 annotationProcessor
依赖配置,它不过是在编译时告诉 javac 我这后面跟的依赖包是传给你 -process
选项里的注解处理器而已。ButterKnife 中的 com.jakewharton:butterknife-compiler:10.2.3
就是这样的一个注解处理器,这下是不是就懂了。
OK,APT流程终于结束了,这里先放一张图片休息一下,后面咱们再聊聊怎么把APT打包成 jar。
休息完之后有人要说了,别人的注解处理器都是一个 jar 包, 你这就一个 class 文件,这跟 ButterKnife 不一样啊。你看看人家的 ButterKnife 的 jar 包好歹也有 58KB。
这个问题~~~问得好,那么现在,我们就开始将我们的注解处理器封装成 jar 包。
将注解处理器打包成 Jar
使用 jar 命令打包
先说明,打包成 jar 很简单,只需要使用 jar 命令并将 FirstAPT.class
和 FirstAnno.class
文件传入即可。我们可以使用如下的命令:
jar -cvf first-apt.jar -C out lic/first/apt/FirstAPT.class -C out lic/first/anno/FirstAnno.class
这样就将 FirstAPT.class
打包成了 first-apt.jar,我们再查看一下这个 first-apt.jar 包里的内容:
jar tf first-apt.jarMETA-INF/
META-INF/MANIFEST.MF
lic/first/apt/FirstAPT.class
lic/first/anno/FirstAnno.class
可见生成的这个 jar 包是没问题的。不过问题又来了,这个 jar 包能交给 javac 用么?
答案是不能的,想想看,这个 jar 包给 javac,那 javac 如何知道你的注解处理器是哪个类呢,你起码得告诉 javac 注解处理器是哪个类吧,也就是说,你得注册一下 APT。
在 jar 包中注册APT的类
注册 APT 是很简单的,一个 jar 包中也只有 META-INF
文件夹可以用来注册跟这个 jar 包相关的信息的。APT 的注册是通过 SPI 机制实现,它的注册,就是在 META-INF/services
底下创建 javax.annotation.processing.Processor
文件,文件内容为自定义的处理器类。
这里有一个新的概念:SPI,不过这个概念先放一放,等后面我们再讲它。现在我们把专注点放到 APT 的注册上来。
先创建一个文件:META-INF/services/javax.annotation.processing.Processor
,然后把自己的 APT 的全类名添加进来:
lic.first.apt.FirstAPT
再生成 jar 包时,将这个文件打包到 jar 中,命令如下:
jar -cvf first-apt.jar -C out lic/first/apt/FirstAPT.class -C out lic/first/anno/FirstAnno.class -C . META-INF
这样生成的 jar 包就包含了 APT 的信息。我们先检查一下 jar 包中的内容:
jar -tf first-apt.jar META-INF/
META-INF/MANIFEST.MF
lic/first/apt/FirstAPT.class
lic/first/anno/FirstAnno.class
META-INF/services/
META-INF/services/javax.annotation.processing.Processor
可以看到 META-INF/services/javax.annotation.processing.Processor
这个文件,这就算是注册成功。
验证注解处理器 jar 包
前面通过 -processor
将注解处理器的 class 文件传入,现在我们更改一下命令,让 javac 使用我们刚生成的 first-apt.jar:
javac -d out lic/first/Hello.java --processor-path first-apt.jar
在编译时,javac 会在命令行中打印如下信息:
注: lic.first.apt.FirstAPT@710726a3 init
注: lic.first.apt.FirstAPT@710726a3 getSupportedSourceVersion
注: lic.first.apt.FirstAPT@710726a3 getSupportedAnnotationTypes
注: lic.first.apt.FirstAPT@710726a3 process annotations = [lic.first.anno.FirstAnno]
注: lic.first.apt.FirstAPT@710726a3 process annotations = []
警告: 批注处理不适用于隐式编译的文件。使用 -implicit 指定用于隐式编译的策略。
1 个警告
这与我们使用 FistAPT.class
进行编译时的输出相同,这也证明了,我们已经可以成功地在 JAR 包中注册注解处理器并确保 javac 编译器能够识别和使用它们。这也是 ButterKnife 的库的原理,如果我们解压它的 jar 包,会发现它在 META-INF/services/javax.annotation.processing.Processor
中注册了下面信息:
butterknife.compiler.ButterKnifeProcessor
这就是 ButterKnife 注册的注解处理器的全类名。
好的,这个流程完成之后我们再休息一下,后面将会把前面提到的 SPI 的坑填上,大家准备好。
简单理解 SPI
在上面一节中,我们在注册 APT 提到了一个概念:SPI。在文章的这一节,我们就详细讲解一下 SPI 这个概念。
SPI 是 Service Provider Interface 的简称,是JDK默认提供的一种将接口和实现类进行分离的机制。允许服务提供者在运行时动态地发现和加载服务实现。它使得开发者可以通过接口定义服务,并且允许服务提供者通过配置文件提供具体的实现,从而实现模块化和可扩展的设计。
SPI机制约定:当一个Jar包需要提供一个接口的实现类时,这个Jar包需要在META-INF/services
目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该Jar包META-INF/services/
里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
上面的说了一大堆,我估计大伙看得也云里雾里的,这个时候就需要举个例子,一个落地的栗子胜过理论的解说。
定义服务接口
打个比方,我现在是一个项目的甲方,在完成这个项目时,我需要其他部门能够完成这个项目的一部分。那么为了告知其他部门我需要他们完成什么功能,以及规范他们的提供的接口,我这边就需要定义这个服务接口,然后让其他人来实现这个接口。在这个例子中,我定义了一个服务接口,这个接口就一个方法,即打印字符串。其他部门作为服务提供者需要实现这个接口,打印不同的字符串。
package lic.first.service;public interface IService {void doPrint();
}
实现服务接口
现在有两个服务提供者,他们根据我们要求的接口,提供了各自版本的服务实现,如下。
package lic.first.service;public class FirstService implements IService {@Overridepublic void doPrint() {System.out.println("the First Service."); }
}
package lic.first.service;public class SecondService implements IService{@Overridepublic void doPrint() {System.out.println("the Second Service."); }
}
现在有了这两个服务实现,为了能够在运行时发现和使用这些接口的实现,我们需要先注册它们。
注册服务的实现
注册服务的视线是通过在 META-INF/services
目录下创建一个以接口全限定名为名称的文件,将自己的实现类全限定名写入其中,从而向 Java 虚拟机注册自己的服务实现。
在我们的这个例子中,就是在 META-INF/services
目录下创建一个名为 lic.first.service.IService
,内容为所有实现类的完整名称:
lic.first.service.FirstService
lic.first.service.SecondService
这样就注册完成了,在接下来的打包过程中,使用如下命令生成 jar 包:
jar -cf service.jar -C out lic/first/service/IService.class -C out lic/first/service/FirstService.class -C out lic/first/service/SecondService.class -C . META-INF/services/lic.first.service.IService
对于打包生成的 jar ,我们再检查一下里面的东西是否完整:
jar tf service.jar META-INF/
META-INF/MANIFEST.MF
lic/first/service/IService.class
lic/first/service/FirstService.class
lic/first/service/SecondService.class
META-INF/services/lic.first.service.IService
可见这里该有的东西都有,那么最后一步就是使用这个服务提供者提供的服务实现了。
服务的加载和使用
服务使用者通过Java标准API(ServiceLoader
类)加载服务提供者的实现,从而在运行时动态发现并使用服务。现在我们编译一个使用这个服务的代码,如下:
package lic.first;import java.util.ServiceLoader;
import lic.first.service.IService;public class DoService {public static void main(String[] args) {ServiceLoader<IService> loader = ServiceLoader.load(IService.class);for(IService eachService : loader) {eachService.doPrint();}}
}
现在通过 javac 命令将其编译为 class 文件:
javac -d out lic/first/DoService.java
现在我们已经有了要使用服务的类,也有了提供服务的包,下面就把它们放到一起运行起来:
java -cp out:service.jar lic.first.DoServicethe First Service.
the Second Service.
可见,系统找到了这两个服务提供者,并执行了他们各自的 doPrint
方法,这就是一个 SPI 例子实现的整个流程。此处注意 -cp
中的 out 指定了 DoService
这个 class 存放的位置,service.jar
指定了我们的服务提供者的实现,中间用冒号连接。
用一张图来概括 SPI 的运行机制,如下:
SPI 与 APT 的关系
接下来我们把 SPI 和 APT 联系在一起再看,会发现 APT 其实就是 SPI 的一个具体应用。为什么会这么说呢,前面说到,我们使用 APT 需要继承 javax.annotation.processing.Processor
,而这个接口就是 Java 提供的,我们可以继承来实现一套注解服务,这个接口跟 META_INF/service
下的文件名是一致的。那么谁来用我们写的注解处理器呢,那就是 javac 命令了,而且 javac 也是通过 ServiceLoader
来读取这些服务的。Java编译器运行javac编译java类之前读取这个清单文件,加载实现Processor
接口的所有类,执行里面的注解处理逻辑,而后再编译java代码。
另外 SPI 这种技术,在 Android 组件化开发架构中是有应用的。
而这时候有人就说了,使用 APT 好复杂啊,自己写也就算了,还需要自己注册。有没有更便捷的方式来做这种事情呢?这个东西还真有,那就是 Google 的 AutoService 库。
简单介绍 AutoService 库
AutoService 是 Google 开发一个自动生成 SPI清单文件的框架。其作用是自动生成SPI清单文件(也就是META-INF/services
目录下的文件)。如果不使用它就需要手动去创建这个文件、手动往这个文件里添加服务(接口实现),为了免去手动操作,才有了AutoService。否则我们就需要像上面的例子一样生成 SPI 清单文件。
上面说了,SPI 最经典的应用就是 APT,现在我们就使用 AutoService 库来演示这个库的用法。当前我们先离开命令行,打开 Android Studio,创建一个 Java 的 Module,注意,是 Java 的 Module。
首先,在 build.gradle
中添加对 AutoService 库的依赖:
plugins {id 'java-library'
}dependencies {annotationProcessor 'com.google.auto.service:auto-service:1.1.1'implementation 'com.google.auto.service:auto-service-annotations:1.0.1'
}
然后在你处理注解处理器类上方添加 @AutoService
注解即可,value
指定成 javax.annotation.processing.Processor
类,因为要生成的SPI清单文件(META-INF/services
下的文件)名称是
javax.annotation.processing.Processor
。这个 Processor
是 Java 内置的,javac 编译前默认的注解处理器接口。如果是我们自定义的接口就指定成自己的接口名,例如上面的 IService.class
。
package lic.swift.apt;import com.google.auto.service.AutoService;@AutoService(value = {Processor.class})
public class FirstProcessor extends AbstractProcessor {@Overridepublic synchronized void init(ProcessingEnvironment processingEnvironment) {processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " init");super.init(processingEnvironment);}@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " process");return false;}
}
刚才我们说写 APT 必须要在 java 的 module 中,而不能在 android library 中写,这是因为 AbstractProcessor
这个抽象类是 JDK SE中的,其实现了 Processor 接口,Android SDK将它删除了(因为不需要也用不着),所以Android Module里面是不存在的。这也说明为什么创建Java SE项目来编写APT代码。
这样在使用了 AutoService 注解之后,我们编译一下这个 module,就会发现服务已经被注册了:
注意看这个文件: META-INF/services/javax.annotation.processing.Processor
,这就是 AutoService 为我们注册的服务。是不是很简单,起码不用自己再注册服务了。
这里还可以说一下,其实 AutoService 这个库也是一个 APT。我们后面要开发的,也是一个注解处理器,只是在处理注解时生成的文件不同而已。
造个 ButterKnife 的轮子
有了前面的内容铺垫之后,我们现在就可以来自己实现一个 ButterKnife 了。在前面我们看到,要写一个 ButterKnife 起码要写两个库,一个包含注解的库(implementation
依赖方式),和一个处理注解的库(annotationProcessor
依赖方式)。那现在就一一实现它们。这也是整篇文章最难的部分了,大家抓好了。
定义注解 BindView
我们先创建一个 java library,然后添加如下的一个注解,这个注解就是后面我们需要处理的:
package lic.swift.anno;@Target(value = {ElementType.FIELD})
@Retention(value = RetentionPolicy.SOURCE)
public @interface BindView {int value();
}
BindView 是 ButterKnife 里面最常用的注解,这里我们就拿这个注解作为例子,自己来实现它。注意这里的元注解 Target
和 Retention
,设定了这个注解只能放到属性之上,且存在于源代码阶段。
ButterKnife 是怎么做的
有人肯定做到这里觉得有点卡住了,感觉懂得很多,但是做起来的,觉得不知道如何下手。既然不知道如何下手,那咱们先看一下 ButterKnife 是如何下手的,然后照着它的样子做就行了。
使用 ButterKnife 的方式如下:
class ExampleActivity extends Activity {@BindView(R.id.user) EditText username;@BindView(R.id.pass) EditText password;@Override public void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.simple_activity);ButterKnife.bind(this);// do something...}
}
上面看到了有个 BindView
的注解,这个注解我们是完成了的。下面在 onCreate
方法中有个ButterKnife.bind(this)
,这是做什么的呢,不用猜肯定是做 findViewById
的地方。但它是怎么做的,我们继续往下看,先点进去:
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {View sourceView = target.getWindow().getDecorView();return bind(target, sourceView);
}@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {Class<?> targetClass = target.getClass();Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);if (constructor == null) {return Unbinder.EMPTY;}try {return constructor.newInstance(target, source);} catch (Exception e) {//do Exception...}
}
代码上做作了省略,关键部门展示出,ButterKnife 通过我们传入的对象找到了一个 Class,然后通过反射创建了这个 Class 对应的对象。而找到啥样的 Class 是通过 findBindingConstructorForClass
这个方法来实现的,点进去:
@Nullable @CheckResult @UiThreadprivate static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {String clsName = cls.getName();if (clsName.startsWith("android.") || clsName.startsWith("java.")|| clsName.startsWith("androidx.")) {if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");return null;}try {Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);} catch (Exception e) {// do Exception...}return bindingCtor;}
}
通过代码,可以看到查找的类是传入的类后面再加上 _ViewBinding
这个类,例如我在 com.my.ExampleActivity
的 onCreate
方法中调用了 ButterKnife.bind(this)
,那么 ButterKnife 会去查找一个名为 com.my.ExampleActivity_ViewBinding
的类,然后通过反射去创建这个类的对象。而这个类,可以肯定地说,是 ButterKnife 生成的类,findViewById
也肯定是在这里调用的。
为了验证一下这个猜测,我们得去看一下生成的 _ViewBinding
这个类的内容,将项目编译后,我们可以在下面的路径找到由 ButterKnife 生成的类:
点开这个对象,代码如下:
public class ExampleActivity_ViewBinding implements Unbinder {private ExampleActivity target;@UiThreadpublic ExampleActivity_ViewBinding(ExampleActivity target) {this(target, target.getWindow().getDecorView());}@UiThreadpublic ExampleActivity_ViewBinding(ExampleActivity target, View source) {this.target = target;target.textView = Utils.findRequiredViewAsType(source, R.id.example_text, "field 'textView'", TextView.class);}
这里的 Utils.findRequiredViewAsType
方法,其实就是 findViewById
调用的地方。也就是这个类在构造的时候,就会调用 findViewById
,以实现各种控件的绑定。
实现自己的 Knife.bind
看完了 ButterKnife 的基本原理,下面我们就参照着它自己完成剩余的部分。上面我们已经自定义了一个 BindView
的注解,但我们还剩一个没有做,那就是 ButterKnife.bind(this)
这个操作。
新建一个类 MyKnife
,然后咱们就参照着 ButterKnife.bind
向里面添加如下代码:
package lic.swift.binder;import android.app.Activity;public class MyKnife {public static void bind(Activity activity) {String bindClassName = activity.getClass().getName() + "_ViewBinding";try {Class<?> bindClass = Class.forName(bindClassName);Constructor<?> constructor = aClass.getConstructor(activity.getClass());constructor.newInstance();} catch (Exception e) {e.printStackTrace();}}
}
此时你会发现,因为我在代码中用了 Activity 这个类,但这个类是属于 Android 而非 Java。而在前面我们写注解的时候,新建的是 java library module,因此我们必须另起一个 android library module,把上面这个类添加进去。
这里我们简单一点,直接根据传入的 Activity 名字,后面加上 _ViewBinding
,查找到这个类,然后通过反射创建这个对象,像 ButterKnife 一样把 findViewById
的操作放到这个类的构造里。那么接下来,我们就得写 APT 去生成这个类了,这也是整个轮子的核心。
实现自己的 APT 生成 ViewBinding 代码
再新建一个 java library module,咱们在里面添加一个类继承自 Processor
:
package lic.swift.apt;import com.google.auto.service.AutoService;
import lic.swift.anno.BindView;@AutoService(value = {Processor.class})
public class FirstProcessor extends AbstractProcessor {private Filer filer;@Overridepublic SourceVersion getSupportedSourceVersion() {return SourceVersion.latestSupported();}@Overridepublic Set<String> getSupportedAnnotationTypes() {return Collections.singleton(BindView.class.getCanonicalName());}@Overridepublic synchronized void init(ProcessingEnvironment processingEnvironment) {super.init(processingEnvironment);processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " init");filer = processingEnvironment.getFiler();}@Overridepublic boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " process");//获取APP中所有用到了BindView注解的对象Set<? extends Element> elementsAnnotated = roundEnvironment.getElementsAnnotatedWith(BindView.class);Map<String, List<VariableElement>> map = new HashMap<>();for (Element element : elementsAnnotated) {if (element instanceof VariableElement variableElement) {String activityName = variableElement.getEnclosingElement().getSimpleName().toString();List<VariableElement> variableElementList = map.computeIfAbsent(activityName, k -> new ArrayList<>());variableElementList.add(variableElement);}}if (map.isEmpty())return false;//开始生成文件Iterator<String> iterator = map.keySet().iterator();while (iterator.hasNext()) {String activityName = iterator.next();List<VariableElement> variableElementList = map.get(activityName);//获取包名TypeElement enclosingElement = (TypeElement) variableElementList.get(0).getEnclosingElement();String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();try {JavaFileObject sourceFile = filer.createSourceFile(packageName + '.' + activityName + "_ViewBinding");Writer writer = sourceFile.openWriter();writer.write("package " + packageName + ";\n");writer.write("public class " + activityName + "_ViewBinding {\n");writer.write("\tpublic " + activityName + "_ViewBinding(" + packageName + "." + activityName + " activity){\n");for (VariableElement variableElement : variableElementList) {//得到变量的名字String variableName = variableElement.getSimpleName().toString();//得到传入的 IDint id = variableElement.getAnnotation(BindView.class).value();//得到变量的类型TypeMirror typeMirror = variableElement.asType();writer.write("\t\tactivity." + variableName + "= (" + typeMirror + ") activity.findViewById(" + id + ");\n");}writer.write("\t}\n}");writer.close();} catch (IOException e) {processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, this + " process \n" + e.getLocalizedMessage());}}return false;}
}
这里我简单说明一下代码,就是获取代码中有 BindView
注解的地方,然后每个类作为一个分组,将这个类中的所有有 BindView
注解的地方放到一起,放到 map 中。然后遍历这个 map,对每个类都生成一个对应的 _ViewBinding
文件,文件的内容就是在构造方法中调用 findViewById
。具体有不明的地方,可以看代码注释。
现在 BindView
注解有了,自己的 Knife.bind
方法也有了,APT 也有了。最后我们就验证一下我们的代码能不能使用。
验证自己的轮子
咱们就模拟正常的 APP 开发,把咱们刚写的程序放进去。首先,新建一个 Android app module,添加如下的依赖:
dependencies {annotationProcessor project(':apt') //注解处理器,用于生成 java 代码implementation project(':binder') //包含 BindView 注解和 MyKnife.bind 代码
}
在 Activity
代码中,我们像 ButterKnife
一样使用依赖的这两个项目:
public class ExampleActivity extends Activity {@BindView(R.id.example_text)protected TextView textView;@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_example);MyKnife.bind(this);}
}
编译一下,查看生成的 Java 代码如下:
到此,完全没有发现任何问题。项目也能正常运行起来,这里我就不贴出运行的截图了,简单而言,就是我们自己造了个 ButterKnife,也成功跑了起来。
写在最后
通过上面的内容,我们了解了什么是 APT,怎么写 APT,也了解了 SPI、AutoService
这些,并最后自己写了一个简单的 ButterKnife
并成功跑起来。不过这些有意义么?其实是没有的,先给大家看一个图:
在现在的 Android Studio 上使用 ButterKnife 时,也会出现这个提示,其意义是在以后的 Android 插件版本生成资源ID 时,将不会生成 final
类型的 ID 值,因此需要避免在注解上使用资源ID 作为参数。如果同一个资源两次生成的资源ID 不一样,而我们通过 ButterKnife 生成的代码却没有更新 ID 值,那么必然会找不到这个控件。
所以说,你看前面讲了那么多,但实际上,一点用都没有。还是老老实实写 findViewById
吧。
好了,完结,散花!