拆轮子系列之ButterKnife源码分析

概述

我们都听过或者用过butterknife,知道butterknife是一款View注入的框架。在android开发时省去我们重复的敲打findViewById等方法。ButterKnife源码的分析将会先行讲解注解,反射,注解处理器,再深入分析源码。

注解

说起注解,大家都很熟悉。不管是Java语言本身自带的@Override、@Override,还是热门的第三方框架butterknife的@BindView、retrofit的@Get等

Java自带的注解

普通注解-用于描述代码的注解

  • @Override - 检查方法是否是重载。如果在父类或者实现的接口里没有找到原型的话,编译器就会报错。
  • @Deprecated - 标记该方法已弃用。如果使用的话,编译器会提示。
  • @SuppressWarnings - 使编译器忽略编译时的警告,使用如@SuppressWarnings(“unused”),忽略无用代码的提示

元注解-注解的注解

元注解-meta-annotation,即注解用来注解其他注解的注解,常用来自定义注解。
栗子:

1
2
3
4
5
6
7
@Document
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Test {
public String name() default "";
}

元注解有四种,栗子里都用到,分别为meta-annotation,Document、Target、Retention、Inherited:

@Document

Document标记这个注解应该被javadoc工具记录。默认情况下,Javadoc是不包括注解的。

@Target

Target描述了这个注解的使用范围,使用方法如@Target(ElementType.TYPE),ElementType的取值有七种,如下:

  • CONSTRUCTOR:用于描述构造器
  • FIELD:用于描述域
  • LOCAL_VARIABLE:用于描述局部变量
  • METHOD:用于描述方法
  • PACKAGE:用于描述包
  • PARAMETER:用于描述参数
  • TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Retention

Retention用来描述这个注解的生命周期,即注解的”存活时间”,使用方法如@Retention(RetentionPolicy.RUNTIME),RetentionPolicy有三种,如下:

  • SOURCE: 将被编译器丢弃(即源文件保留)
  • CLASS: 在class文件中可用,但会被JVM丢弃(即class保留)
  • RUNTIME: 在运行时有效(即运行时保留)

我们知道,Java代码会由源代码编译成class文件,然后再运行。Retention用来描述注解在这个阶段的存活时间。例如,当一个注解为RUNTIME时,表明我们可以在运行时通过反射获取到注解的内容,然后再去做相应的处理。

@Inherited

Inherited译为可继承的,如果一个使用了@Inherited 修饰的 annotation类型 被用于一个 class,则这个 annotation 将被用于该class的子类。

认识这四个元注解,我们就能定义属于自己的注解!

自定义注解

使用@interface自定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组)。可以通过default来声明参数的默认值。

值得一提的是,如果只有一个参数成员,最好把参数名称设为”value”,这样子,这样做有个好处。例如注解 A,正常调用时这样子:@A(value=”jerry”),可以简化为@A(“jerry”)。

这里我举两个栗子来进行介绍,一个是butterknife中的BindView注解,一个是项目中使用到的注解。

butterknife的BindView

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

BindView注解用来冗杂的findViewById操作,使用如下:

1
@BindView(R.id.title) TextView title;

@Retention(CLASS)表明了BindView在class文件中保留,在runtime时不存在,这意味着butterknife是在编译时生成绑定View的代码,因此butterknife在运行时不会造成影响,只会影响编译时的速度,这也解释了butterknife受欢迎的原因。

@Target(FIELD)表明该注解是用来修饰域变量的,如上面栗子所示。

@IdRes int value(); @IdRes是android.support.annotation里定义的注解,表示只接受类似R.id.xxx形式(即为int的Id)的参数。

反射

反射机制允许在运行时发现和使用类的信息。Class类与java.lang.reflect一起对反射的概念进行了支持,允许在运行时利用反射达到:

  • 判断任意一个对象所属的类;
  • 构造任意一个类的对象;
  • 判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 调用任意一个对象的方法

使用反射来模仿ButterKnife

