深入浅出Android注解式绑定控件

吐槽

Android开发中,我们经常写一大坨findViewById(int)类似代码,既浪费时间代码又不美观,所以很多人选择使用第三方注解式绑定库butterknife,其实Android注解式绑定控件,没你想象的那么难,我们自己也可以实现,那就开始动手吧。

注解

什么是注解?注解是用来干嘛的?

从jdk1.5开始,Java提供了注解的功能,允许开发者定义和使用自己的注解类型,该功能由一个定义注解类型的语法和描述一个注解声明的语法,读取注解的API,一个使用注解修饰的class文件和一个注解处理工具组成。首先,你需要接受一个关键字@interface,它可不是接口定义关键字,是Java中表示声明一个注解类的关键字。使用@interface表示我们已经继承了java.lang.annotation.Annotation类,这是一个注解的基类接口,就好像Object类,现在你只需要知道它的存在就行了。还有一条规定:在定义注解时,不能继承其他的注解或接口。那么,这就是最简单的一个注解类

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

@Target的意思是我们注解作用的目标,这里是ElementType.FIELD,也就是作用于字段的
ElementType的类型有以下几种:

  1. CONSTRUCTOR:用于描述构造器
    1. FIELD:用于描述字段
    2. LOCAL_VARIABLE:用于描述局部变量
    3. METHOD:用于描述方法
    4. PACKAGE:用于描述包
    5. PARAMETER:用于描述参数
    6. TYPE:用于描述类、接口(包括注解类型) 或enum声明

@Retention的意思是注解的运行级别
RetentionPolicy的类型有以下几种

  1. SOURCE:在源文件中有效(即源文件保留)
    1. CLASS:在class文件中有效(即class保留)
    2. RUNTIME:在运行时有效(即运行时保留)

使用注解

注解类型是不能直接new对象的,那么这个BindView对象从哪里来呢?
这时就需要用到Java的反射机制。我们知道,每一个继承自Object类的类都会继承一个getClass()方法下面看一下这个方法的原型:

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
private transient Class<?> shadow$_klass_;
private transient int shadow$_monitor_;
/**
* Returns the runtime class of this {@code Object}. The returned
* {@code Class} object is the object that is locked by {@code
* static synchronized} methods of the represented class.
*
* <p><b>The actual result type is {@code Class<? extends |X|>}
* where {@code |X|} is the erasure of the static type of the
* expression on which {@code getClass} is called.</b> For
* example, no cast is required in this code fragment:</p>
*
* <p>
* {@code Number n = 0; }<br>
* {@code Class<? extends Number> c = n.getClass(); }
* </p>
*
* @return The {@code Class} object that represents the runtime
* class of this object.
* @jls 15.8.2 Class Literals
*/
public final Class<?> getClass() {
return shadow$_klass_;
}

这个方法返回的是该类的Class对象,Class中有一个方法叫getDeclaredFields(),是用来返回这个类的全部字段,返回类型是Field[]
通过Field对象的getAnnotation(Class<?>)方法,我们可以获取到任何一个Class的对象,通过getAnnotation(Class<?>),我们就可以获取到BindView的对象了。

1
2
3
4
5
Field[] fields = currentClass.getClass().getDeclaredFields();
for (Field field : fields) { // 遍历所有字段
// 获取字段的注解,如果没有BindView注解,则返回null
BindView bindView = field.getAnnotation(BindView.class);
}

Android项目中应用

定义好了注解,如果我们直接来使用,是没有任何效果的,因为注解只是一段代码,并没有关联上我们的控件,所以我们要编写一个工具类来做关联

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
public class AnnotateUtils {
public static void bind(Activity activity) {
Class<? extends Activity> object = activity.getClass(); // 获取activity的Class实例
Field[] fields = object.getDeclaredFields(); // 通过Class实例获取activity的所有字段
for (Field field : fields) { // 遍历所有字段
// 获取字段的注解,如果没有BindView注解,则返回null
BindView bindView = field.getAnnotation(BindView.class);
if (bindView != null) {
int viewId = bindView.value(); // 获取字段注解的参数,这就是我们传进去控件Id
if (viewId != -1) {
try {
// 获取类中的findViewById方法,参数为int
Method method = object.getMethod("findViewById", int.class);
// 执行该方法,返回一个Object类型的View实例
Object resView = method.invoke(activity, viewId);
field.setAccessible(true);
// 把字段的值设置为该View的实例
field.set(activity, resView);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
}

当我们需要使用注解的时候,我们只需要在定义View的时候,在View的字段上添加对应的注解,把Id传入,然后在onCreate方法中调用上面这个工具类的方法即可。

1
2
3
4
5
6
7
8
9
10
11
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_hello)
private TextView tvHello;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AnnotateUtils.bind(this);
}
}

几点说明:

  1. 如果注解中的值不是value,那么在进行注解是时候,需要给出对应的值的名字,假如我们在注解中做了如下定义:
    1
    2
    3
    4
    5
    6
    7
    8
    @Target(FIELD)
    @Retention(RUNTIME)
    public @interface BindView {
    /**
    * View ID to which the field will be bound.
    */
    @IdRes int id();
    }

那么在注解的时候,需要这样:

1
2
@BindView(id==R.id.tv_hello)
private TextView tvHello;

2 .使用注解的时候,句末不能加”;”

  1. Android中的注解式绑定控件(也是所谓的IOC控制反转在安卓中的一种应用)其实本质的使用就是Java基础中反射的使用。值得一提的是,反射执行的效率是很低的.

热评文章