制作自己的 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 {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    @Override
    public 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 {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " init");
        super.init(processingEnv);
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " getSupportedSourceVersion");
        return SourceVersion.RELEASE_22;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " getSupportedAnnotationTypes");
        return Collections.singleton(FirstAnno.class.getName());
    }

    @Override
    public 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.jar
 
META-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 {
    @Override
    public void doPrint() {
        System.out.println("the First Service.");    
    }
}
package lic.first.service;

public class SecondService implements IService{
    @Override
    public 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.DoService

the 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 {
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " init");
        super.init(processingEnvironment);
    }

    @Override
    public 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 @UiThread
  private 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;

  @UiThread
  public ExampleActivity_ViewBinding(ExampleActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public 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;

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(BindView.class.getCanonicalName());
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, this + " init");
        filer = processingEnvironment.getFiler();
    }

    @Override
    public 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();
                    //得到传入的 ID
                    int 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;

    @Override
    protected 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 吧。
好了,完结,散花!
蕾姆-图片

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C语言基础学习之链表与共同体

数组: 数据结构---操作时候的特点&#xff1a; 优势&#xff1a;随机访问(存取)方便 不足&#xff1a;插入数据删除数据不方便 链式数据结构--链表 struct stu sl; // s1struct stu s2; // s2struct stu s3; //s3 s1-->s2-->s3 特点: 优势:增力和删除数据方便劣势…

2024年手机能做的赚钱软件有哪些?整理了八个手机能做的正规赚钱软件分享

在这个指尖滑动的时代&#xff0c;手机不仅仅是通讯工具&#xff0c;更是我们探索财富的钥匙。你是否曾幻想过&#xff0c;躺在沙发上&#xff0c;轻轻一滑&#xff0c;就能让钱包鼓起来&#xff1f; 今天&#xff0c;就让我们一起来探索那些隐藏在手机里的赚钱秘笈&#xff0c…

ubuntu系统 kubeadm方式搭建k8s集群

服务器环境与要求&#xff1a; 三台服务器 k8s-master01 192.168.26.130 操作系统&#xff1a; Ubuntu20.04 k8s-woker01 192.168.26.140 操作系统&#xff1a; Ubuntu20.04 k8s-woker02 192.168.26.150 操作系统&#xff1a; Ubuntu20.04 最低配置&#xff1a;2…

C++——IO流

C语言的输入和输出 C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键 盘)读取数据&#xff0c;并将值存放在变量中。printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。 注意宽度输出和精度输出控制。C语言借助了相应的缓冲…

信息学奥赛初赛天天练-19-挑战程序阅读-探索因数、所有因数平和、质数的奥秘

PDF文档公众号回复关键字:20240604 1 2023 CSP-J 阅读程序3 阅读程序&#xff08;程序输入不超过数组成字符串定义的范围&#xff1a;判断题正确填√&#xff0c;错误填&#xff1b;除特殊说明外&#xff0c;判断题1.5分&#xff0c;选择题3分&#xff0c;共计40分&#xff…

Windows上配置完整Linux开发环境(六):安装CentOS

1、CentOS vs. Ubuntu 基础和发行周期&#xff1a; CentOS 是基于Red Hat Enterprise Linux (RHEL)的社区版。它与RHEL共享源代码&#xff0c;因此提供与RHEL类似的稳定性。CentOS的版本更新不频繁&#xff0c;通常在RHEL发布新版本后才推出&#xff0c;保持长期支持周期&#…

《STM32Cube高效开发教程基础篇》第7章/案例7.1外部中断示例EXTI方式检测按键

文章目录 目的新建项目CudeMX设置CudeIDE编码 目的 采用外部中断方式检验4个按键的输入 新建项目 在案例6_1基础上另存为Demo7_1EXIT CudeMX设置 CudeIDE编码

MobileNetV4实战:使用MobileNetV4实现图像分类任务(一)

文章目录 摘要安装包安装timm 数据增强Cutout和MixupEMA项目结构计算mean和std生成数据集 摘要 论文链接&#xff1a;https://arxiv.org/pdf/2404.10518 MobileNetV4&#xff0c;作为新一代移动设备神经网络架构&#xff0c;凭借其创新的通用倒置瓶颈UIB块和Mobile MQA注意力块…

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

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

