美文网首页
Android 注解Annotation的使用

Android 注解Annotation的使用

作者: 雷涛赛文 | 来源:发表于2020-12-23 16:46 被阅读0次

      Java 注解(Annotation)又称为 Java 标注,是 JDK5.0 引入的一种注释机制。
      Java 语言中的类、方法、变量、参数和包等都可以被标注,Java 标注可以通过反射获取标注内容。可以在编译、类加载、运行时被读取,并执行相应的处理。

一.Annotation的作用

      注解将一些本来重复性的工作,变成程序自动完成,简化和自动化该过程。比如用于生成Java doc,比如编译时进行格式检查,比如自动生成代码等,用于提升软件的质量和提高软件的生产效率。在反射的 Class, Method, Field 等函数中,有许多于 Annotation 相关的接口。可以在反射中解析并使用 Annotation。

二.Annotation的解释

      元注解是java API提供的,是用于修饰注解的注解,通常用在注解的定义上,Java提供了四种元注解,专门负责新注解的创建工作,四种注解如下:
      @Target:注解的作用目标;用于指明被修饰的注解最终可以作用的目标是谁,也就是指明注解是用来修饰方法的?修饰类的?还是用来修饰字段属性的?
      ElementType.CONSTRUCTOR:用于描述构造器
      ElementType.FIELD:用于描述属性字段
      ElementType.LOCAL_VARIABLE:用于描述局部变量
      ElementType.METHOD:用于描述方法
      ElementType.PACKAGE:用于描述包
      ElementType.PARAMETER:用于描述参数
      ElementType.TYPE:用于描述类、接口(包括注解类型) 或enum声明
      @Retention:注解的生命周期; 表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy 中,包括:
      RetentionPoicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;用于做一些检查性的操作,比如 @Override 和 @SuppressWarnings
      RetentionPoicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;用于在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife)
      RetentionPoicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;用于在运行时去动态获取注解信息。
      @Documented:将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同;
      @Inherited:是否允许子类继承该注解。

三.Annotation的使用

      注解定义格式如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface xxxx {
    int value();
}

      使用注解需要用到Java反射机制,只有通过类才能去获取到其内的方法,变量等。
      下面通过一个实例来进行实战,通过注解来减少View中的findViewById及setOnClickListenter等代码逻辑。

a.View类

public class InjectFragment extends BaseFragment {

    @BindView(R.id.img1)
    private ImageView mImg1;
    @BindView(R.id.img2)
    private ImageView mImg2;

    @Override
    public int getLayoutId() {
        return R.layout.inject_layout;
    }

    @Override
    public void initData(View view) {
        InjectManager.inject(this, view);
    }

    @OnClick({R.id.btn1, R.id.btn2})
    public void onViewClick(View view) {
        switch (view.getId()) {
            case R.id.btn1:
                InputStream is = mContext.getResources().openRawResource(R.drawable.ic_chuancai);
                Bitmap bmp = BitmapFactory.decodeStream(is);
                mImg1.setImageBitmap(bmp);
                break;
            case R.id.btn2:
                InputStream is1 = mContext.getResources().openRawResource(R.drawable.ic_lucai);
                Bitmap bmp1 = BitmapFactory.decodeStream(is1);
                mImg2.setImageBitmap(bmp1);
                break;
        }
    }
}

      通过以上代码可以看到:代码中没有了如findViewById及setOnClickListener,而是多了@BindView,@OnClick注解及InjectManager.inject(this, view),当进行点击事件后,最终会调用到onViewClick方法。

b.自定义注解类

      定义用于field的注解BindView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {

    int value();
}

      定义用于method的注解OnClick

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {

    int[] value();
}

c.注解的解析

public class InjectManager {

    public static void inject(Object obj1, Object obj2) {
        //控件注入
        injectField(obj1, obj2);
        //方法注入
        injectMethod(obj1, obj2);
    }

