备注

原发表于2016.05.21,资料已过时,仅作备份,谨慎参考

前言

[Android] ButterKnife 浅析 中,我们了解了 ButterKnife 的用法,比较简单。

本次文章我们来学习一下 ButterKnife 的 ButterKnifeProcessor 注解处理器,注解处理器能够解析代码中的注解信息,生成相应的 Java 类,这也是 ButterKnife 的关键实现原理。

建议在阅读前先了解下 Java 中『注解』的概念。

准备内容

APT

APT(Annotation processing tool)是在编译时,扫描和处理注解的一个构建工具,可以在编译源代码时额外生成 Java 源代码。

AbstractProcessor

AbstractProcessor 是扫描和处理注解的关键类,ButterKnife 自定义的 Processor 就需要继承自该类。

代码示例如下:

public class MyProcessor extends AbstractProcessor {
// 在 Processor 创建时调用并执行的初始化操作
@Override
public synchronized void init(ProcessingEnvironment env){ } // 关键方法,进行扫描和处理注解,并生成新的源代码
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { } // 指定需要注册的注解
@Override
public Set<String> getSupportedAnnotationTypes() { } // 指定支持的 Java 版本
@Override
public SourceVersion getSupportedSourceVersion() { } }

添加依赖

还记得我们使用 ButterKnife 前所做的添加吗?

dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
} dependencies {
compile 'com.jakewharton:butterknife:8.0.1'
apt 'com.jakewharton:butterknife-compiler:8.0.1'
}

这里 com.jakewharton:butterknife-compiler 就是自定义的注解处理器,我们在 Gradle 中注册使用它。

然而我在项目结构中找了很久也没有找到这个库的文件,有可能是在编译时才去访问的,如果需要可以在 GitHub 中找到:

butterknife-compiler

ButterKnifeProcessor 工作流程

上面注册完自定义的注解处理器后,我们就知道,在编译源代码时,APT 会调用 Processor 来查找解析注解。

主要逻辑

这里主要看 process 处理方法的内容。

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {

    // 查找并解析注解
Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env); // 去除注解的键值
for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingClass bindingClass = entry.getValue(); // 生成源代码
try {
bindingClass.brewJava().writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
e.getMessage());
}
}
return true;
}

可以看到 process 方法的逻辑还是比较容易理解的,就是获取到注解的键值并生成源代码。

查找和解析注解

首先看 findAndParseTargets(RoundEnvironment env) 方法,通过参数 RoundEnvironment env 可以找到我们想要的某一个被注解的元素。

该方法会查找所有 ButterKnife 的注解来进行解析,我们选择最简单的 @BindInt 来看一下:

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); // Process each @BindInt element.
for (Element element : env.getElementsAnnotatedWith(BindInt.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
parseResourceInt(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindInt.class, e);
}
}
...

这里调用了 parseResourceInt 方法传入了被注解的元素进行解析:

  private void parseResourceInt(Element element, Map<TypeElement, BindingClass> targetClassMap,
Set<TypeElement> erasedTargetNames) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Verify that the target type is int.
if (element.asType().getKind() != TypeKind.INT) {
error(element, "@%s field type must be 'int'. (%s.%s)", BindInt.class.getSimpleName(),
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
} // Verify common generated code restrictions.
hasError |= isInaccessibleViaGeneratedCode(BindInt.class, "fields", element);
hasError |= isBindingInWrongPackage(BindInt.class, element); if (hasError) {
return;
} // Assemble information on the field.
String name = element.getSimpleName().toString();
int id = element.getAnnotation(BindInt.class).value(); BindingClass bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
FieldResourceBinding binding = new FieldResourceBinding(id, name, "getInteger", false);
bindingClass.addResource(binding); erasedTargetNames.add(enclosingElement);
}

方法分为前后两部分,先进行了一些检验,检验通过则调用 getOrCreateTargetClass 获取或生成一个类放入数组中。

isInaccessibleViaGeneratedCode 方法检验了:

  1. 方法修饰符不能为 private 和 static
  2. 包类型不能为非 Class
  3. 类的修饰符不能为 private

isBindingInWrongPackage 则检验了包名,不能以 android 或 java 开头。

生成类文件

解析完每个被注解的元素之后会得到一个 Map<TypeElement, BindingClass> targetClassMap,接着就会调用 Map 中每个 bindingClass 生成 Java 源代码:

bindingClass.brewJava().writeTo(filer);

brewJava() 的内容如下所示,添加了方法,父类,接口等信息,最后生成了一个 JavaFile,简单了解即可:

JavaFile brewJava() {
TypeSpec.Builder result = TypeSpec.classBuilder(generatedClassName)
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(Modifier.FINAL);
} else {
result.addTypeVariable(TypeVariableName.get("T", targetTypeName));
} TypeName targetType = isFinal ? targetTypeName : TypeVariableName.get("T");
if (hasParentBinding()) {
result.superclass(ParameterizedTypeName.get(parentBinding.generatedClassName, targetType));
} else {
result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, targetType));
} result.addMethod(createBindMethod(targetType)); if (isGeneratingUnbinder()) {
result.addType(createUnbinderClass(targetType));
} else if (!isFinal) {
result.addMethod(createBindToTargetMethod());
} return JavaFile.builder(generatedClassName.packageName(), result.build())
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}

于是我们编译过后就能在项目中找到类似 MainActivity$$ViewBinder 这样的文件。

