Java自定义注解在Android中的实例应用

Java注解在我们项目开发 中是非常常见的。比如经常用到的几种java内置的注解:

@Override,表示当前的方法定义将覆盖超类中的方法。
@Deprecated,表示当前方法即将废弃,不推荐使用。
@SuppressWarnings,表示忽略编译器的警告信息。

对于上面几个注解想必大家都不会陌生。除此之外,我们还经常在一些第三方框架中看到一些自定义注解。比如大名鼎鼎的ButterKnife和EventBus都是基于注解实现的。网上关于注解的文章数不胜数,但是,很多章都是贴下注解的定义,然后解释下几种元注解,扔出一个自定义注解的例子就不了了之了。刚接触注解的时候,看了半天注解相关的文章也没弄懂注解到底有什么用。其实注解往往是需要结合反射来用的,离了反射,注解也就失去了灵魂。因此,本篇文章我们会先来学习一些注解相关的一些基础知识,然后结合反射来实现一个与ButterKnife一样功能的实例。当然ButterKnife的实现并非是用反射而是使用注解处理器(AnnotationProcessor)来实现的,但是,本篇文章重点是自定义注解,因此,我们就用注解结合反射来模仿ButterKnife的效果。

一、注解基础知识简介

1.注解(Annotation)的声明

同类(class)与接口(interface)一样,注解( @interface)也是一种定义类型,它是在JDK 5.0中引入的。我们可以通过@interface来声明一个注解:

public @interface MAnnotation {
}

2.注解的成员变量
注解与类一样,都存在成员变量。与类的区别是注解中没有方法。因此,来看下如何在注解中声明成员变量。

public @interface MAnnotation {
    string name();
    int age();
}

上述示例中我们在MAnnotation中声明了一个String类型的name和一个int类型的age的成员变量。除此之外,我们还可以为成员变量制定默认值:

public @interface MAnnotation {
    string name() default "Jack";
    int age() default 18;
}

如果注解的成员变量被赋予了默认值,那么使用注解时可以不为成员变量赋值,而是用直接使用默认值。
3.注解的分类
根据注解是否包含成员变量,可以把Annotation分为如下两类:
(1)标记注解 标记注解指的时没有包含成员变量的注解,例如java内置的注解@Override注解。
(2)元数据注解 元数据注解指的是包含成员变量的注解,这类注解可以接受更多的元数据。例如,ButteerKnife的@BindView注解

4.元注解
元注解可以理解为注解的注解。用来提供对给其他的注解类型类型做说明。JDK中提供了如下4个元注解:

@Target
@Retention
@Documented
@Inherited

针对以上四种注解,我们来分别解析

(1)@Target注解
指定Annotation用于修饰哪些程序元素。@Target也包含一个名为”value“的成员变量,该value成员变量类型为ElementType[ ],ElementType为枚举类型,值有如下几个:

ElementType.TYPE:能修饰类、接口或枚举类型
ElementType.FIELD:能修饰成员变量
ElementType.METHOD:能修饰方法
ElementType.PARAMETER:能修饰参数
ElementType.CONSTRUCTOR:能修饰构造器
ElementType.LOCAL_VARIABLE:能修饰局部变量
ElementType.ANNOTATION_TYPE:能修饰注解
ElementType.PACKAGE:能修饰包

举个栗子,用FIELD和METHOD来作为Target的value,那么MAnnotation 就只能用来修饰类的成员变量和方法

@Target(ElementType.FIELD,, ElementType.METHOD)
public @interface MAnnotation {
    string name() default "Jack";
    int age() default 18;
}

(2)@Retention
这个注解定义了该注解可以保留多长时间。某些注解仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
@Retention同样包含一个名为“value”的成员变量,该value成员变量是RetentionPolicy枚举类型。使用@Retention时,必须为其value指定值。value成员变量的值只能是如下3个:

RetentionPolicy.SOURCE:Annotation只保留在源代码中,编译器编译时,直接丢弃这种Annotation。
RetentionPolicy.CLASS:编译器把Annotation记录在class文件中。当运行Java程序时,JVM中不再保留该Annotation。
RetentionPolicy.RUNTIME:编译器把Annotation记录在class文件中。当运行Java程序时,JVM会保留该Annotation,程序可以通过反射获取该Annotation的信息。

@Retention举个栗子:

