butterknife 源码分析

本文详细解释了ButterKnife注解的执行过程,包括编译时扫描注解、生成Java代码,以及在运行时如何通过`bind(this)`方法绑定视图。重点介绍了BindView注解的处理方法和处理器的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

target.subtitle = null;

target.hello = null;

target.listOfThings = null;

target.footer = null;

target.headerViews = null;

view2130968578.setOnClickListener(null);

view2130968578.setOnLongClickListener(null);

view2130968578 = null;

((AdapterView<?>) view2130968579).setOnItemClickListener(null);

view2130968579 = null;

this.target = null;

}

}


ButterKnife 的执行流程


总的来说,大概可以分为以下几步:

  • 在编译的时候扫描注解,并做相应的处理,生成 java 代码,生成 Java 代码是调用 javapoet 库生成的。

  • 当我们调用 ButterKnife.bind(this); 方法的时候,他会根据类的全限定类型,找到相应的代码,并执行。完成 findViewById 和 setOnClick ,setOnLongClick 等操作。

第一步:在编译的时候扫描注解,并做相应的处理,生成 java 代码。这一步,可以拆分为几个小步骤:

  • 定义我们的注解,声明我们的注解是否保存到 java doc 中,可以作用于哪些区域(Filed ,Class等),以及是源码时注解,编译时注解还是运行时注解等)

  • 继承 AbstractProcessor,表示支持哪些类型的注解,支持哪些版本,

  • 重写 process 方法,处理相关的注解,存进 Map 集合中

  • 根据扫描到的注解信息(即 Map 集合),调用 javapoet 库生成 Java 代码。


butterknife-annotations 讲解


外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们知道 ButterKnife 自定义很多的注解,有 BindArray,BindBitmap,BindColor,BindView 等,这里我们以 BindView 为例子讲解就 OK 了,其他的也是基本类似的,这里就不再讲解了。

//编译时注解

@Retention(CLASS)

//成员变量, (includes enum constants)

@Target(FIELD)

public @interface BindView {

/** View ID to which the field will be bound. */

@IdRes int value();

}

Processor 解析器说明


我们先来看一些基本方法:在 init 方法里面得到一些辅助工具类,这样有一个好处,确保工具类是单例的,因为 init 方法只会在初始化的时候调用。如果对注解还不了解的话,建议先阅读这一篇博客,Android 编译时注解 —— 语法详解

public synchronized void init(ProcessingEnvironment env) {

super.init(env);


//辅助工具类

elementUtils = env.getElementUtils();

typeUtils = env.getTypeUtils();

filer = env.getFiler();


}

接着重写 getSupportedAnnotationTypes 方法,返回我们支持的注解类型。

@Override

public Set getSupportedAnnotationTypes() {

Set types = new LinkedHashSet<>();

for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {

types.add(annotation.getCanonicalName());

}

//返回支持注解的类型

return types;

}

private Set<Class<? extends Annotation>> getSupportedAnnotations() {

Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

annotations.add(BindArray.class);

annotations.add(BindBitmap.class);

annotations.add(BindBool.class);

annotations.add(BindColor.class);

annotations.add(BindDimen.class);

annotations.add(BindDrawable.class);

annotations.add(BindFloat.class);

annotations.add(BindInt.class);

annotations.add(BindString.class);

annotations.add(BindView.class);

annotations.add(BindViews.class);

annotations.addAll(LISTENERS);

return annotations;

}

接下来来看我们的重点, process 方法。所做的工作大概就是拿到我们所有的注解信息,存进 map 集合,遍历 map 集合,做相应的 处理,生成 java 代码。

@Override

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

// 拿到所有的注解信息,TypeElement 作为 key,BindingSet 作为 value

Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

// 遍历 map 里面的所有信息,并生成 java 代码

for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {

TypeElement typeElement = entry.getKey();

BindingSet binding = entry.getValue();

JavaFile javaFile = binding.brewJava(sdk);

try {

javaFile.writeTo(filer);

} catch (IOException e) {

error(typeElement, “Unable to write binding for type %s: %s”, typeElement, e

.getMessage());

}

}

