【Android】注解框架(二)-- 基础知识(Java注解)& 运行时注解框架

目录

  1. 【Android】注解框架(一)-- 基础知识Java 反射
  2. 【Android】注解框架(二)-- 基础知识(Java注解)& 运行时注解框架
  3. 【Android】注解框架(三)-- 编译时注解,手写ButterKnife
  4. 【Android】注解框架(四)-- 一行代码注入微信支付

定义

注解是 JDK5 之后的新特性,是一种特殊的注释,它为我们在代码中添加信息提供了一种形式上的方法,使我们可以在稍后某个时候非常方便的使用这些数据。

Java内置的注解

JavaSE5内置了三种注解,定义在java.lang包中:

  1. @Override : 表示当前方法覆盖超类中的方法。如果你所写的方法和超类中的方法不同的话,编译器会报错。主要用于检查。
  2. @Deprecated : 表明当前的元素已经不适用。当使用了注解为@Deprecated的元素时,编译器会报出警告。
  3. @SuppressWarnings : 关闭不当的编译器警告。

自定义注解

元注解

元注解主要用来注解定义的注解。目前主要有四种元注解。

  1. @Target

    表明当前注解可以使用在哪种元素上。
    ElementType有以下几种:

    • CONSTRUCTOR 构造器声明
    • FIELD 域声明(包括enum实例)
    • LOCAL_VARIABLE 局部变量声明
    • METHOD 方法声明
    • PACKAGE 包声明
    • PARAMETER 参数声明
    • TYPE 类、接口、注解类型、enum类型
  2. @Retention

    表示需要在什么级别保存该注解信息。

    • SOURCE 源码级别,注解将被编译器丢弃,只存在源码中,其功能是与编译器交互,用于代码检测,如@Override,@SuppressWarings,许多框架如Dragger就是使用这个级别的注解,这个级别的框架额外效率损耗发生在编译时。
    • CLASS 字节码级别,注解存在源码与字节码文件中,主要用于编译时生成而外的文件,如XML,Java文件等,这个级别需要添加JVM加载时候的代理(javaagent),使用代理来动态修改字节码文件(由于Android虚拟机并不支持所以本专题不会再做介绍,在Android中可以使用aspectJ来实现类似这个级别的功能)。
    • RUNTIME 运行时级别,注解存在源码,字节码与Java虚拟机中,主要用于运行时反射获取相关信息,许多框架如OrmLite就是使用这个级别的注解,这个级别的框架额外的效率损耗发生在程序运行时。
  3. @Documented 被修饰的注解会生成到javadoc中。

  4. @Inherited 可以让注解类似被继承一样,但是这并不是真的继承。通过使用@Inherited,只可以让子类类对象使用getAnnotations()反射获取父类被@Inherited修饰的注解。

简单实例 -- Android运行时注解