@Target(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MAnnotation {
    string name() default "Jack";
    int age() default 18;
}

(3)@Documented
@Documented是一个标记注解,如果定义注解MAnnotation ,使用了@Documented修饰定义,则在用javadoc命令生成API文档后,所有使用注解MAnnotation 修饰的程序元素,将会包含注解MAnnotation 的说明。举了这么久的栗子,也挺累,这个就不举了吧。。。
(4)@Inherited
@Inherited是一个标记注解,指定注解具有继承性。要注意的是它并不是说注解本身可以继承,而是说如果一个父类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了父类的注解。这次我们还是需要举个栗子的:

Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface MAnnotation {}


@MAnnotation 
public class ClassA{}


public class ClassB extends ClassA {}

注解 @MAnnotation 被 @Inherited 修饰,ClassA 被 MAnnotation 注解,ClassB 继承 ClassA,那么此时ClassB也拥有@MAnnotation 注解。

二、使用注解与反射实现ButterKnife的功能

说了这么久,关于注解的相关基础知识终于讲完了。但是,即使看到这里不知道小伙伴是否仍然会迷茫,注解到底有什么用?在文章开头我们就提到离开反射的注解是没有灵魂的,因此,正是因为反射才赋予了注解实质的用途。那么接下来,我们用注解和反射来模仿并实现ButterKnife的功能吧。
首先,来分析一下要实现的功能。

使用注解注入布局文件省去setContentView
使用注解省去findViewById
使用注解省去setOnClickListener

**1.根据上述需求,我们可以定义三个注解。**如下:

//	给Activity注入布局文件的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectLayout {
    int value() default -1;
}

//	查找控件ID的注解
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface BindView {
    int value() default -1;
}

//	给View设置监听事件的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value();
}

2.声明BindProcessor类通过反射处理以上注解

	//  处理@InjectLayout 
	private static void injectLayout(Activity activity) {
        Class activityClass = activity.getClass();
        if (activityClass.isAnnotationPresent(InjectLayout.class)) {
            InjectLayout mId = activityClass.getAnnotation(InjectLayout.class);
            int id = mId.value();
            try {
                Method method = activityClass.getMethod("setContentView", int.class);
                method.setAccessible(true);
                method.invoke(activity, id);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

	//  处理@BindView
    private static void bindView(Activity activity) {
        Class activityClass = activity.getClass();
        Field[] declaredFields = activityClass.getDeclaredFields();
        for (Field field : declaredFields) {
            if (field.isAnnotationPresent(BindView.class)) {
                BindView mId = field.getAnnotation(BindView.class);
                int id = mId.value();
                try {
                    Method method = activityClass.getMethod("findViewById", int.class);
                    method.setAccessible(true);
                    Object view = method.invoke(activity, id);
                    field.setAccessible(true);
                    field.set(activity, view);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }
	//  处理@OnClick
    private static void bindOnClick(final Activity activity) {
        Class cls = activity.getClass();
        Method[] methods = cls.getMethods();
        for (int i = 0; i < methods.length; i++) {
            final Method method = methods[i];
            if (method.isAnnotationPresent(OnClick.class)) {
                OnClick mOnclick = method.getAnnotation(OnClick.class);
                int[] ids = mOnclick.value();
                for (int j = 0; j < ids.length; j++) {
                    final View view = activity.findViewById(ids[j]);
                    if(view==null) continue;
                    view.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            try {
                                method.setAccessible(true);
                                method.invoke(activity, view);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        }
    }


//	bind方法对外开放
public static void bind(Activity activity) {
        injectLayout(activity);
        bindView(activity);
        bindOnClick(activity);
    }

以上代码并没有什么难度,只要是了解一点反射知识的相信都能看懂。那么我们只要在Activity中调用bind方法后便可以使用注解了。下面来看Activity中的代码:

@InjectLayout(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv_test)
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        BindProcessor.bind(this);
        mButton.setText("通过注解设置的Text");
    }

    @OnClick({R.id.tv_test, R.id.btn_reflect})
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.tv_test:
                Toast.makeText(this, "通过注解点解了Text", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_reflect:
                testReflect();
                break;
        }
    }
}

可以看到,我们并没有在onCreate方法中调用setContentView方法,也没有去为Button按钮findViewById,更没有为其设置监听事件,我们统统都是用上面自定义的注解实现的。那么效果如何呢?我们来看下运行及起来的效果:
Java自定义注解在Android中的实例应用_第1张图片

效果貌似还不错,实现了与ButterKnife的部分功能,甚至我们还比ButterKnife多了一个注入布局的功能。但是,我们要知道的是ButterKnife并非是直接用反射实现的,因为反射是在运行时处理的,会影响到程序的效率。但对于神一般存在的Jake,怎么会做如此没有效率的事情。关于ButterKnife是如何实现注解,我们在下篇文章中在做探讨。

源码参考

好库推荐

给大家推荐一下BannerViewPager。这是一个基于ViewPager实现的具有强大功能的无限轮播库。通过BannerViewPager可以实现腾讯视频、QQ音乐、酷狗音乐、支付宝、天猫、淘宝、优酷视频、喜马拉雅、网易云音乐、哔哩哔哩等APP的Banner样式以及指示器样式。

欢迎大家到github关注BannerViewPager!

你可能感兴趣的:(Java自定义注解在Android中的实例应用)