0.文章导入
ButterKnife算是一款知名老牌 Android 开发框架了,通过注解绑定视图,避免了 findViewById() 的操作,广受好评!下面我们从原理开始,一步一步解析黄油刀吧!
1.注解框架
见另外一篇内容,这里就跳过了:https://www.jianshu.com/writer#/notebooks/18080563/notes/42184171
2.原理分析
绑定黄油刀,需要在inflate之后调用ButterKnife.bind(this),那么分析以下bind方法到底干了什么事情。源码如下:
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
可见,首先获取getDecorView,这view是PhoneWindow对应的布局,是一个FrameLayout,DecorView内部又分为两部分,一部分是ActionBar,另一部分是ContentParent,即activity在setContentView对应的布局。然后调用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);
}
}
createBinding 方法中 主要是先找到类对应的构造器constructor,然后通过constructor来new一个新的对象。接下来看看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;
}
先检查BINDINGS是否存在 Class 对应的 Constructor,如果存在则直接返回,否则去构造对应的 Constructor。其中BINDINGS是一个LinkedHashMap:
Map
接下来看当不存在对应 Constructor 时如何构造一个新的,首先如果clsName是系统相关的,则直接返回 null,否则先创建一个新的 Class,再获取类名,clsName + "_ViewBinding",如:MainActivity_ViewBinding.class ,在获取类中的构造方法。这个构造方法中是怎么实现的呢?源码如下:
@UiThread
public MainActivity_ViewBinding(MainActivity target, View source) {
this.target = target;
target.mTextView = Utils.findRequiredViewAsType(source, R.id.hello, "field 'mTextView'", TextView.class);
}
好,我们明白了,原来在这里有通过id找到了target.mTextView。再看看 Utils.findRequiredViewAsType这个方法是怎么实现的。
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}
恍然大悟,其实黄油刀帮你写了findViewById,仅此而已!
3.注解处理器
还记得刚才的MainActivity_ViewBinding.class是怎么生成的吗?要生成这个类就要先得到这个类必须的基础信息,这就涉及到了annotationProcessor技术,和 APT(Annotation Processing Tool)技术类似,它是一种注解处理器,项目编译时对源代码进行扫描检测找出存活时间为RetentionPolicy.CLASS的指定注解,然后对注解进行解析处理,进而得到要生成的类的必要信息,然后根据这些信息动态生成对应的 java 类。接下来分析一下butterknife-compiler 这个黄油刀的注解处理器,主要的方法是:process;
@Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) {
Map bindingMap = findAndParseTargets(env);
for (Map.Entry entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
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;
}
第一步是findAndParseTargets(env),这个方法是注解信息的扫描收集。这个方法有点长,只看看主要的流程部分。
private Map findAndParseTargets(RoundEnvironment env) {
Map builderMap = new LinkedHashMap<>();
Set erasedTargetNames = new LinkedHashSet<>();
scanForRClasses(env);
......
......
// 获取所有使用BindView注解的元素
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
......
......
// 将builderMap中的数据添加到队列中
Deque> entries =
new ArrayDeque<>(builderMap.entrySet());
Map bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
// 出队列
Map.Entry entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
// 查找当前类元素的父类元素
TypeElement parentType = findParentType(type, erasedTargetNames);
// 如果没找到则保存TypeElement和对应BindingSet
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
// 如果找到父类元素,则给当前类元素对应的BindingSet.Builder设置父BindingSet
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// 再次入队列
entries.addLast(entry);
}
}
}
return bindingMap;
}
4.JavaPoet
以上方法遍历 bindingMap,根据BindingSet
得到一个JavaFile
对象,然后输入 java 类,这个过程用到了JavaPoet开源库,提供了一种友好的方式来辅助生成 java 类代码,同时将类代码生成文件,否则需要自己拼接字符串来实现,可以发现BindingSet
除了保存信息目标类信息外,还封装了 JavaPoet 生成目标类代码的过程。
最终生成了一个_ViewBinding.class的文件,关于javapoet后面再学习。