ButterKnife源码剖析

前言

ButterKnife是一个Android视图快速注入库,它通过给view字段添加注解的方式,让我们省去了findViewById()和setOnClick等方法,从而简化了代码。

注意:本文是基于7.0.0版本的剖析。

注解

关于Java中注解的相关知识,建议阅读:注解(张孝祥Java视频笔记)

这里直接介绍ButterKnife中的相关注解,Bind和OnClick。

1
2
3
4
5
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
/** View ID to which the field will be bound. */
int[] value();
}

Bind注解是编译时注解,修饰的对象是字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Target(METHOD)
@Retention(CLASS)
@ListenerClass(
targetType = "android.view.View",
setter = "setOnClickListener",
type = "butterknife.internal.DebouncingOnClickListener",
method = @ListenerMethod(
name = "doClick",
parameters = "android.view.View"
)
)
public @interface OnClick {
/** View IDs to which the method will be bound. */
int[] value() default { View.NO_ID };
}

OnClick注解也是编译时注解,修饰的对象是方法。

这些注解都是编译时注解,在编译的时候由apt(Annotation Processing Tool) 解析自动解析。ButterKnife便是用了Java Annotation Processing技术,就是在Java代码编译成Java字节码的时候就已经处理了@Bind、@OnClick(ButterKnife还支持很多其他的注解)这些注解了。

开发者可以自定义注解,并且自己定义注解解析器来处理它们。Annotation processing是在编译阶段执行的,它的原理就是读入Java源代码,解析注解,然后生成新的Java代码。新生成的Java代码最后被编译成Java字节码,注解解析器(Annotation Processor)不能改变读入的Java 类,比如不能加入或删除Java方法。

例子

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainActivity extends AppCompatActivity {

@Bind(R.id.tv)
TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ButterKnife.bind(this);
}

@OnClick(R.id.tv)
public void MyClick() {
Intent intent = new Intent(MainActivity.this, Main2Activity.class);
startActivity(intent);
}

}

代码编译完成后,会在图示目录下生成MainActivity$$ViewBinder类。
此处输入图片的描述
MainActivity$$ViewBinder的源码后续会进行分析,这里我们只需要知道使用了ButterKnife的名为ClassName的类,在编译后会在相应的目录下生成类名为ClassName$$ViewBinder类即可。

关于ButterKnife是如何生成ClassName$$ViewBinder类的建议阅读:Butterknife 源码剖析(二)

流程分析

借助上面的例子分析ButterKnife的流程,在onCreate()方法中调用了ButterKnife.bind(this),bind方法的源码如下所示:

1
2
3
public static void bind(Activity target) {
bind(target, target, Finder.ACTIVITY);
}

