Android之注解框架原理

当我们在使用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 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/LVXIANGAN/article/details/88350717