制作自己的 ButterKnife(使用 AutoService 和 APT 注解处理器在编译期生成 Java 代码)

ButterKnife

开发过 Android 的肯定都知道曾经有这么一个库,它能够让你不用再写 findViewById 这样的代码,这就是大名鼎鼎的 ButterKnife(https://github.com/JakeWharton/butterknife)。虽然现在这个库已经不再维护,最后的版本 10.2.3 也停留在了2020年8月份,但这个库当时公布时,其影响力还是不容小觑的。
ButterKnife 图标这个库是怎么个用法我们待会再看,先看 ButterKnife 的依赖方式。与引用其他的库不同的是,使用 ButterKnife 需要使用如下的依赖配置:

dependencies {implementation 'com.jakewharton:butterknife:10.2.3'annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'
}

一般来说,我们在项目中添加依赖时,一般会用到 implementationapi,那么这里的 annotationProcessor 是啥呢?为啥用 ButterKnife 需要使用这种方式的依赖呢?
这个问题~~问得好,那我们就先来回答这个问题。

Gradle 的五种依赖配置

目前 Gradle 版本支持的依赖配置有:implementationapicompileOnlyruntimeOnlyannotationProcessor,此外依赖配置还可以加一些配置项,例如AndroidTestImplementationdebugApi等等。而其中常用的依赖配置是 implementationapicompileOnly
关于这几个依赖配置的区别可以看下图:
Gradle的五种依赖配置项通过个图我们看到,使用 annotationProcessor 是注解处理器的依赖配置,如果在项目中使用注解处理器,那么就需要通过 annotationProcessor 来进行依赖。
那么问题就来了,注解处理器有什么用了,ButterKnife 除了要依赖一个库,为什么还必须要依赖一个注解处理器呢?
这又是一个好问题,不过在此处我先通俗的简单解释一下什么是注解处理器,也就是APT,即 Annotation Processing Tool。

简单解释 APT

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 之后的编译流程

Java编译过程通过图上可以看出,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 方法用于初始化被调用了一次,getSupportedSourceVersiongetSupportedAnnotationTypes 方法也都被调用,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。
lovelive壁纸休息完之后有人要说了,别人的注解处理器都是一个 jar 包, 你这就一个 class 文件,这跟 ButterKnife 不一样啊。你看看人家的 ButterKnife 的 jar 包好歹也有 58KB。
这个问题~~~问得好,那么现在,我们就开始将我们的注解处理器封装成 jar 包。

将注解处理器打包成 Jar

使用 jar 命令打包

先说明,打包成 jar 很简单,只需要使用 jar 命令并将 FirstAPT.classFirstAnno.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运行机制

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,就会发现服务已经被注册了:
生成Processor文件注意看这个文件: 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 里面最常用的注解,这里我们就拿这个注解作为例子,自己来实现它。注意这里的元注解 TargetRetention,设定了这个注解只能放到属性之上,且存在于源代码阶段。

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.ExampleActivityonCreate 方法中调用了 ButterKnife.bind(this),那么 ButterKnife 会去查找一个名为 com.my.ExampleActivity_ViewBinding 的类,然后通过反射去创建这个类的对象。而这个类,可以肯定地说,是 ButterKnife 生成的类,findViewById 也肯定是在这里调用的。
为了验证一下这个猜测,我们得去看一下生成的 _ViewBinding 这个类的内容,将项目编译后,我们可以在下面的路径找到由 ButterKnife 生成的类:
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 代码如下:
自己的APT生成的代码到此,完全没有发现任何问题。项目也能正常运行起来,这里我就不贴出运行的截图了,简单而言,就是我们自己造了个 ButterKnife,也成功跑了起来。

写在最后

通过上面的内容,我们了解了什么是 APT,怎么写 APT,也了解了 SPI、AutoService 这些,并最后自己写了一个简单的 ButterKnife 并成功跑起来。不过这些有意义么?其实是没有的,先给大家看一个图:
Android Studio 的提示在现在的 Android Studio 上使用 ButterKnife 时,也会出现这个提示,其意义是在以后的 Android 插件版本生成资源ID 时,将不会生成 final 类型的 ID 值,因此需要避免在注解上使用资源ID 作为参数。如果同一个资源两次生成的资源ID 不一样,而我们通过 ButterKnife 生成的代码却没有更新 ID 值,那么必然会找不到这个控件。
所以说,你看前面讲了那么多,但实际上,一点用都没有。还是老老实实写 findViewById 吧。
好了,完结,散花!
蕾姆-图片

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/22359.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LabVIEW实现汽车逆变器功能测试系统

​介绍了如何利用LabVIEW开发汽车逆变器&#xff08;包括功率板和控制板&#xff09;的自动测试设备&#xff08;ATE&#xff09;&#xff0c;实现对额定800V电压、300A电流的逆变器进行功能测试。系统通过CAN2.0通讯协议&#xff0c;实现电机控制、温度传感器监测、电压校验和…

Java程序策——Java连接数据库保姆级教程(超详细步骤)

【Java程序策】——连接数据库 目录 ​编辑 一&#xff1a;在数据库中建立一个表&#xff08;student表&#xff09; 1.1&#xff1a;进入mysql 1.2&#xff1a;建立一个“数据库成员” 1.3&#xff1a;建立一个表&#xff08;student表&#xff09; 1.4&#xff1a;给表…

关于 spring boot 的目录详解和配置文件

目录 配置文件 spring boot 的配置文件有两种格式&#xff0c;分别是 properties 和 yml&#xff08;yaml&#xff09;。这两种格式的配置文件是可以同时存在的&#xff0c;此时会以 properties 的文件为主&#xff0c;但一般都是使用同一种格式的。 格式 properties 语法格…

36. 【Java教程】输入输出流

本小节将会介绍基本输入输出的 Java 标准类&#xff0c;通过本小节的学习&#xff0c;你将了解到什么是输入和输入&#xff0c;什么是流&#xff1b;输入输出流的应用场景&#xff0c;File类的使用&#xff0c;什么是文件&#xff0c;Java 提供的输入输出流相关 API 等内容。 1…

eNSP学习——OSPF的DR与BDR

目录 相关命令 原理概述 实验内容 实验目的 实验拓扑 实验编址 实验步骤 1、基本配置 2、搭建基本的OSPF网络 3、查看默认情况下的DR/BDR状态 4、根据现网需求影响DR/BDR选举 需要eNSP各种配置命令的点击链接自取&#xff1a;华为&#xff45;NSP各种设备配置命令大…

小白级教程—安装Ubuntu 20.04 LTS服务器

下载 本教程将使用20.04版进行教学 由于官方速度可能有点慢&#xff0c;可以下方的使用清华镜像下载 https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/ 点击20.24版本 选择 ubuntu-20.04.6-live-server-amd64.iso 新建虚拟机 下载好后 我们使用 VMware 打开它 这里选…

数组的详细介绍

数组是一组相同类型元素的集合&#xff0c;也就是说&#xff1a;数组至少包含两个及以上的元素&#xff0c;且元素类型相同。 数组包括一维数组和多维数组&#xff0c;其中二维数组最常见。下面我们一一介绍。 一维数组&#xff1a; 格式&#xff1a;type name [常量值]&…

2024开放式耳机怎么买才好?这里可以教你六招!

有不少人都在说“开放式蓝牙耳机无音质”&#xff0c;大多数的购买者往往既贪恋蓝牙耳机的便携性&#xff0c;又想要有线耳机的Hifi快感&#xff0c;对于我们来说最重要的就是确定预算和需求&#xff0c;这样才能定位到最适合自己的开放式蓝牙耳机。这么多年零零总总听下来的蓝…

【python】成功解决“ModuleNotFoundError: No module named ‘IPython’”错误的全面指南

成功解决“ModuleNotFoundError: No module named IPython’”错误的全面指南 一、引言 在Python编程中&#xff0c;ModuleNotFoundError是一种常见的错误类型&#xff0c;它通常表明Python解释器无法找到你试图导入的模块。特别是当你遇到“ModuleNotFoundError: No module…

求助!什么软件可以人声分离?手机上可以进行人声分离操作吗?

在数字时代&#xff0c;音频处理变得越来越重要&#xff0c;而人声分离技术则是其中的一项关键技术。很多人可能都有过这样的疑问&#xff1a;什么软件可以实现人声分离&#xff1f;手机上能否进行人声分离操作&#xff1f;今天&#xff0c;我们就来为大家解答这些问题&#xf…

MySql学习(一)——MySQL概述之MySQL的启动

文章目录 一、MySQl概述1.1 启动MySQL1.2 客户端连接1.3 关系型数据库1.4 总结 一、MySQl概述 数据库&#xff1a;存储数据的仓库&#xff0c;数据是有组织的进行存储&#xff0c;简称为&#xff08;DB&#xff09;数据库管理系统&#xff1a;操纵和管理数据库的大型软件&…

微型计算机公司 Raspberry Pi 也开始涉足人工智能领域,计划出售AI芯片

Raspberry Pi 与 Hailo 合作&#xff0c;为其微型计算机提供可选的 AI 插件。 随着AI热潮的持续&#xff0c;就连微型计算机公司Raspberry Pi也计划出售AI芯片。它与 Raspberry Pi 的相机软件集成&#xff0c;可以在微型计算机上本地运行聊天机器人等基于人工智能的应用程序。…

Maven实战: 创建自定义archetype

在手动创建SpringBoot应用中&#xff0c;我们讲过手动创建SpringBoot工程是比较麻烦的&#xff0c;尤其是公司内部不有自定义扩展和集成的情况下。利用Maven的archetype:generate能基于项目模板生成功能&#xff0c;自定义模板能让整个创建过程自动化&#xff0c;这样既能大大降…

【UML用户指南】-07-对基本结构建模-公共机制

目录 1、术语和概念 1.1、注解&#xff08;note&#xff09; 1.2、修饰 1.3、衍型 1.4、标记值 1.5、约束 1.6、标准元素 1.7、外廓&#xff08;profile&#xff09; 2、对新特性建模 3、对新语义建模 注解 &#xff08;note&#xff09;是附加在元素或元素集上用来表…

ICLR24大模型提示(2/11) | BatchPrompt:多样本批量提示事半功倍

【摘要】由于大型语言模型 (LLM) 的 token 限制不断增加&#xff0c;使得长上下文成为输入&#xff0c;使用单个数据样本进行提示可能不再是一种有效的方式。提高效率的一个直接策略是在 token 限制内对数据进行批处理&#xff08;例如&#xff0c;gpt-3.5-turbo 为 8k&#xf…

Python08 循环

循环1. while2. for循环控制1.back 退出当前循环2.continue 结束本次继续下次循环 i 0 while i < 10:print(f我正在学习python {i})i1 #循环增量 print(while 结束)while True:print(f我正在学习python {i})if i > 20:break #退出循环i1 #循环增量 print(while2 结束)# …

【Python机器学习】无监督学习——不同类型的预处理

之前学习过&#xff0c;一些算法&#xff08;比如神经网络和SVM&#xff09;对数据缩放非常敏感。因此&#xff0c;通常的做法是对特征进行调节&#xff0c;使数据更适合于这些算法。通常来说&#xff0c;这是对数据的一种简单的按照特征的缩放和移动。举例&#xff1a; impor…

【python】成功解决“ModuleNotFoundError: No module named ‘graphviz’”错误的全面指南

成功解决“ModuleNotFoundError: No module named ‘graphviz’”错误的全面指南 一、引言 在Python编程中&#xff0c;当尝试导入一个不存在的模块时&#xff0c;你会遇到ModuleNotFoundError错误。当你看到“ModuleNotFoundError: No module named ‘graphviz’”这样的错…

vue无需引入第三方, 将web页面内容直接下载为docx

vue无需引入第三方&#xff0c; 将web页面内容直接下载为docx 将web页面内容重绘 html &#xff0c;通过 a 标签直接下载 通过写行内样式&#xff0c;控制docx中的文字图效果 let echHtmlWithIf ;if (this.chartImg.length) {if (this.exceed10Min) {echHtmlWithIf <div…

Linux线程安全:线程互斥

一、线程互斥的概念 1.1临界资源与互斥的关系 临界资源&#xff1a;多线程执行流共享的资源就叫做临界资源。 临界区&#xff1a;每个线程内部&#xff0c;访问临界资源的代码&#xff0c;就叫做临界区。 互斥&#xff1a;任何时刻&#xff0c;互斥保证有且只有一个执行流进入…