bind方法中又调用了bind方法,参数分别为Activity和Finder,我们看到这里的第三个参数是Finder.ACTIVITY,先简单看下Finder的源码:

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
71
72
73
74
75
public enum Finder {
VIEW {
@Override protected View findView(Object source, int id) {
return ((View) source).findViewById(id);
}

@Override public Context getContext(Object source) {
return ((View) source).getContext();
}
},
ACTIVITY {
@Override protected View findView(Object source, int id) {
return ((Activity) source).findViewById(id);
}

@Override public Context getContext(Object source) {
return (Activity) source;
}
},
DIALOG {
@Override protected View findView(Object source, int id) {
return ((Dialog) source).findViewById(id);
}

@Override public Context getContext(Object source) {
return ((Dialog) source).getContext();
}
};

....(此处省去部分代码)

public <T> T findRequiredView(Object source, int id, String who) {
T view = findOptionalView(source, id, who);
if (view == null) {
String name = getContext(source).getResources().getResourceEntryName(id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' annotation.");
}
return view;
}

public <T> T findOptionalView(Object source, int id, String who) {
View view = findView(source, id);
return castView(view, id, who);
}

@SuppressWarnings("unchecked") // That's the point.
public <T> T castView(View view, int id, String who) {
try {
return (T) view;
} catch (ClassCastException e) {
if (who == null) {
throw new AssertionError();
}
String name = view.getResources().getResourceEntryName(id);
throw new IllegalStateException("View '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was of the wrong type. See cause for more info.", e);
}
}

....(此处省去部分代码)

protected abstract View findView(Object source, int id);

public abstract Context getContext(Object source);

Finder是ButterKnife的一个内部枚举类,这里只贴出了后面分析会用到的代码,其他的由于篇幅的原因省略了。Finder内部定义了两个抽象方法,同时在Finder类的内部又有三个成员枚举类分别实现了这两个抽象方法,这两个抽象方法很简单,第一个方法findView就是调用对应类的findViewById方法来查找对应的对象,第二个方法getContext就是调用对应类的getContext方法来获得对应类型的context。再来看下findRequiredView方法,findRequiredView方法内部调用了findOptionalView方法,而findOptionalView方法内部调用findView方法去查找View,然后强制转换成对应的View类型。
继续看bind方法的源码,源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
static void bind(Object target, Object source, Finder finder) {
Class<?> targetClass = target.getClass();
try {
if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
if (viewBinder != null) {
viewBinder.bind(finder, target, source);
}
} catch (Exception e) {
throw new RuntimeException("Unable to bind views for " + targetClass.getName(), e);
}
}

bind方法主要做了两件事情,一是根据目标类,查找编译时候生成生成的目标类的对应的名为ClassName$$ViewBinder的类,二是调用编译时生成的类的bind方法完成相应的设置。
首先看下findViewBinderForClass(targetClass)方法,源码如下所示:

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
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
throws IllegalAccessException, InstantiationException {
//从内存中查找
ViewBinder<Object> viewBinder = BINDERS.get(cls);
if (viewBinder != null) {
if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
return viewBinder;
}
String clsName = cls.getName();
//检查是否为framework class
if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return NOP_VIEW_BINDER;
}
try {
//实例化“MainActivity$$ViewBinder”这样的类
Class<?> viewBindingClass = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
//noinspection unchecked
viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
//异常,则去父类查找
viewBinder = findViewBinderForClass(cls.getSuperclass());
}
//放入内存并返回
BINDERS.put(cls, viewBinder);
return viewBinder;
}

首先从内存中查找,BINDERS的定义:

1
static final Map<Class<?>, ViewBinder<Object>> BINDERS = new LinkedHashMap<Class<?>, ViewBinder<Object>>();

内存中没有,执行后面的代码,判断如果是framework class,就放弃查找并返回ViewBinder的空实现实例。

1
2
3
4
5
6
public static final String ANDROID_PREFIX = "android.";
public static final String JAVA_PREFIX = "java.";
static final ViewBinder<Object> NOP_VIEW_BINDER = new ViewBinder<Object>() {
@Override public void bind(Finder finder, Object target, Object source) { }
@Override public void unbind(Object target) { }
};

如果不是framework class的话,会去实例化“MainActivity$$ViewBinder”这样的类,如果viewBinder不为空,放入缓存并返回;如果ClassNotFoundException异常则去父类查找。

1
public static final String SUFFIX = "$$ViewBinder";

分析完findViewBinderForClass(Class<?> cls)方法后,我们继续回到上面的bind()方法,查找到的ViewBinder不为空,则执行viewBinder.bind(finder,target,source)方法,我们知道查找到的ViewBinder就是我们在前面提到的MainActivity$$ViewBinder类,接下来我们看看MainActivity$$ViewBinder类的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity$$ViewBinder<T extends tk.thinkerzhangyan.myapplication.MainActivity> implements ViewBinder<T> {
@Override public void bind(final Finder finder, final T target, Object source) {
View view;
view = finder.findRequiredView(source, 2131427422, "field 'mTextViewOne' and method 'MyClickOne'");
target.mTextViewOne = finder.castView(view, 2131427422, "field 'mTextViewOne'");
view.setOnClickListener(
new butterknife.internal.DebouncingOnClickListener() {
@Override public void doClick(
android.view.View p0
) {
target.MyClickOne();
}
});
}

@Override public void unbind(T target) {
target.mTextView = null;
}
}

可以看到MainActivity$$ViewBinder的bind方法中调用了Finder的findRequiredView来查找Activity中的对应id的View,通过前面的分析,我们可以知道findRequiredView方法最终是通过调用Activity的findViewById来查找对应id的View的,查找到对应的View后,给对应的View设置响应的点击事件。至此我们就知道,ButterKnifede是如何工作的了。

通过上面的分析我们可以明白为什么我们用@Bind、@OnClick等注解标注的属性或方法必须是public或protected的,因为ButterKnife是通过MainActivity.this.button来注入View的。

为什么要这样呢?有些注入框架比如RoboGuice你是可以把View设置成private的,答案就是性能。如果你把View设置成private,那么框架必须通过反射来注入View,一个很大的缺点就是在Activity运行时大量使用反射会影响App的运行性能,造成卡顿以及生成很多临时Java对象更容易触发GC,不管现在手机的CPU处理器变得多快,如果有些操作会影响性能,那么是肯定要避免的,这就是ButterKnife与其他注入框架的不同。

通过反射来实现ButterKnife的原理:

当执行绑定的时候,通过反射获取当前类的所有成员变量,然后检查这些成员变量上是否有自己定义的ButterKnife注解,如果有的话获取注解的值,然后调用当前类的findViewById方法来获取View,然后通过反射的方式,将通过findViewById查找到的View,设置给注解修饰的成员变量,点击事件的处理同理。

通过反射的方式,来实现ButterKnife效率不高,因为反射的过程中会产生很多的临时变量,临时变量过多可能会导致内存抖动,内存抖动可能会引起卡顿。

总结

Butterknife是基于Java注解解析技术,自己定义注解,同时通过继承AbstractProcessor自定义注解解析器,编译java源代码的时候,自定义的注解的解析器的process会被调用,查询类中是否含有自己定义的注解,如果含有自己定义的注解的话,就生成相应一个新的java类,新类的名字是ClassName$$ViewBinder,新生成的类实现了ViewBinder接口,实现了bind跟unBind方法,新生成的类也会被编译生成字节码,当程序运行的时候调用Butterknife.bind(this)执行动态绑定的时候,会去查找当前类对应的ClassName$$ViewBinder类,通过反射创建这个类的实例,然后调用其bind方法来完成view的查找和点击事件的设置。

注意由于Butterknife中自定义的注解的是存在与字节码阶段的,所以添加的注解,当加载进JVM的时候,会被忽略,所以不会影响程序的性能。另外注解是在编译阶段处理的,只是消耗编译的时间,不会影响程序的运行时间。

ButterKnife通过注解的方式来绑定View的属性或方法。减少代码的书写,使代码更为简洁明了,同时不消耗额外的性能,当然这样也有个缺点,就是可读性会差一些,需要开发者自己做出取舍。

扩展,如何自定义Android中的注解库

创建一个Module,在这个Module里面自定义注解,然后创建一个Module,在这个Modlue里面自定义注解处理器放。

自定义注解处理器的时候,常用到两个库:auto-service和javapoet