return false;

}

这里我们进入 findAndParseTargets 方法,看里面到底是怎样将注解信息存进 map 集合的?

findAndParseTargets 方法里面 针对每一个自定义注解(BindArray,BindBitmap,BindColor,BindView) 等都做了处理,这里我们重点关注 @BindView 的处理即可。其他注解的处理思想也是一样的。

我们先来看一下 findAndParseTargets 方法的前半部分,遍历 env.getElementsAnnotatedWith(BindView.class) 集合,并调用 parseBindView 方法去转化。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {

Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();

Set erasedTargetNames = new LinkedHashSet<>();

scanForRClasses(env);

// Process each @BindView element.

for (Element element : env.getElementsAnnotatedWith(BindView.class)) {

// we don’t SuperficialValidation.validateElement(element)

// so that an unresolved View type can be generated by later processing rounds

try {

parseBindView(element, builderMap, erasedTargetNames);

} catch (Exception e) {

logParsingError(element, BindView.class, e);

}

}


// 后半部分,待会再讲

}

可以看到牵绊部分的主要逻辑在 parseBindView 方法里面,主要做了以下几步操作:

  • 判断被注解 @BindView 修饰的成员变量是不是合法的,private 或者 static 修饰的,则出错。

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,

Set erasedTargetNames) {

TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

// 判断是否被注解在属性上,如果该属性是被 private 或者 static 修饰的,则出错

// 判断是否被注解在错误的包中,若包名以“android”或者“java”开头,则出错

boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, “fields”, element)

|| isBindingInWrongPackage(BindView.class, element);

// Verify that the target type extends from View.

TypeMirror elementType = element.asType();

if (elementType.getKind() == TypeKind.TYPEVAR) {

TypeVariable typeVariable = (TypeVariable) elementType;

elementType = typeVariable.getUpperBound();

}

Name qualifiedName = enclosingElement.getQualifiedName();

Name simpleName = element.getSimpleName();

// 判断元素是不是View及其子类或者Interface

if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {

if (elementType.getKind() == TypeKind.ERROR) {

note(element, "@%s field with unresolved type (%s) "

  • “must elsewhere be generated as a View or interface. (%s.%s)”,

BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);

} else {

error(element, “@%s fields must extend from View or be an interface. (%s.%s)”,

BindView.class.getSimpleName(), qualifiedName, simpleName);

hasError = true;

}

}

// 如果有错误,直接返回

if (hasError) {

return;

}

// Assemble information on the field.

int id = element.getAnnotation(BindView.class).value();

// 根据所在的类元素去查找 builder

BindingSet.Builder builder = builderMap.get(enclosingElement);

QualifiedId qualifiedId = elementToQualifiedId(element, id);

// 如果相应的 builder 已经存在

if (builder != null) {

// 验证 ID 是否已经被绑定

String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));

// 被绑定了,出错,返回

if (existingBindingName != null) {

error(element, “Attempt to use @%s for an already bound ID %d on ‘%s’. (%s.%s)”,

BindView.class.getSimpleName(), id, existingBindingName,

enclosingElement.getQualifiedName(), element.getSimpleName());

return;

}

} else {

// 如果没有相应的 builder,就需要重新生成,并别存放到 builderMap 中

builder = getOrCreateBindingBuilder(builderMap, enclosingElement);

}

String name = simpleName.toString();

TypeName type = TypeName.get(elementType);

boolean required = isFieldRequired(element);

builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

// Add the type-erased version to the valid binding targets set.

erasedTargetNames.add(enclosingElement);

}

