ButterKnife简单实现与源码理解

ButterKnife是一个可以很方便的获取View及处理其他操作的第三方库,出自JW之手。他可以让我们代码中大量的findViewByid省略,专注于业务和逻辑。这些作为安卓开发者都很清楚,不需要多说。

一、自己实现简易ButterKnife

1、原理

  • 注解

    类似一个标签,可以像图钉一样固定在代码中各个位置。

  • 反射

    通过拿到类的字节码文件来获取类中的资源,如属性、方法等。

2、实现

模仿ButterKnife,定义一个入口类,用一个bind方法初始化:

1
2
3
4
5
6
public class YBind {
public static void bind(Activity context){
bindView(activity);
bindClick(activity);
}
}

这里以最常用的绑定view和点击方法为例。

接下来定义两个注解:

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface YBindView {
int value();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface YBindClick {
int value();
}

两个注解上,Target表示该注解可以绑定什么类型的成员;Retention表示该注解的作用范围,这里都是运行时。

然后就是两个具体方法的实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
   private static void bindView(Activity activity) {
//获取class
Class clazz = activity.getClass();
//通过字节码文件获取所有属性
Field[] declaredFields = clazz.getDeclaredFields();
//遍历
for (Field field : declaredFields){
field.setAccessible(true);//解除限定,设置可修改
YBindView annotation = field.getAnnotation(YBindView.class);
if (annotation != null){
//获取每个属性上的注解,如果不为空则拿到注解的值
int id = annotation.value();
try {
//通过findViewById拿到view并赋值
field.set(activity,activity.findViewById(id));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
//绑定点击方法思路相同,只不过是遍历方法,多做一步点击并执行方法
private static void bindClick(final Activity activity) {
Class clazz = activity.getClass();
Method[] declaredMethods = clazz.getDeclaredMethods();
for (final Method method:declaredMethods){
method.setAccessible(true);
YBindClick annotation = method.getAnnotation(YBindClick.class);
if (annotation != null){
int id = annotation.value();
View view = activity.findViewById(id);//依然需要先拿到view
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
method.invoke(activity);//执行对应方法
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
});
}
}
}

最后测试一下,果然是没问题的。

但是,如此火的一个开源库实现难道就这么简单?怎么可能…

二、阅读源码

1、入口

同样的bind方法作为入口,点进源码一看:

1
2
3
4
5
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}

果然还是不同的,继续跟进:

1
2
3
4
5
6
7
8
9
10
11
12
13
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
//...省略掉处理过程
}
}

通过传进来的activity拿到窗口,并且获取到构造器,然后创建实例。那么跟进获取构造器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
} catch (ClassNotFoundException e) {
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}

中间的一句loadClass通过className+ViewBinding拿到对应的字节码文件,拿到构造器并返回。BINDINGS是一个为了能够复用这些构造器创建的map,解决了反射影响性能的问题。

等一下,这就完了?翻了半天,没有其他处理,引入的包里除了入口类只剩下其他所有的注解类。

正纳闷怎么实现的处理,想到直接去搜一下这个ViewBinding,果然是能搜到的。里面通过构造方法,调用了工具类来处理findViewById等操作,就是上面简易实现中的注解和反射的应用了。

但是这个类明显是ButterKnife为我们自动生成的,它又是怎么来的?

2、核心

通过gradle引入的包并不能看到全部源码,所以去github查看他的compiler包,其中一个类ButterKnifeProcessor实现了ViewBinding类的生成,看一下主要过程。

重写process方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override 
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();

JavaFile javaFile = binding.brewJava(sdk, debuggable, useLegacyTypes);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}

return false;
}

首先用findAndParseTargets方法扫描了所有注解类拿到map,再遍历这个map的键值对拿到kv值,然后以此作为信息,通过JavaFile生成java类。至于每个方法的具体实现,直接去看源码就好,就不贴了。

梳理了这些,再回头去看bind流程,就能豁然开朗了。至此算是明白了他的实现思路。

三、小结

果然还是不能把事情想得太简单,自己的简单实现虽然能够使用,但其实缺少太多的细节,比如怎么才能在BaseActivity中一次定义就能随意使用、反射性能问题的处理等。这些都是我远远想不到的,也是为什么大神的开源库能被广泛的使用。

所以说,什么东西都不能只是会用,还要理解原理,最好是还能自己会写。看问题要细致并且挖的深一些,才能有收获。

Powered by Hexo

Copyright © 2018 - 2022 Yshen's Blog All Rights Reserved.

UV : | PV :

Fork me on GitHub