到这里 ButterKnifeProcessor 的工作就结束了,ButterKnife 就能够调用生成的类来进行绑定工作,我们将在下一篇文章中对余下的流程进行学习。

参考资料

AndroidSutdio 编译时自动生成源代码

Android 浅析 ButterKnife (二) 源码解析

[旧][Android] ButterKnifeProcessor 工作流程分析的更多相关文章

  1. [旧][Android] LayoutInflater 工作流程

    备注 原发表于2016.06.20,资料已过时,仅作备份,谨慎参考 前言 感觉很长时间没写文章了,这个星期因为回家和处理项目问题,还是花了很多时间的.虽然知道很多东西如果只是看一下用一次,很快就会遗忘 ...

  2. Kafka工作流程分析

    Kafka工作流程分析 生产过程分析 写入方式 producer采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(patition)中,属于顺序写磁盘(顺序写磁盘 ...

  3. Kafka之工作流程分析

    Kafka之工作流程分析 kafka核心组成 一.Kafka生产过程分析 1.1 写入方式 producer采用推(push)模式将消息发布到broker,每条消息都被追加(append)到分区(pa ...

  4. 【转】Hostapd工作流程分析

    [转]Hostapd工作流程分析 转自:http://blog.chinaunix.net/uid-30081165-id-5290531.html Hostapd是一个运行在用户态的守护进程,可以通 ...

  5. 第2章 rsync算法原理和工作流程分析

    本文通过示例详细分析rsync算法原理和rsync的工作流程,是对rsync官方技术报告和官方推荐文章的解释. 以下是本文的姊妹篇: 1.rsync(一):基本命令和用法 2.rsync(二):ino ...

  6. [国嵌笔记][030][U-Boot工作流程分析]

    uboot工作流程分析 程序入口 1.打开顶层目录的Makefile,找到目标smdk2440_config的命令中的第三项(smdk2440) 2.进入目录board/samsung/smdk244 ...

  7. rsync算法原理和工作流程分析

    本文通过示例详细分析rsync算法原理和rsync的工作流程,是对rsync官方技术报告和官方推荐文章的解释.本文不会介绍如何使用rsync命令(见rsync基本用法),而是详细解释它如何实现高效的增 ...

  8. nodejs的Express框架源码分析、工作流程分析

    nodejs的Express框架源码分析.工作流程分析 1.Express的编写流程 2.Express关键api的使用及其作用分析 app.use(middleware); connect pack ...

  9. Mysql工作流程分析

    Mysql工作流程图 工作流程分析 1. 所有的用户连接请求都先发往连接管理器 2. 连接管理器    (1)一直处于侦听状态    (2)用于侦听用户请求 3. 线程管理器    (1)因为每个用户 ...

随机推荐

  1. GIL全局解释器锁、死锁现象、python多线程的用处、进程池与线程池理论

    昨日内容回顾 僵尸进程与孤儿进程 # 僵尸进程: 所有的进程在运行结束之后并不会立刻销毁(父进程需要获取该进程的资源) # 孤儿进程: 子进程正常运行 但是产生该子进程的父进程意外死亡 # 守护进程: ...

  2. unity3d C# soket客户端接受失败

    using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using S ...

  3. xargs、管道、exec区别

    作者:ilexwg链接:https://www.zhihu.com/question/27452459/answer/170834758来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转 ...

  4. 2022年写的香橙派 OrangePi Zero 用python获取dht11温度和湿度

    感谢网上资料和个人的不放弃,终于方便的解决了香橙派 OrangePi Zero用python获取dht11温湿度的问题. 网上关于香橙派的资料比起树莓派真是少之又少,现在香橙派zero能干的活暂时也只 ...

  5. Vue.js的组件(slot/动态组件等)、单文件组件、递归组件使用

    一.组件 1> 组件命名方式有两种(注意:在DOM模板中只有kebab-case命名方法才生效): html中引用组件: <!-- 在DOM模板中,只有 kebab-case命名才生效 - ...

  6. 「 MySQL高级篇 」MySQL索引原理,设计原则

    大家好,我是melo,一名大二后台练习生,大年初三,我又来充当反内卷第一人了!!! 专栏引言 MySQL,一个熟悉又陌生的名词,早在学习Javaweb的时候,我们就用到了MySQL数据库,在那个阶段, ...

  7. Programiz 中文系列教程·翻译完成

    原文:Programiz 协议:CC BY-NC-SA 4.0 欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远. 在线阅读 ApacheCN 学习资源 目录 Programiz C ...

  8. Windows使用Nginx配置本地文件代理(查看本地资源或图片)

    一.前言 我们在实际开发中会经常遇到把图片上传到服务器上,比如七牛云.阿里云.腾讯云一些提供服务的商家.但是我们的图片隐私也会泄露,所以我们还是上传到本地,使用Nginx来代理到我们本地图片.这样就可 ...

  9. 不会用SpringBoot连接Redis,那就赶紧看这篇

    摘要:如何通过springboot来集成操作Redis. 本文分享自华为云社区<SpringBoot连接Redis操作教程>,作者: 灰小猿. 今天来和大家分享一个如何通过springbo ...

  10. springBoot2.*使用redis集群/单机方法

    在springboot1.x系列中,其中使用的是jedis,但是到了springboot2.x其中使用的是Lettuce. 此处springboot2.x,所以使用的是Lettuce.关于jedis跟 ...