分分钟带你读懂-ButterKnife-的源码(1)

**接下来来看我们的重点, 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”, “KaTeX parse error: Expected 'EOF', got '}' at position 32: …e") .build()); }̲ // 如果 @OnTouch…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(“KaTeX parse error: Expected 'EOF', got '}' at position 17: … view", VIEW); }̲ // 遍历 viewB…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(“KaTeX parse error: Expected 'EOF', got '}' at position 45: …()", CONTEXT); }̲ if (hasResourc…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(KaTeX parse error: Expected 'EOF', got '}' at position 29: …getId().code); }̲ else { builder…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(“, KaTeX parse error: Expected 'EOF', got '}' at position 39: …getRawType()); }̲ builder.add(")…L”, builder.build());
return;
}

###ButterKnife 是怎样实现代码注入的
使用过 ButterKnife 得人基本都知道,我们是通过 bind 方法来实现注入的,即自动帮我们 findViewById ,解放我们的双手,提高工作效率。下面我们一起来看一下 bind 方法是怎样实现注入的。

@NonNull
@UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}

可以看到 bind 方法很简单,逻辑基本都交给 createBinding 方法去完成。我们一起进入 createBinding 方法来看一下到底做了什么。

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
// 从 Class 中查找 constructor
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

if (constructor == null) {
return Unbinder.EMPTY;
}

//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
// 反射实例化构造方法
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException(“Unable to create binding instance.”, cause);
}
}

其实 createBinding 来说,主要做了这几件事情

  • 传入 class ,通过 findBindingConstructorForClass 方法来实例化 constructor
  • 利用反射来初始化 constructor 对象
  • 初始化 constructor 失败会抛出异常

下面我们一起来看一下 findBindingConstructorForClass 方法是怎样实现的。

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
// 读取缓存,如果不为空,直接返回
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); if (bindingCtor != null) { if (debug) Log.d(TAG, "HIT: Cached in binding map."); return bindingCtor; } // 如果是 android ,java 原生的文件,不处理 String clsName = cls.getName(); if (clsName.startsWith("android.") || clsName.startsWith("java.")) { if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search."); return null; } try { Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + “_ViewBinding”);
//noinspection unchecked
// 在原来所在的类查找
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View
.class);
if (debug) Log.d(TAG, “HIT: Loaded binding class and constructor.”);
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
// 在原来的类查找,查找不到,到父类去查找
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
// 存进 LinkedHashMap 缓存
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}

它的实现思想是这样的:

  • 读取缓存,若缓存命中,直接返回,这样有利于提高效率。从代码中可以看到,缓存是通过存进 map 集合实现的。
  • 是否是我们目标文件,是的话,进行处理,不是的话,直接返回,并打印相应的日志
  • 利用类加载器加载我们自己生成的 class 文件,并获取其构造方法,获取到,直接返回。获取不到,会抛出异常,在异常的处理中,我们再从当前 class 文件的父类去查找。并把结果存进 map 集合中,做缓存处理。

我们对 ButterKnife 的分析到此为止。
###题外话
这篇博客主要是分析了 ButterKnife 的主要原理实现,对 ButterKnife 里面的一些实现细节并未详细分析。不过对我们读懂代码已经足够了。下一个系列,主要讲解 CoordinatorLayout 的实现原理及怎样自定义 CoordinatorLayout 的 behavior 实现仿新浪微博发现页面的效果,敬请期待。

扫一扫,可以加下VX, 目前是一名程序员,不仅分享 Android开发相关知识,同时还分享技术人成长历程,包括个人总结,职场经验,面试经验等,希望能让你少走一点弯路。

关于我

更多信息可以点击关于我 , 非常希望和大家一起交流 , 共同进步

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

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

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

img

img

img

img

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

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

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

总结

找工作是个很辛苦的事情,而且一般周期都比较长,有时候既看个人技术,也看运气。第一次找工作,最后的结果虽然不尽如人意,不过收获远比offer大。接下来就是针对自己的不足,好好努力了。

最后为了节约大家的时间,我把我学习所用的资料和面试遇到的问题和答案都整理成了PDF文档

喜欢文章的话请关注、点赞、转发 谢谢!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

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

总结

找工作是个很辛苦的事情,而且一般周期都比较长,有时候既看个人技术,也看运气。第一次找工作,最后的结果虽然不尽如人意,不过收获远比offer大。接下来就是针对自己的不足,好好努力了。

最后为了节约大家的时间,我把我学习所用的资料和面试遇到的问题和答案都整理成了PDF文档

喜欢文章的话请关注、点赞、转发 谢谢!

[外链图片转存中…(img-590g5iYc-1713523208289)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 28
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值