Springboot+vue二手房交易管理系统

Springbootvue二手房交易管理系统&#xff0c;项目用mybatis与数据库&#xff0c;数据库为mysql&#xff0c;16个表结构。有问题直接加我询问&#xff0c;我会一直在线哒。 功能如下&#xff1a; 房东管理 用户管理 房源信息管理&#xff08;可预约和查看评论&#xff09; 看房…

C语言 指针——函数指针的典型应用:通用排序

目录 编程实现升序和降序排序 如果不使用函数指针编程… 使用函数指针编写一个通用的排序函数 小结 编程实现升序和降序排序 如果不使用函数指针编程… 使用函数指针编写一个通用的排序函数 小结 正确理解指针的概念  指针是一种特殊的数据类型  指针类型的变量&am…

【动态规划】状态压缩dp

发现dp调试打最后二维dp表非常有用 1.吃奶酪类 先出状态&#xff0c;再走到哪 dp[1][0]0;for(int i3;i<maxn;i){//状态 for(int j1;j<n;j){//走过j if(i&(1<<j)){ for(int k0;k<n;k){//刚才在k dp[i][j]; } } } } P1433 吃奶酪 - 洛谷 | 计算机科学教育新生…

ARP欺骗的原理与详细步骤

ARP是什么&#xff1a; 我还记得在计算机网络课程当中&#xff0c;学过ARP协议&#xff0c;ARP是地址转换协议&#xff0c;是链路层的协议&#xff0c;是硬件与上层之间的接口&#xff0c;同时对上层提供服务。在局域网中主机与主机之间不能直接通过IP地址进行通信&#xff0c…

做ozon开单前需要多少钱,做ozon开单前需要多少钱

在电子商务的浪潮中&#xff0c;OZON平台以其独特的商业模式和市场定位&#xff0c;吸引了众多创业者和商家的目光。然而&#xff0c;在决定投身OZON平台之前&#xff0c;对开店成本的全面了解至关重要。本文将详细解析OZON开店前的各项费用&#xff0c;并提供一些高效投入的策…

go的反射和断言

在go中对于一个变量&#xff0c;主要包含两个信息变量类型&#xff08;type&#xff09;和变量值&#xff08;value&#xff09; 可以通过reflect包在运行的时候动态获取变量信息&#xff0c;并能够进行操作 对于Type可以通过reflect.TypeOf()获取到变量的类型信息 reflect.Ty…

网络服务DHCP的安装

DHCP的安装 检查并且安装dhcp有关软件包 rpm -qc dhcp #检查是否存在dhcp yum install -y dhcp #进行yum安装查看系统的配置文件 切换到对应目录查看相关文件配置&#xff0c;发现是空目录。 将官方提供的example复制到原配置文件中 cp /usr/share/doc/dhcp-4.2.5/dhcpd.…

This Python interpreter is in a conda environment

今天在查看python版本的时候出现警告 Warning: This Python interpreter is in a conda environment, but the environment has not been activated. Libraries may fail to load. To activate this environment please see https://conda.io/activation 这个警告意味着你…

Windows家庭版 WSL2非C盘详细安装配置与WSL代理设置+WSL基础环境CUDA安装

1 WSL2 配置 1.1 WSL 开启 注意&#xff1a;需要在windows功能中开启“Hyper-V”和“适用于Linux的Windows子系统”功能 但是&#xff01;windows家庭版&#xff08;windows home&#xff09;是默认没有Hyper-V功能的&#xff0c;自己手动安装&#xff1a; 创建一个记事本&a…

DeepDriving | 基于YOLOv8分割模型实现垃圾识别

本文来源公众号“DeepDriving”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;基于YOLOv8分割模型实现垃圾识别 0. 引言 YOLOv8是Ultralytics开源的一个非常火的AI算法&#xff0c;目前支持目标检测、实例分割、姿态估计等任务。…

Java List数据结构与常用方法

1.1 数据结构概述 Java的集合框架其实就是对数据结构的封装&#xff0c;在学习集合框架之前&#xff0c;有必要先了解下数据结构。 1.1.1 什么是数据结构 所谓数据结构&#xff0c;其实就是计算机存储、组织数据的方式。 数据结构是用来分析研究数据存储操作的&#xff0c;其实…