parseBindView 方法分析完毕之后,我们在回过头来看一下 findAndParseTargets 方法的后半部分,主要做的工作是对 bindingMap 进行重排序。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {

// 省略前半部分

// Associate superclass binders with their subclass binders. This is a queue-based tree walk

// which starts at the roots (superclasses) and walks to the leafs (subclasses).

Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =

new ArrayDeque<>(builderMap.entrySet());

Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();

while (!entries.isEmpty()) {

Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

TypeElement type = entry.getKey();

BindingSet.Builder builder = entry.getValue();

//获取 type 的父类的 TypeElement

TypeElement parentType = findParentType(type, erasedTargetNames);

// 为空,存进 map

if (parentType == null) {

bindingMap.put(type, builder.build());

} else {

// 获取 parentType 的 BindingSet

BindingSet parentBinding = bindingMap.get(parentType);

if (parentBinding != null) {

builder.setParent(parentBinding);

bindingMap.put(type, builder.build());

} else {

// Has a superclass binding but we haven’t built it yet. Re-enqueue for later.

// 为空,加到队列的尾部,等待下一次处理

entries.addLast(entry);

}

}

}

return bindingMap;

}

到这里为止,我们已经分析完 ButterKnifeProcessor 是怎样处理注解的相关知识,并存进 map 集合中的,下面我们回到 process 方法,看一下是怎样生成 java 模板代码的

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

// 拿到所有的注解信息,TypeElement 作为 key,BindingSet 作为 value

Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

// 遍历 map 里面的所有信息,并生成 java 代码

for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {

TypeElement typeElement = entry.getKey();

BindingSet binding = entry.getValue();

// 生成 javaFile 对象

JavaFile javaFile = binding.brewJava(sdk);

try {

// 生成 java 模板代码

javaFile.writeTo(filer);

} catch (IOException e) {

error(typeElement, “Unable to write binding for type %s: %s”, typeElement, e

.getMessage());

}

}

return false;

}

生成代码的核心代码只有这几行

// 生成 javaFile 对象

JavaFile javaFile = binding.brewJava(sdk);

try {

// 生成 java 模板代码

javaFile.writeTo(filer);

} catch (IOException e) {

error(typeElement, “Unable to write binding for type %s: %s”, typeElement, e

.getMessage());

}

跟踪进去,发现是调用 square 公司开源的库 javapoet 开生成代码的。关于 javaPoet 的使用可以参考官网地址

JavaFile brewJava(int sdk) {

return JavaFile.builder(bindingClassName.packageName(), createType(sdk))

.addFileComment(“Generated code from Butter Knife. Do not modify!”)

.build();

}

private TypeSpec createType(int sdk) {

TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())

.addModifiers(PUBLIC);

if (isFinal) {

result.addModifiers(FINAL);

}

if (parentBinding != null) {

result.superclass(parentBinding.bindingClassName);

} else {

result.addSuperinterface(UNBINDER);

}

if (hasTargetField()) {

result.addField(targetTypeName, “target”, PRIVATE);

}

// 如果是 View 或者是 View 的子类的话,添加构造方法

if (isView) {

result.addMethod(createBindingConstructorForView());

} else if (isActivity) { // 如果是 Activity 或者是 Activity 的子类的话,添加构造方法

result.addMethod(createBindingConstructorForActivity());

} else if (isDialog) { // 如果是 Dialog 或者是 Dialog 的子类的话,添加构造方法

result.addMethod(createBindingConstructorForDialog());

}

// 如果构造方法不需要 View 参数,添加 需要 View 参数的构造方法

if (!constructorNeedsView()) {

// Add a delegating constructor with a target type + view signature for reflective use.

result.addMethod(createBindingViewDelegateConstructor());

}

result.addMethod(createBindingConstructor(sdk));

if (hasViewBindings() || parentBinding == null) {

//生成unBind方法

result.addMethod(createBindingUnbindMethod(result));

}

return result.build();

}

接着我们一起来看一下 createBindingConstructor(sdk) 方法,大概做的事情就是

  • 判断是否有设置监听,如果有监听,将 View 设置为 final

  • 遍历 viewBindings ,调用 addViewBinding 生成 findViewById 形式的代码。