    private static void injectField(Object obj1, Object obj2) {
        Class clazz1 = obj1.getClass();
        Class clazz2 = obj2.getClass();
        Field[] declaredFields = clazz1.getDeclaredFields();
        //遍历class中所有的Field
        for (int i = 0; i < declaredFields.length; i++) {
            Field field = declaredFields[i];
            //设置为可访问,暴力反射,就算是私有的也能访问到
            field.setAccessible(true);
            //获取Field上的注解对象
            BindView annotation = field.getAnnotation(BindView.class);
            //不是所有Filed上都有想要的注解,需要对annotation进行null判断
            if (annotation == null) {
                continue;
            }
            //获取注解中的值
            int id = annotation.value();
            //获取控件,通过反射获取
            try {
                Method findViewById = clazz2.getMethod("findViewById", int.class);
                findViewById.setAccessible(true);
                View view = (View) findViewById.invoke(obj2, id);
                //将view赋值给field
                field.set(obj1, view);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    private static void injectMethod(final Object obj1, Object obj2) {
        Class clazz1 = obj1.getClass();
        Class clazz2 = obj2.getClass();
        //获取所有的方法(私有方法也可以获取到)
        Method[] declaredMethods = clazz1.getDeclaredMethods();
        for (int i = 0; i < declaredMethods.length; i++) {
            final Method method = declaredMethods[i];
            //获取方法上面的注解
            OnClick annotation = method.getAnnotation(OnClick.class);
            if (annotation == null) {
                continue;
            }
            //传入需要响应的实例及方法
            OnClickListenerInvoke clickListenerInvoke = new OnClickListenerInvoke(obj1, method, "onClick");
            Object clickListener = Proxy.newProxyInstance(View.OnClickListener.class.getClassLoader(),
                    new Class[]{View.OnClickListener.class}, clickListenerInvoke);
            int[] value = annotation.value();
            for (int j = 0; j < value.length; j++) {
                int id = value[j];
                //获取控件,通过反射获取
                try {
                    Method findViewById = clazz2.getMethod("findViewById", int.class);
                    findViewById.setAccessible(true);
                    View view = (View) findViewById.invoke(obj2, id);
                    Method setOnClickListenerMethod = view.getClass().getMethod(
                            "setOnClickListener", View.OnClickListener.class);
                    //通过动态代理实现设置setOnClickListener
                    setOnClickListenerMethod.invoke(view, clickListener);
                } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

      通过以上可以看到,通过反射对view中的控件进行赋值,通过反射及动态代理Java动态代理学习对控件进行设置点击响应。通过动态代理View.onClickListener后,当点击控件后,会最终执行到OnClickListenerInvoke的invoke()方法,参数method是onClick,但是view中没有onClick这个方法,所以不能直接使用method.invoke(),需要替换成view中的method执行invoke(),处理如下:

public class OnClickListenerInvoke implements InvocationHandler {
    private Object mObject;
    private Method mMethod;
    private String mEventName;

    public OnClickListenerInvoke(Object obj, Method method, String eventName) {
        mMethod = method;
        mObject = obj;
        mEventName = eventName;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //此处的method是onClick或者onLongClick,需要执行的是onViewClick或onViewLongClick方法
        if (method.getName().equals(mEventName)) {
            return mMethod.invoke(mObject, args);
        }
        return null;
    }
}

      通过以上流程,注释就完成了。

四.Annotation的扩展

      当点击事件不止setOnClickListener一个,再加上setOnLongClickListener,那需要如何处理呢?简单方式就是在InjectManager内再加一个方法,实现setOnLongClickListener的逻辑,这样的话,后续加一个注解就需要在InjectManager加方法;
      当一个method上有多个注解时,我们需要来判断是自定义的注解然后来进行解释,那应该如何处理呢?那就需要定义一个元注解,在自定义的注解上加上该元注解就可以区分是否是自己定义的注解。
      结合以上两点,实现如下:

a.View

   @OnClick({R.id.btn1, R.id.btn2})
    public void onViewClick(View view) {
        switch (view.getId()) {
            case R.id.btn1:
                break;
            case R.id.btn2:
                break;
        }
    }

    @OnLongClick(R.id.btn2)
    public boolean onViewOnLongClick(View view) {
        Toast.makeText(mContext, "这是长按事件", Toast.LENGTH_SHORT).show();
        return true;
    }

b.自定义注解及元注解

      元注解:
      listenerSetting:要设置的Listenter方法名字;
      listenerClass:Listenter方法需要设置的参数表示的类;
      eventName:点击后Listener的回调方法名字;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClickEvent {

    String listenerSetting();

    Class listenerClass();

    String eventName();
}

      OnClick注解修改:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ClickEvent(listenerSetting = "setOnClickListener", listenerClass = View.OnClickListener.class,
        eventName = "onClick")
public @interface OnClick {

    int[] value();
}

      OnLongClick注解修改:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ClickEvent(listenerSetting = "setOnLongClickListener", listenerClass =
        View.OnLongClickListener.class, eventName = "onLongClick")
public @interface OnLongClick {

    int[] value();
}

      通过以上可以看到,OnClick及OnLongClick上面加入了ClickEvent注解,且传入了需要的参数,从而可以区分出是setOnClickListener还是setOnLongClickListener。

c.注解的解析

//InjectManager.java
    //统一处理所有自定义的注解
    private static void injectMethod(final Object obj1, Object obj2) {
        Class clazz1 = obj1.getClass();
        Class clazz2 = obj2.getClass();
        //获取所有的方法
        Method[] declaredMethods = clazz1.getDeclaredMethods();
        for (int i = 0; i < declaredMethods.length; i++) {
            final Method method = declaredMethods[i];
            //获取到某一方法上的所有的Annotation
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                //获取到某个Annotation对应的class
                Class<? extends Annotation> annotationTypeClass = annotation.annotationType();
                //获取到Annotation上的Annotation,来区分是自定义注解还是其他注解
                ClickEvent ano = annotationTypeClass.getAnnotation(ClickEvent.class);
                if (ano == null) {
                    continue;
                }
                //获取到Annotation上的Annotation对应的值
                String listenerSetting = ano.listenerSetting();
                Class listenerClass = ano.listenerClass();
                String eventName = ano.eventName();
                //创建动态代理对象
                OnClickListenerInvoke clickListenerInvoke = new OnClickListenerInvoke(obj1, method, eventName);
                Object clickListener = Proxy.newProxyInstance(listenerClass.getClassLoader(),
                        new Class[]{listenerClass}, clickListenerInvoke);
                try {
                    //进入该逻辑的annotation是OnClick或OnLongClick,但是通过annotation访问不到value(),可以强制转换,但是会增加处理逻辑,此处不合适
                    /*String name = annotation.annotationType().getSimpleName();
                    int[] ids;
                    if (name.equals("OnClick")) {
                        OnClick oc = (OnClick) annotation;
                        ids = oc.value();
                    } else {
                        OnLongClick olc = (OnLongClick) annotation;
                        ids = olc.value();
                    }*/
                    //所有此处通过反射获取到int[]
                    Method valueMethod = annotationTypeClass.getDeclaredMethod("value");
                    int[] ids = (int[]) valueMethod.invoke(annotation);
                    for (int id : ids) {
                        Method findViewById = clazz2.getMethod("findViewById", int.class);
                        findViewById.setAccessible(true);
                        View view = (View) findViewById.invoke(obj2, id);
                        Method setOnClickListenerMethod = view.getClass().getMethod(
                                listenerSetting, listenerClass);
                        //通过动态代理实现设置ClickListener
                        setOnClickListenerMethod.invoke(view, clickListener);
                    }
                } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }

      总结一下步骤:
      ①:先通过class获取到class内的所有methods;
      ②:遍历所有methods,获取到method上的所有annotations;
      ③:遍历所有annotations,通过annotation.annotationType()来得到annotation对应的class,然后获取到class上是否含有ClickEvent注解;
      ④:如果含有InjectEvent注解,那么获取到ClickEvent注解携带的参数内容;
      ⑤:创建对应的动态代理,然后把method传入,供后续invoke()时执行;
      ⑥:通过annotation class反射获取到value()方法,继而获取到view中annotation内的参数数组ids;
      ⑦:遍历所有的ids,通过id来获取到组件,然后设置点击事件。

相关文章

网友评论

      本文标题:Android 注解Annotation的使用

      本文链接:https://www.haomeiwen.com/subject/amzynktx.html