美文网首页
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