概述
日常开发中的各种注解还是比较常见的,比如代码里面的各种@Override,@Nullable,后端的话Spring里面的各种注入以及依赖注入框架Dagger.对于更关注于界面的客户端,大名鼎鼎的ButterKnife以及XUtils中的ViewInject也是基于注解实现和使用的。先不去纠结运行效率,自己来写一段简单的注解代码,了解背后的原理。
基本原理
基于注解的开发,有一个基本概念IOC(Inverse Of Control):控制反转,简单说就是new对象的事情交给框架完成。但实际上类似ButterKnife等框架要做的事情是不让开发人员写一堆的findViewById、成员变量和类型转换(Android API26以后findViewById的返回值有变化,不用再强转),要达到此效果还需要配合ButterKnife在Android Studio中的插件使用。而实现注入的基础就是反射+自定义注解。
注入实现的基础步骤
先定义一个注解,取名Inject.通常自定义注解里面要声明两个关键字@Rentention(该注解何时生效)、@Target(该注解的适用范围)。注解内部可以定义其可以接受的类型,名称名可以随意,但如果是value的话,可以省略。这里我定义为name
@Retention(RetentionPolicy.RUNTIME) //该注解会被加载到JVM中
@Target(ElementType.FIELD) //该注解只能用在字段上面(成员变量)
public @interface Inject {
//代表该自定义注解类可以接受的类型
String name();
}
新建一个JavaBean,取名Person,创建一个成员变量player,并且使用@Inject,其值为欧文。注意因为名称是name,所以不可以忽略。要用’名称=值’的形式,如果名称为value才是可以缺省的。
public class Person {
@Inject(name="欧文")
private String player;
@Override
public String toString() {
return "Person{" +
"player='" + player + '\'' +
'}';
}
}
在主方法中,通过暴力反射获取Person里的player字段,再获取该字段上的自定义注解对象(要传入具体的注解类,因为一个字段是可以跟多个注解的),进一步获得该注解对象的值,至此就可以将值赋给player字段,也就完成了对player的注入。
Person person = new Person();
Class clazz = Person.class;
Field field = clazz.getDeclaredField("name");
/*
* 获取该字段上的自定义注解类对象
*/
Inject annotation = field.getAnnotation(Inject.class);
/*
* 获取Inject上的值
*/
String name1 = annotation.name();
field.setAccessible(true);
field.set(person, name1);
System.out.println(person);
类似的,如果要注入View到成员变量,也是这个路数,只不过相应的自定义注解的值类型是int(控件ID),当然反射获取的就是整个Activity中定义的成员变量了,因为我们对控件的声明都会写在这里
注入View到成员变量
1.自定义一个注解类ViewInject(随意命名),运行时获取控件的ID,声明在字段上:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ViewInject {
int value();
}
2.以Activity组件示例,成员变量声明一个布局中的控件,并为其添加注解:
@ViewInject(R.id.text)
private TextView mTextView;
3.setContentView后,布局组件生效,将Activity作为参数,开始注入:
ViewUtils.inject(this);
4.开始完成ViewUtils类,也就是真正执行注入操作的地方:
private static void bindView(Activity activity) {
//1.获取声明组件的字节码
Class<? extends Activity> clazz = activity.getClass();
//2.获取字节码中所有字段
Field[] declaredFields = clazz.getDeclaredFields();
for (Field field : declaredFields) {
//3.遍历,筛选出只添加了@ViewInject的字段
ViewInject annotation = field.getAnnotation(ViewInject.class);
if (annotation != null) {
//4.获取自定义注解上的id,通过id获取到该控件
View view = activity.findViewById(annotation.value());
if (view != null) {
try {
//5.通过暴力反射将view赋值给该字段
field.setAccessible(true);
field.set(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
5.Activity里验证:mTextView.setText("成功注入");
事件的绑定
以最简单的点击事件为例,展示如何通过注解实现事件的绑定。同样也是为了简单,先看怎么实现单个控件的事件绑定。
1.自定义一个注解类OnClick(随意命名),运行时获取控件的ID,声明在方法上:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnClick {
int value();
}
2.以Activity组件示例,随便命名一个方法作为点击事件绑定的对应方法,并为其添加注解,这里的形参为空:
@OnClick(R.id.text)
private void clickView() {
}
3.完成ViewUtils类中进行事件绑定的代码部分
private static void bindOnClick(final Activity activity) {
//1.获取声明组件的字节码
Class<? extends Activity> clazz = activity.getClass();
//2.获取所有声明的方法
Method[] declaredMethods = clazz.getDeclaredMethods();
for (final Method method : declaredMethods) {
//3.遍历,筛选出只添加了@OnClick的字段
OnClick annotation = method.getAnnotation(OnClick.class);
if (annotation != null) {
final int value = annotation.value();
//4.获取自定义注解上的id,通过id获取到该控件
final View view = activity.findViewById(value);
//5.在该控件的点击事件中,通过暴力反射调用对应在Activity中声明的方法
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
method.setAccessible(true);
method.invoke(activity);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
});
}
}
}
和View的注入类似,就是把本来在Activity做的setOnClickListener拿到这里面来,而通过反射调用在Activity里声明的方法,也就相当于绑定了事件。注意因为声明的方法是无参的,因此反射调用方法时可变参数的长度为0
4.Activity里验证:
@OnClick(R.id.text)
private void clickView() {
Toast.makeText(this, "事件绑定成功", Toast.LENGTH_SHORT).show();
}
总结
一个很简单的IOC框架完成了,但是距离实际使用差的还多,一个是基于运行时的注解通过反射完成,效率还是多少受了影响,虽然实际上是感受不出来的。(SUN已经做了大量优化)。更主要的是这样在开发中只是换了中形式,如果布局中有一堆需要操作的控件,写一堆@ViewInject和各种控件Id,而且还要自己声明变量,并没有提高多少效率。看看耳熟能详的ButterKnife,其实是基于编译时解析的技术。后续