通常情况下,在Android开发中如果不使用类似于ButterKnife和XUtils之类的IOC框架的话,那么就需要在代码中嵌入非常多的findViewById,那么我们不通过第三方类库,而是我们手动实现类似于XUtils的控件绑定的一个IOC框架,那么我们在写DEMO的时候就直接很方便的使用自己的代码了。

  1. 定义注解

    // 1.绑定控件注解
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Bind {
        int value();
        int[] parentId() default 0;
    }
    // 2.检查网络注解
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface CheckNet {
    
    }
    // 3.绑定事件注解
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface OnClick {
        int[] value();
        int[] parentId() default 0;
    }
    // 4. 绑定布局
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface ContentView {
        int value();
    }
    
    • 定义注解的时候需要@interface
    • 注解参数的可支持数据类型:
      • 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
      • String类型
      • Class类型
      • enum类型
      • Annotation类型
      • 以上所有类型的数组
    • 参数的类型只能是public或者不写两种访问修饰符
    • 如果注解没有参数,那么就和2中一样,是一个空注解,用的时候直接标注
    • 如果注解只有一个参数,就和4中一样,尽量使用value来表示参数,这样在使用的时候可以直接@Bind(R.id.text_view),而不需要使用@Bind(value=R.id.text_view)
    • 如果注解的参数有默认值,可以参考3中的int[] parentId() default 0,在使用的时候如果不需要赋值可以不写这个参数
    • 注解参数必须有确切的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此, 使用空字符串或0作为默认值是一种常用的做法。
  2. 解析注解

    我们以Activity为例

    1. 解析ContentView

      @Override
      public void inject(Activity activity) {
         Class<?> clazz = activity.getClass();
         // activity设置布局
         try {
             ContentView contentView = findContentView(clazz);
             if (contentView != null) {
                 int layoutId = contentView.value();
                 if (layoutId > 0) {
                     Method setContentView = clazz.getMethod("setContentView", int.class);
                     setContentView.invoke(activity, layoutId);
                 }
             }
         } catch (Exception e) {
             e.printStackTrace();
         }
         injectObject(activity, clazz, new ViewFinder(activity));
      }
      
      private static ContentView findContentView(Class<?> clazz) {
         return clazz != null ? clazz.getAnnotation(ContentView.class) : null;
      }
      

      通过反射来获取类上的注解ContentView,如果有的话,在通过contentView.value()来获取到设置在注解上的布局Id,最后通过反射来将setContentView方法设置好。

    2. 绑定控件

      public static void injectObject(Object handler, Class<?> clazz, ViewFinder finder) {
         try {
             injectView(handler, clazz, finder);
             injectEvent(handler, clazz, finder);
         } catch (Exception e) {
             e.printStackTrace();
         }
      }
      
      private static void injectView(Object handler, Class<?> clazz, ViewFinder finder) throws Exception {
         // 获取class的所有属性
         Field[] fields = clazz.getDeclaredFields();
      
         // 遍历并找到所有的Bind注解的属性
         for (Field field : fields) {
             Bind viewById = field.getAnnotation(Bind.class);
             if (viewById != null) {
                 // 获取View
                 View view = finder.findViewById(viewById.value(), viewById.parentId());
                 if (view != null) {
                     // 反射注入view
                     field.setAccessible(true);
                     field.set(handler, view);
                 } else {
                     throw new Exception("Invalid @Bind for "
                             + clazz.getSimpleName() + "." + field.getName());
                 }
             }
      
         }
      }
      

      和上面一样,首先通过反射获取到所有的Field参数,通过遍历Field获取到每个属性上的注解,当获取到的注解不为空的时候,就说明当前的属性被Bind注解了,之后再获取到View并通过field的set方法将view关联到注解的参数上。

    3. 绑定事件

      private static void injectEvent(Object handler, Class<?> clazz, ViewFinder finder) throws Exception {
         // 获取class所有的方法
         Method[] methods = clazz.getDeclaredMethods();
      
         // 遍历找到onClick注解的方法
         for (Method method : methods) {
             OnClick onClick = method.getAnnotation(OnClick.class);
             boolean checkNet = method.getAnnotation(CheckNet.class) != null;
             if (onClick != null) {
                 // 获取注解中的value值
                 int[] views = onClick.value();
                 int[] parentIds = onClick.parentId();
                 int parentLen = parentIds == null ? 0 : parentIds.length;
                 for (int i = 0; i < views.length; i++) {
                     // findViewById找到View
                     int viewId = views[i];
                     int parentId = parentLen > i ? parentIds[i] : 0;
                     View view = finder.findViewById(viewId, parentId);
                     if (view != null) {
                         // 设置setOnClickListener反射注入方法
                         view.setOnClickListener(new MyOnClickListener(method, handler, checkNet));
                     } else {
                         throw new Exception("Invalid @OnClick for "
                                 + clazz.getSimpleName() + "." + method.getName());
                     }
                 }
             }
         }
      }
      
      private static class MyOnClickListener implements View.OnClickListener {
         private Method method;
         private Object handler;
         private boolean checkNet;
      
         public MyOnClickListener(Method method, Object handler, boolean checkNet) {
             this.method = method;
             this.handler = handler;
             this.checkNet = checkNet;
         }
      
         @Override
         public void onClick(View v) {
             if (checkNet && !NetStateUtil.isNetworkConnected(v.getContext())) {
                 Toast.makeText(v.getContext(), "网络错误!", Toast.LENGTH_SHORT).show();
                 return;
             }
      
             // 注入方法
             try {
                 method.setAccessible(true);
                 method.invoke(handler, v);
             } catch (Exception e) {
                 e.printStackTrace();
             }
         }
      }
      

      绑定事件和绑定控件一样,都是通过遍历来获取注解,再通过注解的参数来设置View的setOnClickListener
      这里我们又通过另外一个注解CheckNet来判断点击控件时候的参数,这样就不需要每次在交互需要判断网络的情况下写判断网络的代码了,直接一条@CheckNet就搞定。

  3. 使用注解

    // 绑定控件
    @Bind(R.id.viewpager)
    ViewPager viewpager;
    
    // 绑定事件并检查网络
    @OnClick(R.id.dialog)
    @CheckNet
    void showDialog(TextView tv) {
        Intent intent = new Intent(getActivity(), DialogViewActivity.class);
        startActivity(intent);
    }
    

后记

通过自定义注解的学习,当我们需要使用的时候,可以通过自己来写并扩展我们所需要的功能,这样在使用的时候会非常方便,能够在业务变动的时候能够修正和扩展。

运行时IOC注解框架:github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容