这里使用反射来模仿下ButterKnife来简化这些代码。当然ButterKnife不是用反射的,它是使用注解处理器的
先定义个BindView注解:

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

调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MainActivity extends Activity {
@BindView(R.id.tv_title)
private TextView tvTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
NewerKnife.bind(this);
tvTitle.setText("after bind view");
}
}

在声明TextView时使用BindView注解,然后setContentView后,使用NewerKnife.bind(this),这样就可以避免繁琐的findViewById操作。

findViewById的操作都在NewerKnife.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
public class NewerKnife {
public static void bind(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 获取类的所有域变量
Field[] fields = clazz.getFields();
for (Field field : fields) {
// 获取BindView注解
BindView bindView = field.getAnnotation(BindView.class);
if (bindView != null) {
// 存在注解,获取int值,ex:R.id.tv_title
int viewId = bindView.value();
try {
// 获取findViewById的Method实例
Method findViewMethod = clazz.getMethod("findViewById", int.class);
// 调用findViewMethod来获得View
Object view = findViewMethod.invoke(activity, viewId);
// tvTitle-private,设置可操作性
field.setAccessible(true);
// 将结果赋值给field
field.set(activity, view);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}

注解处理器-AbstractProcessor

前面说到的反射是在运行时处理注解,有些人可能担心反射对应用的性能有大的影响。其实通过注解处理器可以在编译时扫描注解,处理注解,包括生成代码等,这样就不会对应用的性能造成影响。前面我们提到的ButterKnife就是通过注解处理器来的。

Element

在Java中,将源代码看成Element,Element的子接口有ExecutableElement, PackageElement, Parameterizable, QualifiedNameable, TypeElement, TypeParameterElement, VariableElement。通过下面的栗子就可以直观的理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.jerry.practice; // PackageElement
public class Foo { // TypeElement
private int a; // VariableElement
private Foo b; // VariableElement
public Foo() {
} // ExecuteableElement
public void setA( // ExecuteableElement
int newA // TypeParameterElement
) {
}
}

TypeMirror

TypeMirror表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。

Element代表的是源代码。举个栗子,TypeElement代表源代码中的类或接口的元素,是未经编译的。也就是说我们无法根据TypeElement获取类的信息,例如类里面的方法、域、超类等信息。而通过TypeElement.asType()得到的TypeMirror就可以知道。

AbstractProcessor介绍

AbstractProcessor有四个比较重要的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// ...
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}

init

初始化处理器,提供以下工具类

  • Elements:一个用来处理Element的工具类,例如获取元素的包和注释等。
  • Types:一个用来处理TypeMirror的工具类,例如获取类型的超类型等。
  • Filer:顾名思义,使用Filer你可以创建文件。
  • Messager:用来打印输出日志信息。

process

扫描处理注解,生成代码,必须重载。入参介绍:

1
2
Set<? extends TypeElement> annotations //需要被处理的注解集合
RoundEnvironment roundEnv //通过这个可以查询出包含特定注解的被注解元素

如果process返回true,说明不需要后续的Processor处理它;返回false则需要。

getSupportedAnnotationTypes
返回你需要处理的注解集合,可以@SupportedAnnotationTypes代替。

getSupportedSourceVersion
指定Java版本,可以用@SupportedSourceVersion代替。

AbstractProcessor使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({"com.jerry.annotation.ProcessorParam"})
public class Processor extends AbstractProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("-------------------in");
Set<Element> elements = (Set<Element>) roundEnv.getElementsAnnotatedWith(ProcessorParam.class);
for (Element element : elements) {
System.out.println(element.getSimpleName());
System.out.println(element.getAnnotation(ProcessorParam.class).value());
}
System.out.println("-------------------");
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends Activity {
@ProcessorParam("jerry")
private String param;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

万事俱备只欠东风,我们需要将AbstractProcessor注册到编译器中。由于AbstractProcessor是属于javax.annotation.processing包的,因此需要新建一个依赖的Java Library。在这个library里,在src/main/resources/META-INF/services里需要有javax.annotation.processing.Processor这个文件,里面放了注解处理器的路径,例如:

1
com.jerry.annotation.Processor

ButterKnife源码分析


上文我们详细介绍了注解处理器,这里再结合butterknife再次强调下。在编译源文件时,会分析扫描注解,当扫描到butterknife定义的@BindView、@OnClick等注解时,会使用JavaPoet来生成代码。生成后的文件会再次分析,直到没有分析到需要处理的注解位置。

JavaPoet简介

Poet译为诗人,JavaPoet可以帮助便捷地生成代码,而不是手动繁琐的拼接语句。简要介绍下比较关键的几个类:

  • MethodSpec 代表一个构造函数或方法声明。
  • TypeSpec 代表一个类,接口,或者枚举声明。
  • FieldSpec 代表一个成员变量,一个字段声明。

ButterKnife效果

源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MainActivity extends Activity {
@BindView(R.id.tv_title)
public TextView tvTitle;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
@OnClick(R.id.tv_title)
void titleClick() {
}
}

编译生成的文件可以在build/source/apt下可以看到:

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
public class MainActivity_ViewBinding implements Unbinder {
private MainActivity target;
private View view2131034112;
@UiThread
public MainActivity_ViewBinding(MainActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
this.target = target;
View view;
view = Utils.findRequiredView(source, R.id.tv_title, "field 'tvTitle' and method 'titleClick'");
target.tvTitle = Utils.castView(view, R.id.tv_title, "field 'tvTitle'", TextView.class);
view2131034112 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.titleClick();
}
});
}
@Override
@CallSuper
public void unbind() {
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.tvTitle = null;
view2131034112.setOnClickListener(null);
view2131034112 = null;
}
}

注意到源文件名是MainActivity,而生成的文件是MainActivity_ViewBinding。

在构造函数内,使用

1
target.tvTitle = Utils.findRequiredViewAsType(source, R.id.tv_title, "field 'tvTitle'", TextView.class);

利用Utils.findRequiredViewAsType得到的结果赋值到我们定义的TextView(tvTitle)上,这也解释了为什么tvTitle需要用public来修饰。

进一步看看findRequiredViewAsType。

1
2
3
4
5
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}

