1. Butter Knife 介绍
Butter Knife 是一个开源的依赖注入框架, 主要起到一个语法糖的效果, 比如 Button button = (Button) findViewById(R.id.button);
, 就可以简化成 @BindView(R.id.button) Button button;
. 详细的可以看这里. 显然可以看出是使用注解做到的, 至于是编译时注解还是运行时注解, 下面会开始分析. 这里使用的版本是 8.6.0, 简单起见, 本篇只分析 View 的注入.
2. 注入原理分析
这里是一个 activity
public class LoginActivity extends Activity {
@BindView(R.id.loginButton)
Button button;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.login_layout);
ButterKnife.bind(this);
}
......
}
所以是怎么注入的呢 ? 感觉应该和 ButterKnife.bind(this);
有关.
该方法如下
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
继续点开 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());
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);
}
}
简化一下也就是这样
private static Unbinder createBinding(Object target, View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
return constructor.newInstance(target, source);
// 异常处理 ...
}
Unbinder
是一个接口, 声明如下
public interface Unbinder {
@UiThread void unbind();
Unbinder EMPTY = new Unbinder() {
@Override public void unbind() { }
};
}
大概逻辑就是找到要绑定的那个类的构造函数, 然后利用反射创造出实例, 这里的声明是 Constructor<? extends Unbinder>
, 所以返回的是一个 Unbinder
实例. 如果没找到构造函数, 就返回一个 new 出来的 Unbinder
.
继续点开 findBindingConstructorForClass
@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<?> 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;
}
稍微简化一下
private static Constructor<? extends Unbinder> findxxx(Class<?> cls) {
// 首先在缓存中寻找
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) { return bindingCtor; }
// 缓存中没有
String clsName = cls.getName();
// 是否是 android 源码中的文件
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");
bindingCtor = (Constructor<? extends Unbinder>)
bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
// 继续在父类中寻找
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
}
// 异常处理
// 放入缓存
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
BINDINGS
是一个 map, 声明如下
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS =
new LinkedHashMap<>();
作用是缓存类对应的构造函数
所以 findBindingConstructorForClass
的主要过程是这样的:
-
在缓存中寻找某个类的构造函数
有 -> 返回构造函数
没有 -> 转 2
-
是否是 android 源码中的文件
是 -> 返回 null
没有 -> 转 3
取得某个类的构造器, 这个类的名字是
clsName + "_ViewBinding"
出异常则将参数换为父类, 返回 1
将构造函数放入缓存
返回构造函数
这里要注意的有 3 个:
这里用 map 作为缓存, 是为了加快速度, 因为反射的效率是很低的, 而移动设备对性能的要求比较高
之所以要添加第 2 步, 是因为要在父类中寻找, 可能找到 android 源码里去.
这里找了很久的构造函数, 并不是传入的类的, (啊这不是废话吗), 是
clsName_ViewBinding
这个类的. 在这里的话应该叫LoginActivity_ViewBinding.java
终于找到和 View 注入相关的东西了.现在有 1 个问题, 这是啥 ?
在 intellij idea
中, 使用 ctrl + shift + n
组合键, 可以找到这个文件, 路径如下: app\build\generated\source\apt\debug\...\LoginActivity_ViewBinding.java
public class LoginActivity_ViewBinding implements Unbinder {
private LoginActivity target;
private View view2131558546;
@UiThread
public LoginActivity_ViewBinding(LoginActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public LoginActivity_ViewBinding(final LoginActivity target, View source) {
this.target = target;
View view;
// findRequiredView 中利用 findViewById 找到 view
view = Utils.findRequiredView(source, R.id.loginButton, "field 'button' and method 'login'");
// castView 中利用 cls.cast(view); 将 view 强转成 button
target.button = Utils.castView(view, R.id.loginButton, "field 'button'", Button.class);
view2131558546 = view;
// other
}
@Override
@CallSuper
public void unbind() {
LoginActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.button = null;
view2131558546 = null;
}
}
我们现在来捋一捋
之前调用的
constructor.newInstance(target, source);
其实调用了LoginActivity_ViewBinding(final LoginActivity target, View source)
, 返回的Unbinder
实例也就是LoginActivity_ViewBinding
实例, 所以一开始的ButterKnife.bind(this);
方法的返回值就是这个实例, 这个实例只有一个方法unbind()
, 用于解除当前 activity 的绑定.在构造方法中, 用
findViewById
找到对应的 view 并强转成 button, 然后直接赋给target.button
, 所以用Butter Knife
时, 用注解标注的内容不能是 private 的, 否则会拿不到这个成员变量.
所以 Butter Knife
实现依赖注入的方法就是额外生成一个类, 在类的构造函数中写入我们偷懒没写的 findViewById
等方法, 并添加一个解除绑定的方法.
在调用 ButterKnife.bind(xxx);
方法时会利用反射生成额外类的实例, 此时绑定便完成了.
到这里就解释完了是怎么实现依赖注入的, 接下来研究这个类是怎么生成的.
3. 辅助类的生成
1. 信息构建
既然是额外生成辅助类实现的, 那么可以肯定是利用编译时注解. 其实直接看 BindView
的定义, 会发现是 CLASS
级别的, 所以当然是用注解处理器处理的. 关于编译时注解参见上一篇, 其中提到了接下来比较重要的 element
.
在 github
上看 Butter Knife
的源码, 可以发现目录结构如下
butterknife // 上面分析的代码都在这里
butterknife-annotations // 自定义注解
butterknife-compiler // 注解处理
butterknife-gradle-plugin // gradle 插件
butterknife-lint // lint 检查
在 butterknife-compiler
包下有 ButterKnifeProcessor.java
文件, 这就是我们要找的注解处理器了.
类的声明如下 public final class ButterKnifeProcessor extends AbstractProcessor
我们先看 init()
方法
private Elements elementUtils; // element 辅助类
private Types typeUtils; // 操作 TypeMirror 的辅助类
private Filer filer; // 文件操作辅助类
@Override public synchronized void init(ProcessingEnvironment env) {
super.init(env);
String sdk = env.getOptions().get(OPTION_SDK_INT);
if (sdk != null) {
try {
this.sdk = Integer.parseInt(sdk);
} catch (NumberFormatException e) {
env.getMessager()
.printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
+ sdk
+ "'. Falling back to API 1 support.");
}
}
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
try {
trees = Trees.instance(processingEnv);
} catch (IllegalArgumentException ignored) {
}
}
可以看到主要是对一些辅助类进行了初始化.
接着看最重要的 process
方法
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
// 解析每个注解
// TypeElement 表示类或接口级别的元素
// BindingSet 是自定义的一个类, 表示所有需要绑定的元素的信息的集合
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
// 利用 javapoet 生成 java 文件, 也就是 xxx_ViewBinding.java
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;
}
这里利用 javapoet 进行代码生成, 这是同作者写的另一个开源的代码生成框架.
这里先不管这个, 往下看 findAndParseTargets
实现
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
// 这个 map 的 key 是 类或者接口 类型的 element, value 是对应的 builder, 用于存储要生成的辅助类的一些信息
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
scanForRClasses(env);
// 其他注解的处理
// 处理每个用 BindView 注解标注的元素, 这里的 Element 都是 Field 级别的
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.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).
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;
}
主要逻辑写在了 parseBindView
里.
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
// 得到父元素, 这里一般是 activity 类
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// 检查元素是否可达(private? public? 是否写在类的成员变量里?) 是否在错误的包里 (安卓源码 ?)
boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element) || isBindingInWrongPackage(BindView.class, element);
// 类型检查这里被我省略了
if (hasError) {
return;
}
// 得到 view 的 id
int id = element.getAnnotation(BindView.class).value();
// 检查缓存中是否有这个 activity
BindingSet.Builder builder = builderMap.get(enclosingElement);
// QualifiedId 就是带上 activity 所在包名的信息的 id
QualifiedId qualifiedId = elementToQualifiedId(element, id);
// 检查这个 id 是否已经被绑定过了
if (builder != null) {
String existingBindingName =
builder.findExistingBindingName(getId(qualifiedId));
// 如果不为 null 说明已经被绑定过了, 直接返回
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 不存在, 就创建一个新的, 并放入 map 中
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
// 在该 builder 中添加新的 field 信息, 即关于绑定的信息
builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}
这里比较重要的是 builder 的创建过程, 即 getOrCreateBindingBuilder
private BindingSet.Builder getOrCreateBindingBuilder(
Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder == null) {
// 传入父元素, 即类信息创建 builder
builder = BindingSet.newBuilder(enclosingElement);
builderMap.put(enclosingElement, builder);
}
return builder;
}
其中 BindingSet.newBuilder
如下
static Builder newBuilder(TypeElement enclosingElement) {
// 利用 typemirror 可以获得元素的详细类型信息
TypeMirror typeMirror = enclosingElement.asType();
boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);
TypeName targetType = TypeName.get(typeMirror);
if (targetType instanceof ParameterizedTypeName) {
targetType = ((ParameterizedTypeName) targetType).rawType;
}
String packageName = getPackage(enclosingElement).getQualifiedName().toString();
String className = enclosingElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
// 这里是 javapoet 的语法, 后一个参数是类名, 即我们要找的 xxx_ViewBinding
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}
综上, 每处理一个 @BindView
注解标注的元素, 会经历如下过程
得到该元素的父元素, 一般为 activity
合法性检查
检查 builderMap 中是否有该父元素对应的值 (builerMap 用于缓存, key 是类或者接口 类型的元素, value 是对应的 builder, builder 用于存储要生成的辅助类的类名, 其中需要绑定的元素(如 field), 以及一些其他信息 )
如果没有, 新建相应的键值对, 这时 builder 包含一些类本身的信息, 如类名, 是否是 final, 是否是 activity 等等
如果有, 获取 id, 并检查 id 是否已被绑定, 被绑定则报错
在获得的 builder 中添加该元素的相关信息, 该信息用于生成在
xxx_ViewBinding
的构造函数中的查找及赋值的相关代码.builderMap 会经过一些处理(父类子类关系的调整等), 最后转变成 BindingSet, 也就是我们一开始看到的
Map<TypeElement, BindingSet> bindingMap
这里完成了类信息的构建, 接下来还有最后一步, 代码生成
2. 代码生成
代码生成是这句话: JavaFile javaFile = binding.brewJava(sdk);
, 获得的 javaFile
包含了整个类的信息, 点进 brewJava
看看.
JavaFile brewJava(int sdk) {
return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
这里重要的是 createType
// 利用 bindingset 中的信息生成最终的辅助类
private TypeSpec createType(int sdk) {
// 添加 public 修饰
// TypeSpec 用于生成类
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);
}
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
// 创建对应的构造函数
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
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) {
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
这里是构造函数的生成
private MethodSpec createBindingConstructor(int sdk) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
if (hasMethodBindings()) {
constructor.addParameter(targetTypeName, "target", FINAL);
} else {
constructor.addParameter(targetTypeName, "target");
}
if (constructorNeedsView()) {
constructor.addParameter(VIEW, "source");
} else {
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());
}
if (hasOnTouchMethodBindings()) {
constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
.addMember("value", "$S", "ClickableViewAccessibility")
.build());
}
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);
}
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(constructor, binding);
看一下
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();
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;
}
List<MemberViewBinding> requiredBindings = binding.getRequiredBindings();
if (requiredBindings.isEmpty()) {
result.addStatement("view = source.findViewById($L)", binding.getId().code);
} else if (!binding.isBoundToRoot()) {
result.addStatement("view = $T.findRequiredView(source, $L, $S)", UTILS,
binding.getId().code, asHumanDescription(requiredBindings));
}
addFieldBinding(result, binding);
addMethodBindings(result, binding);
}
代码很简单, 可以看到 view = $T.findRequiredView(source, $L, $S)
代表生成的代码中的 view = Utils.findRequiredView(source, R.id.loginButton, "field 'button' and method 'login'");
代码生成部分与 javapoet
关系比较强, 所以还是要先了解 javapoet
才能比较深入地了解, 这里就不继续分析了.
总结一下, Butter Knife
主要是利用编译时注解和 javapoet
在编译时动态生成辅助类, 在 bind
方法运行时运用反射创建辅助类的实例, 起到了语法糖的效果==.