private MethodSpec createBindingConstructor(int sdk) {

MethodSpec.Builder constructor = MethodSpec.constructorBuilder()

.addAnnotation(UI_THREAD)

.addModifiers(PUBLIC);

// 如果有方法绑定,比如 @onClick,那么增加一个 targetTypeName 类型 的方法参数 target,并且是 final 类型的

if (hasMethodBindings()) {

constructor.addParameter(targetTypeName, “target”, FINAL);

} else { // 如果没有 ,不是 final 类型的

constructor.addParameter(targetTypeName, “target”);

}

//如果有注解的 View,那么添加 VIEW 类型 source 参数

if (constructorNeedsView()) {

constructor.addParameter(VIEW, “source”);

} else {

// 添加 Context 类型的 context 参数

constructor.addParameter(CONTEXT, “context”);

}

if (hasUnqualifiedResourceBindings()) {

// Aapt can change IDs out from underneath us, just suppress since all will work at

// runtime.

constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)

.addMember(“value”, “$S”, “ResourceType”)

.build());

}

// 如果 @OnTouch 绑定 View,添加 @SuppressLint(“ClickableViewAccessibility”)

if (hasOnTouchMethodBindings()) {

constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)

.addMember(“value”, “$S”, “ClickableViewAccessibility”)

.build());

}

// 如果 parentBinding 不为空,调用父类 的构造方法

if (parentBinding != null) {

if (parentBinding.constructorNeedsView()) {

constructor.addStatement(“super(target, source)”);

} else if (constructorNeedsView()) {

constructor.addStatement(“super(target, source.getContext())”);

} else {

constructor.addStatement(“super(target, context)”);

}

constructor.addCode(“\n”);

}

// 添加成员变量

if (hasTargetField()) {

constructor.addStatement(“this.target = target”);

constructor.addCode(“\n”);

}

if (hasViewBindings()) {

if (hasViewLocal()) {

// Local variable in which all views will be temporarily stored.

constructor.addStatement(“$T view”, VIEW);

}

// 遍历 viewBindings,生成 source.findViewById($L) 代码

for (ViewBinding binding : viewBindings) {

addViewBinding(constructor, binding);

}

for (FieldCollectionViewBinding binding : collectionBindings) {

constructor.addStatement(“$L”, binding.render());

}

if (!resourceBindings.isEmpty()) {

constructor.addCode(“\n”);

}

}

if (!resourceBindings.isEmpty()) {

if (constructorNeedsView()) {

constructor.addStatement(“$T context = source.getContext()”, CONTEXT);

}

if (hasResourceBindingsNeedingResource(sdk)) {

constructor.addStatement(“$T res = context.getResources()”, RESOURCES);

}

for (ResourceBinding binding : resourceBindings) {

constructor.addStatement(“$L”, binding.render(sdk));

}

}

return constructor.build();

}

下面我们一起来看一下 addViewBinding 方法是怎样生成代码的。

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {

if (binding.isSingleFieldBinding()) {

// Optimize the common case where there’s a single binding directly to a field.

FieldViewBinding fieldBinding = binding.getFieldBinding();

// 注意这里直接使用了 target. 的形式,所以属性肯定是不能 private 的

CodeBlock.Builder builder = CodeBlock.builder()

.add("target.$L = ", fieldBinding.getName());

boolean requiresCast = requiresCast(fieldBinding.getType());

if (!requiresCast && !fieldBinding.isRequired()) {

builder.add(“source.findViewById($L)”, binding.getId().code);

} else {

builder.add(“$T.find”, UTILS);

builder.add(fieldBinding.isRequired() ? “RequiredView” : “OptionalView”);

if (requiresCast) {

builder.add(“AsType”);

}

builder.add(“(source, $L”, binding.getId().code);

if (fieldBinding.isRequired() || requiresCast) {

builder.add(“, $S”, asHumanDescription(singletonList(fieldBinding)));

}

if (requiresCast) {

builder.add(“, $T.class”, fieldBinding.getRawType());

}

builder.add(“)”);

}

result.addStatement(“$L”, builder.build());

return;

}

ButterKnife 是怎样实现代码注入的


使用过 ButterKnife 得人基本都知道,我们是通过 bind 方法来实现注入的,即自动帮我们 findViewById ,解放我们的双手,提高工作效率。下面我们一起来看一下 bind 方法是怎样实现注入的。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

我搜集整理过这几年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

img

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

且会持续更新!**

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

我搜集整理过这几年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

img

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值