ButterKnife源码解析

ButterKnife介绍

ButterKnife 是 Github 上著名 Android 大佬 JakeWharton 的一个库,主要用于解决 Android 平台上 findViewById(),setOnClickListener() 等重复代码,使用起来非常方便。

几年前的View注入框架

记得刚开始学 Android 的时候,最喜欢的框架就是 xUtils,其中也有一个类似 ButterKnife 中的 @BindView 的注解,叫做 @ViewInject,当时用的很爽,结果后来得知其内部使用的是反射实现的,要知道反射在 Android 上会对性能有一定的影响,又听别人说 Butterknife 不是用反射实现的,然后就舍弃了 xUtils 转用 ButterKnife。

接下来就来看看 ButterKnife 和 xUtils 到底在实现上有哪些不同吧。

xUtils 的实现方式

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
//下面代码来自 xUtils 中 ViewInject.java
private static void injectObject(Object handler, ViewFinder finder) {

Class<?> handlerType = handler.getClass();

// inject ContentView
ContentView contentView = handlerType.getAnnotation(ContentView.class);
if (contentView != null) {
try {
Method setContentViewMethod = handlerType.getMethod("setContentView", int.class);
//反射调用setContentView
setContentViewMethod.invoke(handler, contentView.value());
} catch (Throwable e) {
LogUtils.e(e.getMessage(), e);
}
}

// inject view
//获取 Activity 所用公共字段
Field[] fields = handlerType.getDeclaredFields();
if (fields != null && fields.length > 0) {
for (Field field : fields) {
//处理 @ViewInject 注解
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
try {
//
View view = finder.findViewById(viewInject.value(), viewInject.parentId());
if (view != null) {
field.setAccessible(true);
//反射设置View
field.set(handler, view);
}
} catch (Throwable e) {
LogUtils.e(e.getMessage(), e);
}
} else {
//@ResInject 注解的相关处理
ResInject resInject = field.getAnnotation(ResInject.class);
if (resInject != null) {
try {
Object res = ResLoader.loadRes(
resInject.type(), finder.getContext(), resInject.id());
if (res != null) {
field.setAccessible(true);
field.set(handler, res);
}
} catch (Throwable e) {
LogUtils.e(e.getMessage(), e);
}
} else {
PreferenceInject preferenceInject = field.getAnnotation(PreferenceInject.class);
if (preferenceInject != null) {
try {
Preference preference = finder.findPreference(preferenceInject.value());
if (preference != null) {
field.setAccessible(true);
field.set(handler, preference);
}
} catch (Throwable e) {
LogUtils.e(e.getMessage(), e);
}
}
}
}
}
}

//省略部分代码......
}

上面的代码就是 xUtils 中 ViewInject 这块代码的实现,不仅使用了反射,而且还没有做缓存处理,控件少还好,如控件多,会明显感觉卡顿,那有没有好的办法,答案肯定是有的, ButterKnife 就很好的实现了.下面我们就好好分析下ButterKnife是怎么实现的.

ButterKnife 的基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

class ExampleActivity extends Activity {

@BindView(R.id.user) EditText username;
@BindView(R.id.pass) EditText password;

//String 资源的绑定
@BindString(R.string.login_error) String loginErrorMessage;

//OnClick 事件的绑定
@OnClick(R.id.submit) void submit() {
// TODO call server...
}

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
//必须在 setContentView 之后调用
ButterKnife.bind(this);
// TODO Use fields...
}
}

使用分为两步:

  1. 在需要注入的View上添加注解@BindView
  2. 在onCreate的setContentView后面调用ButterKnife.bind(this);

源码分析

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

//代码目录 ButterKnife.java
//首先调用的就是该方法
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
//获取DecorView 并传入createbinding方法中
return createBinding(target, sourceView);
}

//代码目录同上
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 意为构造器对象,主要用于创建对象用,这里虽然使用了反射,但是在findBindingConstructorForClass方法中做了缓存处理,反复获取同一个只会执行一次反射.
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 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 开头的类 就直接返回null
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 {
// 加载类ActivityName_ViewBinding类是在编译的时候生成的
Class<?> bindingClass = Class.forName(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;
}

ButterKnife的源码的核心部分大部分就是上面这些,大致步骤如下

  1. 基于编译时注解解析,来生成ActivityName_ViewBinding类,类在 build/generaated/source/apt/packageName/ActivityName_ViewBinding.java 目录下.
  2. 在程序运行的时候,用反射调用生成的ActivityName_ViewBinding.java,在类中调用findViewById方法.

总结

ButterKnife 的核心是使用 APT 在代码编译的时候,自动生成代码,这一块涉及的内容比较多以后在单独分析,总之只需要知道ButterKnife 的实现方式就是帮你生成了 findViewById() 方法并没有使用反射强行赋值。