castView方法是将得到的View转化成具体的子View,这里是TextView。而findRequiredView里进行了findViewById的操作。

这里我们可能会有疑问,在生成的MainActivity_ViewBinding的构造方法使用到MainActivity,而我们在使用ButterKnife时会使用ButterKnife.bind(this)将Activity传递到ButterKnife里,这之间是怎么一个过程?

进去ButterKnife.bind(this)看看。

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

根据activity得到DecorView,再传递到createBinding。

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
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);
}
}

上面的代码里,获取到Constructor后,再运用反射生成实例,在实例里的findView操作就会被调用到。接下来看看如何获取到Constructor的。

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
@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 = 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;
}

从上面可以看到Constructor获取的过程,根据className得到className_ViewBinding,就可以得到Constructor。并且会将得到的Constructor缓存起来,避免反射的性能问题。

这样一来,ButterKnife.bind(this)传递进去的MainActivity会通过反射生成MainActivity_ViewBinding实例。在这个实例的构造函数内,进行findViewById、setOnclickListener等操作

编译过程

MainActivity在编译时会生成处理findViewById等操作的MainActivity_ViewBinding。接下来我们探索下ButterKnife偷偷在注解处理器里做了什么。

从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);
// 利用JavaPoet生成代码
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
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;
}

process里主要做了两件事情:

  • findAndParseTargets
    获得TypeElement -> BindingSet的映射关系,TypeElement指的是类或接口,在本文所举的栗子中是MainActivity。BindingSet里包含了生成代码时的一些参数。
  • 运用JavaPoet框架来生成代码
    生成的代码类形式为xxxx_ViewBinding

深层的具体代码就暂不分析了,从宏观上把握以下ButterKnife的内部构造。。。。。