  • auto-service自动用于在META-INF/services目录文件夹下创建javax.annotation.processing.Processor文件;

  • javapoet用于产生 .java 源文件的辅助库,它可以很方便地帮助我们生成需要的.java 源文件。

auto-service使用很简单,只需要在定义的注解处理器上添加@AutoService(Processor.class)即可,示例如下:

1
2
3
4
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
....
}

如果不使用auto-service,需要我们自己手动的在相应的目录下创建相关的文件。步骤如下:
1、在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;

此处输入图片的描述

这个文件的作用,就是告诉jvm(或者是编译器)我们自己定义的注解处理器的位置。如果没有这个文件的话,我们的注解处理器是根本不会起作用的。

关于javapoet的使用,请查阅API。

前面我们提到,需要将自定义的注解和注解解析器放在两个不同的Module里面,之所以将注解和注解处理器放在两个不同的Module里面,主要是因为,只有在编译的时候需要这个注解处理器,而在打包APK的时候这个注解处理器是不需要打包进APK里面的,所以我们单独给注解处理器建了一个Module。

注解处理器只在编译处理期间需要用到,编译处理完后就没有实际作用了,而主项目添加了这个库会引入很多不必要的文件,为了处理这个问题我们需要引入个插件android-apt,它能很好地处理这个问题。

它有两个目的:

  • 允许配置只在编译时作为注解处理器的依赖,而不添加到最后的APK或library
  • 设置源路径,使注解处理器生成的代码能被Android Studio正确的引用
1
伴随着 Android Gradle 插件 2.2 版本的发布,android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt,并推荐大家使用 Android 官方插件提供的相同能力。也就是说,大约三年前推出的 android-apt 即将告别开发者,退出历史舞台,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt。

annotationProcessor的功能和android-apt的功能是一样的。这里我们只介绍annotationProcessor的使用。

在工程中使用我们自定义的注解,需要在工程中依赖包含我们自定义注解的Module,为了使用我们自定义的注解解析器处理我们自定义的注解,需要借助于annotationProcessor。这些都可以通过配置gradle文件来完成,附录使用eventbus的gradle文件。

1
2
3
4
5
dependencies {
compile 'org.greenrobot:eventbus:3.0.0'
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.0.1'
}

依赖自定义的注解库:

1
compile 'org.greenrobot:eventbus:3.0.0'

借助于annotationProcessor依赖,自定义的注解处理处理器:

1
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.0.1'

Android 注解指南

自定义注解之编译时注解(RetentionPolicy.CLASS)(一)(建议先看这个系列)
自定义注解之编译时注解(RetentionPolicy.CLASS)(二)——JavaPoet
自定义注解之编译时注解(RetentionPolicy.CLASS)(三)—— 常用接口介绍

android-apt切换为官方annotationProcessor

Android」Android开发你需要知道的注解(Annotation)

Android进阶之自定义注解(建议看这个)
如何自定义 Android 注解?
Android,几分钟教你怎么应用自定义注解

参考资料:

Butterknife 源码剖析(一) 关于Butterknife的工作流程推荐看这篇

Butterknife 源码剖析(二)

butterknife 源码分析

ButterKnife框架原理

Android ButterKnife 浅析

ButterKnife源码剖析

ButterKnife – 源码分析 – 在‘编译期’间生成findViewById等代码

android-open-source-project-cracking github上的博客

JakeWharton/butterknife

【Android】ButterKnife 8.x详解

Android Studio上方便使用butterknife注解框架的偷懒插件Android Butterknife Zelezny

Java Annotation 及几个常用开源项目注解原理简析

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器