知其然知其所以然
ButterKnife使用Java Annotation Processing技术,在Java代码编译成Java字节码的时候处理注解@BindView、@OnClick、@BindXXX(ButterKnife支持的注解:在Butterknife-annotations包下)生成对应的ViewBinding类,在这个类中进行资源和视图的绑定操作.
现在大致流程我们已经清楚了,那么源码中到底是如何实现的呢?
我们先看下源码工程的目录(ButterKnife版本:8.8.1):
Module对应的职责如下:
- butterknife: android library model 提供android使用的API
- butterknife-annotations: java-model,使用时的注解
- butterknife-compiler: java-model,编译时用到的注解的处理器
- butterknife-gradle-plugin: 自定义的gradle插件,辅助生成有关代码
- butterknife-integration-test: 项目的测试用例
- butterknife-lint:项目的lint检查
首先我们先看下butterknife的构成
看起来真是简洁啊~ 我们来依次看下吧~
DebouncingOnClickListener
这是一个抽象类实现了View.OnClickListener并做了相应的处理来消除同一帧中发布的多个点击,当点击一个按钮将禁用该框架的所有按钮。
public abstract class DebouncingOnClickListener implements View.OnClickListener {
static boolean enabled = true;
private static final Runnable ENABLE_AGAIN = new Runnable() {
@Override public void run() {
enabled = true;
}
};
@Override public final void onClick(View v) {
if (enabled) {
enabled = false;
v.post(ENABLE_AGAIN);
doClick(v);
}
}
public abstract void doClick(View v);
}
ImmutableList
这是一个被final修饰继承AbstractList方法的不可变轻量集合类,因为在实际使用时需要的方法过少,所有没有必要去使用ArrayList,它的实现也很简单。
final class ImmutableList<T> extends AbstractList<T> implements RandomAccess {
private final T[] views;
ImmutableList(T[] views) {
this.views = views;
}
@Override public T get(int index) {
return views[index];
}
@Override public int size() {
return views.length;
}
@Override public boolean contains(Object o) {
for (T view : views) {
if (view == o) {
return true;
}
}
return false;
}
}
Utils
这个类看名字就知道是个工具类,它的作用主要就是获取资源和类型强转,代码就省略了,参考价值不大...
ButterKnife相关方法解析
-
ButterKnife.bind()方法
- bind(Activity target):这个方法会先根据Activity取得decorView,然后再调用createBinding(target,decorView)方法。
- bind(View target): 这个方法比较特殊,它会将视图及其子视图用作视图根,调用createBinding(target,target)方法。
- bind(Dialog target):这个方法会先根据Dialog取得decorView,然后再调用createBinding(target,decorView)方法。
- bind(Object target, Activity source):同bind(Activity target)。
- bind(Object target, Dialog source):同bind(Dialog target)
- bind(Object target, View source):直接调用createBinding(target,source)方法。
参数中的target其实就是发生视图绑定的地方,也就是我们使用@BindXXX注解所在的类。所以在ViewHodler中调用的方法应该是最后一个
ButterKnife.createBinding()方法
在前面的bind方法中最后都调用了这个方法来结束,那这个方法中到底有什么呢?
/**
*
* @param target
* @param source target所在的根view或者自身(if target == view)
* @ret urn
*/
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());
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);
}
}
@Nullable @CheckResult @UiThread
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;
}
//类名检测
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_ViewBinding类文件
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);
}
//找到以后加入缓存
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
首先获取到target的class,然后根据class去缓存中寻找是否已经绑定过,绑定过就直接返回,否则取得类名并检查类名是否是android.或者java.开头的,如果是的话就直接返回null(ps:这里是禁止去接触framework层的代码),检查通过之后就会去寻找在编译以后生成的对应XX_ViewBinding类文件通过反射调用构造方法来进行绑定。如果没有找到对应的XX_ViewBinding类文件会在target的父类中继续寻找直到找到或者失败抛异常。最终如果绑定成功以后会加入缓存池中并返回。
接着我们先看下butterknife-annotations的构成
不得不说这些注解已经能够基本满足我们日常工作了。那我就挑几个大家不常用或者不熟悉的注解来说下怎么使用吧~
@OnCheckedChanged
这个注解的作用主要是在android.widget.CompoundButton上,但是当你去看源码时发现这只是个抽象类,那么它的实现类到底是谁?在官网上我们看到分别是:CheckBox,RadioButton,Switch,ToggleButton
这个注解对应的替代的方法就是setOnCheckedChangeListener,使用如下:
@OnCheckedChanged(R.id.example) void onChecked(boolean checked) {
Toast.makeText(this, checked ? "Checked!" : " Unchecked!",Toast.LENGTH_SHORT).show();
}
@OnTextChanged
这个注解是用来替代addTextChangedListener。以前我们在对EditText进行文本变化监听时不得不重写TextChangedListener的三个方法,但是很多时候我们只需要用到其中一个方法。这个时候使用这个注解就能满足我们的要求,注解内部使用枚举变量分别三个方法,我们在使用注解时传入对应的枚举变量对应的方法就会被回调,使用方法如下:
//因为在注解定义时默认返回TEXT_CHANGED 所以callback = TEXT_CHANGED时可以省略
@OnTextChanged(R.id.example) void onTextChanged(CharSequence text) {
Toast.makeText(this, "Text changed: " + text, Toast.LENGTH_SHORT).show();
}
@OnTextChanged(value = R.id.example, callback = BEFORE_TEXT_CHANGED)
void onBeforeTextChanged(CharSequence text) {
Toast.makeText(this, "Before text changed: " + text,Toast.LENGTH_SHORT).show();
}
@OnTextChanged(value = R.id.example, callback = AFTER_TEXT_CHANGED)
void onAfterTextChanged(CharSequence text) {
Toast.makeText(this, "Before text changed: " + text,Toast.LENGTH_SHORT).show();
}
这里就不对所有注解怎么使用进行赘述了,大家只要在使用时去翻一翻源码即可.
然后我们先看下butterknife-compiler的构成
因为处理注解使用的是Java Annotation Processing,所以入口肯定是ButterKnifeProcessor。
在看这个类的源码之前先说一点关于JavaPoet的知识
- MethodSpec 代表一个构造函数或方法声明。
- TypeSpec 代表一个类,接口,或者枚举声明。
- FieldSpec 代表一个成员变量,一个字段声明。
- JavaFile包含一个顶级类的Java文件。
举个小栗子🐒
这是一个HelloLianJia类
package com.lianjia.hello;
public final class HelloLianJia {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
使用JavaPoet来生成上面的HelloLianJia类
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, World!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloLianJia")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.lianjia.hello", helloWorld)
.build();
javaFile.writeTo(System.out);
上述代码中我们先创建了一个MethodSpec来声明main方法,它配置了修饰符,返回类型,参数和代码语句。 然后我们使用TypeSpec创建了HelloLianJia类,它配置了修饰符之后将main方法添加到HelloLianJia类中,最后通过JavaFile将其添加到HelloWorld.java文件中。
在了解了JavaPoet相关知识后我们来看ButterKnifeProcessor的process方法
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//1.遍历和解析注解 结果存在Map中
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
//遍历Map 通过JavaPoet生成对应的ViewBinding文件
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
//2.这里就是ViewBinding生成的过程
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
解析的过程就是先将所有注解遍历解析一遍,然后使用JavaPoet根据解析结果生成对应的XXX_ViewBinding文件.
1. findAndParseTargets
/**
* 遍历和解析注解
* @param env
* @return 返回遍历的结果
*/
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
//注意这里创建的Map的value值为BindingSet.Builder 而不是方法定义的返回类型BindingSet
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
//将相关的View与R.id.XX建立对应的关系
scanForRClasses(env);
// Process each @BindAnim element.
for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
if (!SuperficialValidation.validateElement(element)) continue;
try {
//1-2.我们在这里只说解析@BindAnim的过程,其他的注解类似
parseResourceAnimation(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindAnim.class, e);
}
}
.....................省略其他注解的解析过程...........................
// 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).
// 这里的主要目的就是为了将Map.Entry<TypeElement, BindingSet.Builder>转化成Map<TypeElement, BindingSet>
//第一步将Map.Entry<TypeElement, BindingSet.Builder>转换成Map.Entry<TypeElement, BindingSet.Builder>
//这么做的主要原因是Map.Entry<K,V>可以直接调用getKey()和getValue()
//那为什么要使用Deque呢? 看注释提醒需要将父类的绑定者和它们子类的绑定者关联起来,然后基于Deque从根到叶子的树遍历
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();
//这里就开始判断是否有父类型存在
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
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;
}
1-2. parseResourceAnimation
private void parseResourceAnimation(Element element,
Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify that the target type is Animation.检验类型是否是Animation类型
if (!ANIMATION_TYPE.equals(element.asType().toString())) {
error(element, "@%s field type must be 'Animation'. (%s.%s)", BindAnim.class.getSimpleName(),
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
// Verify common generated code restrictions.检验通用生成的代码限制。
hasError |= isInaccessibleViaGeneratedCode(BindAnim.class, "fields", element);
//检验类的包名不能是android.或者java.开头的
hasError |= isBindingInWrongPackage(BindAnim.class, element);
if (hasError) {
return;
}
// Assemble information on the field.
String name = element.getSimpleName().toString();
//获取id
int id = element.getAnnotation(BindAnim.class).value();
//将id和对应的packageName存储起来
QualifiedId qualifiedId = elementToQualifiedId(element, id);
//根据enclosingElement生成对应的BindingSet.Buidler
BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
//然后对id和name进行封装以后添加到builder中
builder.addResource(new FieldAnimationBinding(getId(qualifiedId), name));
erasedTargetNames.add(enclosingElement);
}
1-3. 关于BindingSet.Builder
BindingSet采用的是建造者设计模式来构建对应的实例,在内部类Buidler中会对注解的TypeName、bindingClassName、parentBinding和注解所在的Target是什么进行存储。
static final class Builder {
private final TypeName targetTypeName;
private final ClassName bindingClassName;
private final boolean isFinal;
private final boolean isView;
private final boolean isActivity;
private final boolean isDialog;
private BindingSet parentBinding;
.........
}
2. BindingSet.brewJava
这个方法里面处理了所有的注解,包含了ViewBinding类的生成过程
JavaFile brewJava(int sdk, boolean debuggable) {
return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
private TypeSpec createType(int sdk, boolean debuggable) {
//类名,修饰符类型为public
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
//final类型判断
if (isFinal) {
result.addModifiers(FINAL);
}
//父类的注解绑定判断
if (parentBinding != null) {
//继承父类
result.superclass(parentBinding.bindingClassName);
} else {
//实现Unbinder接口
result.addSuperinterface(UNBINDER);
}
//target字段的添加
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
//创建构造方法 在最前面的bind方法中view、activity、dialog对应的绑定方法不一样,同理生成的构造方法也不一样
//这里的构造方法其实没有做什么事情,只是相当于一个重载方法入口的匹配过程
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
//绑定是否需要View
if (!constructorNeedsView()) {
// Add a delegating constructor with a target type + view signature for reflective use.
// 添加具有目标类型和视图签名的代理构造方法以供反射使用
result.addMethod(createBindingViewDelegateConstructor());
}
//真正做处理的构造方法在这里创建
result.addMethod(createBindingConstructor(sdk, debuggable));
//类型的绑定需要视图层次结构或者父类的注解绑定不存在
if (hasViewBindings() || parentBinding == null) {
//unbind方法的添加过程
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
至此,ButterKnife的最核心的部分已经讲述完毕...
这个时候,可能会有人说:" 那R2是在什么地方生成的呢? "🐒
R2的出现起初是为了解决ButterKnife在Library中使用时无法使用R进行资源绑定,后来为了统一无论是在什么工程使用统一使用R2,而R2的配置其实是在 butterknife-gradle-plugin里通过gradle插件来实现的.
这里就是R2的诞生之地~
butterknife-lint
作为项目检查,其内部存在一个核心类InvalidR2UsageDetector用来确保生成的R2在注释外部不被引用,感兴趣的朋友可以去看看源码时怎么实现。逃:)