目录
- 【Android】注解框架(一)-- 基础知识Java 反射
- 【Android】注解框架(二)-- 基础知识(Java注解)& 运行时注解框架
- 【Android】注解框架(三)-- 编译时注解,手写ButterKnife
- 【Android】注解框架(四)-- 一行代码注入微信支付
定义
注解是 JDK5 之后的新特性,是一种特殊的注释,它为我们在代码中添加信息提供了一种形式上的方法,使我们可以在稍后某个时候非常方便的使用这些数据。
Java内置的注解
JavaSE5内置了三种注解,定义在java.lang包中:
- @Override : 表示当前方法覆盖超类中的方法。如果你所写的方法和超类中的方法不同的话,编译器会报错。主要用于检查。
- @Deprecated : 表明当前的元素已经不适用。当使用了注解为
@Deprecated
的元素时,编译器会报出警告。 - @SuppressWarnings : 关闭不当的编译器警告。
自定义注解
元注解
元注解主要用来注解定义的注解。目前主要有四种元注解。
-
@Target
表明当前注解可以使用在哪种元素上。
ElementType
有以下几种:- CONSTRUCTOR 构造器声明
- FIELD 域声明(包括enum实例)
- LOCAL_VARIABLE 局部变量声明
- METHOD 方法声明
- PACKAGE 包声明
- PARAMETER 参数声明
- TYPE 类、接口、注解类型、enum类型
-
@Retention
表示需要在什么级别保存该注解信息。
SOURCE
源码级别,注解将被编译器丢弃,只存在源码中,其功能是与编译器交互,用于代码检测,如@Override,@SuppressWarings,许多框架如Dragger就是使用这个级别的注解,这个级别的框架额外效率损耗发生在编译时。CLASS
字节码级别,注解存在源码与字节码文件中,主要用于编译时生成而外的文件,如XML,Java文件等,这个级别需要添加JVM加载时候的代理(javaagent),使用代理来动态修改字节码文件(由于Android虚拟机并不支持所以本专题不会再做介绍,在Android中可以使用aspectJ来实现类似这个级别的功能)。RUNTIME
运行时级别,注解存在源码,字节码与Java虚拟机中,主要用于运行时反射获取相关信息,许多框架如OrmLite就是使用这个级别的注解,这个级别的框架额外的效率损耗发生在程序运行时。
-
@Documented
被修饰的注解会生成到javadoc中。 -
@Inherited
可以让注解类似被继承一样,但是这并不是真的继承。通过使用@Inherited
,只可以让子类类对象使用getAnnotations()
反射获取父类被@Inherited
修饰的注解。
简单实例 -- Android运行时注解
通常情况下,在Android开发中如果不使用类似于ButterKnife和XUtils之类的IOC框架的话,那么就需要在代码中嵌入非常多的findViewById
,那么我们不通过第三方类库,而是我们手动实现类似于XUtils的控件绑定的一个IOC框架,那么我们在写DEMO的时候就直接很方便的使用自己的代码了。
-
定义注解
// 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作为默认值是一种常用的做法。
- 定义注解的时候需要
-
解析注解
我们以Activity为例
-
解析
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
方法设置好。 -
绑定控件
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关联到注解的参数上。 -
绑定事件
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
就搞定。
-
-
使用注解
// 绑定控件 @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地址