当我们在使用Butterknife和Retrofit 的时候,都会遇到注解这个概念。文中我们会通过写一个简化findViewById(int)过程的注解来学习。
首先学习写注解框架前,先简单介绍下我们需要用到的需要反射和注解的知识。
反射 (Reflection)
定义:反射机制是指在运行状态中
对于任意一个类,都能知道这个类的所有属性和方法;
对于任何一个对象,都能够调用它的任何一个方法和属性;
这样动态获取新的以及动态调用对象方法的功能就叫做反射。
一般通过反射获取一个类的信息主要有以下几个方法:
获取Class
//以String类为例
//方法一:
Class c = Class.forName("java.lang.String"); //这里一定是用完整的包名
//方法二:
Class c1=String.class;
//方法三:
String str = new String();
Class c2=str.getClass();
这里获取的c、c1、c2都是相等的。第一种写法我们会常见点。
获取类的属性(成员变量)
Field[] fields = c.getDeclaredFields();
这里返回的是一个数组 ,包含所有的属性。获取到的每一个属性Filed,包含一系列的方法可以获取及修改他的内容。
获取类的方法
// 获取所有的方法
Method[] mt = c.getDeclaredMethods();
等会要说的例子中会用到以上三个方法。
注解(Annotation)
一、元注解:
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
@Target:
作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
取值(ElementType)有:
ElementType.CONSTRUCTOR 能修饰构造器
ElementType.LOCAL_VARIABLE 能修饰局部变量
ElementType.ANNOTATION_TYPE 能修饰注解
ElementType.PACKAGE 能修饰包
ElementType.PARAMETER 能修饰参数
ElementType.METHOD 能修饰方法
ElementType.FIELD 能修饰成员变量
ElementType.TYPE 能修饰类、接口或枚举类型
@Retention:
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值有以下三种:
RetentionPolicy.SOURCE 只在源代码中保留,一般都是用来增加代码的理解性或者帮助代码检查之类的,比如我们的Override;
RetentionPolicy.CLASS 默认的选择,能把注解保留到编译后的字节码class文件中,仅仅到字节码文件中,运行时是无法得到的;
RetentionPolicy.RUNTIME 注解不仅 能保留到class字节码文件中,还能在运行通过反射获取到,这也是我们最常用的。
@Documented
@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。Documented是一个标记注解,没有成员。
@Inherited
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
这些类型和它们所支持的类在java.lang.annotation包中可以找到。
二、自定义注解:
使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过default来声明参数的默认值。
定义注解格式:
public @interface 注解名 {定义体}
注解参数的可支持数据类型:
1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
2.String类型
3.Class类型
4.enum类型
5.Annotation类型
6.以上所有类型的数组
在对反射和注解有了大致的了解后通过一个实例来加深理解:
实例
首先编写一个普通布局文件activity_main.xml 包含一个button:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="测试"/>
</LinearLayout>
如果在没有使用注解框架前我们的使用会是下面这样:
public class MainActivity extends Activity implements OnClickListener{
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 找到按钮
bt = (Button) findViewById(R.id.btn);
// 设置点击事件
bt.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Toast.makeText(this, "测试", Toast.LENGTH_LONG).show();
}
}
在使用了注解的写法后变成了这样:
public class MainActivity extends Activity {
@BindView(value = R.id.btn, click = "clickview")
private Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Butterfly.bind(this);
}
public void clickview() {
Log.d("MainActivity : ", "click");
}
}
可以看到在使用注解框架后代码变的简洁了,主要是省去了findViewById(int)和setOnClickListener(this)的这些操作,是不是感觉有点Butterknife的样子了,别急下面我们开始编写:
首先我们的思路要明确:框架是帮我们完成了findViewById(int)和setOnClickListener(this)的过程
如上代码 @BindView(value = R.id.btn, click = "clickview") 中的R.id.btn就是value的值是我们需要传递的 方法名clickview可以通过反射获取方法得到。
当需要的信息传递过去后,我们需要读取框架的信息然后让框架生效,这里是通过 Butterfly.bind(this); 来实现的。
代码时刻:
首先我们创建一个注解
/**
* author: zhuzhou on 2017/3/22
* email: [email protected]
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
String click() default "";
}
@Target(ElementType.FIELD)表示我们声明的注解是作用到字段上的,就是这里的Button
@Retention(RetentionPolicy.RUNTIME)这个表示我们声明的注解是运行的时候有效的
这里定义了两个属性:
value int类型 表示控件的id
click String类型 点击事件的方法名称
好了,定义完上面这些你已经可以去MainActivity里面字段上面定义使用了,但是不会生效。
为了让注解生效,接下来我们编写读取注解中方法的核心部分,新定义一个类 Butterfly.java:
/**
* author: zhuzhou on 2017/3/22
* email: [email protected]
*/
public class Butterfly {
public static void bind(Activity act) {
Class c = act.getClass();
// 获取这个activity中的所有字段
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
// 循环拿到每一个字段
if (field.isAnnotationPresent(BindView.class)) { // 如果这个字段有注入的注解
// 获取注解对象
BindView b = field.getAnnotation(BindView.class);
int value = b.value();
field.setAccessible(true); // 即使私有的也可以设置数据
Object view = null;
try {
view = act.findViewById(value);
// 设置字段的属性
field.set(act, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
Log.i("TAG", "注入属性失败:" + field.getClass().getName() + ":" + field.getName());
}
try {
if (view instanceof View) {
View v = (View) view;
String methodName = b.click(); // 获取点击事件的触发的方法名称
EventListener eventListener = null;
if (!TextUtils.isEmpty(methodName)) {
eventListener = new EventListener(act);
v.setOnClickListener(eventListener);
eventListener.setClickMethodName(methodName);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
从上面我们可以看到,我们首先通过使用反射act.getClass()获取到了类的实例,然后 通过c.getDeclaredFields() 获取到了类里面的所有字段,接着for循环拿到每一个属性筛选出有 BindView.class 注解的字段, 拿到id的信息(也就是value的值)再调用findViewById(int)找到控件,利用反射赋值。
最后设置点击事件这里是定义了一个EventListener 的类下面会给出;这里主要是创建EventListener 对象,然后保存了点击事件的方法名称,最后通过反射获取方法进行处理;这些都在EventListener 里面完成;
接下来定义EventListener.java 类:
public class EventListener implements View.OnClickListener {
private String TAG = "EventListener";
private Object receiver = null;
private String clickMethodName = "";
public EventListener(Object receiver) {
this.receiver = receiver;
}
public void setClickMethodName(String clickMethodName) {
this.clickMethodName = clickMethodName;
}
@Override
public void onClick(View v) {
Method method = null;
try {
method = receiver.getClass().getMethod(clickMethodName);
if (method != null) {
method.invoke(receiver);
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "未找到:" + clickMethodName + "方法");
}
try {
if (method == null) {
method = receiver.getClass().getMethod(clickMethodName, View.class);
if (method != null) {
method.invoke(receiver, v);
}
}
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "未找到带view类型参数的:" + clickMethodName + "方法");
}
}
}
可以看到EventListener 里面通过传进来的方法名然后通过反射receiver.getClass().getMethod() 获取到了方法。EventListener 已经实现了View.OnClickListener接口;到这里view的点击事件就实现完了。
最后通过上面带注解的MainActivity.java即可愉快的使用注解了。
好了,至此一个简单的注解框架就完成了,当然这里只是给出了一个view的点击事件,其他的事件也可以类似实现。因为所有代码上面都已经给出了,这里就没有提供demo。有需要的可以留言。
---------------------
作者:Mackkill
来源:CSDN
原文:https://blog.csdn.net/mackkill/article/details/65448625
版权声明:本文为博主原创文章,转载请附上博文链接!