转载自:http://www.jianshu.com/p/ca7f22b4b751
前言
从 jdk5开始,Java增加了对元数据的支持,也就是Annotation,Annotation其实就是对代码的一种特殊标记,这些标记可以在编译,类加载和运行时被读取,并执行相应的处理。当然刚刚说了,Annotation只是一种标记,所以要是在代码里面不用这些标记也是能完成相应的工作的,只是有时候用注解能简化很多代码,看起来非常的简洁。
基本的Annotation
- @Override——限定重写父类方法
- @Deprecated——标示已过时
- @SuppressWarning——抑制编译器警告
- @SafeVarargs——这货与Java7里面的堆污染有关,具体想了解的,传送到这里
JDK的元Annotation
JDK除了提供上述的几种基本的Annotation外,还提供了几种Annotation,用于修饰其他的Annotation定义
-
@Retention 这个是决定你Annotation存活的时间的,它包含一个RetationPolicy的value成员变量,用于指定它所修饰的Annotation保留时间,一般有:
- Retationpolicy.CLASS:编译器将把Annotation记录在Class文件中,不过当java程序执行的时候,JVM将抛弃它。
- Retationpolicy.SOURCE : Annotation只保留在原代码中,当编译器编译的时候就会抛弃它。
- Retationpolicy.RUNTIME : 在Retationpolicy.CLASS的基础上,JVM执行的时候也不会抛弃它,所以我们一般在程序中可以通过反射来获得这个注解,然后进行处理。
-
@Target 这个注解一般用来指定被修饰的Annotation修饰哪些元素,这个注解也包含一个value变量:
- ElementType.ANNOTATION_TYPE : 指定该Annotation只能修饰Annotation。
- ElementType.CONSTRUCTOR: 指定只能修饰构造器。
- ElementType.FIELD: 指定只能成员变量。
- ElementType.LOCAL_VARIABLE: 指定只能修饰局部变量。
- ElementType.METHOD: 指定只能修饰方法。
- ElementType.PACKAGE: 指定只能修饰包定义。
- ElementType.PARAMETER: 指定只能修饰参数。
- ElementType.TYPE: 指定可以修饰类,接口,枚举定义。
- @Document 这个注解修饰的Annotation类可以被javadoc工具提取成文档
- @Inherited 被他修饰的Annotation具有继承性
自定义Annotation
上面讲了一些jdk自带的Annotation,那么我们现在就可以用这些jdk自带的Annotation来实现一些我们想要的功能。由于最近在看butterknife的源码,那么我们就一步一步地模仿butterknife的实现吧。
首先先讲一下的用法吧:
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@ViewInject(R.id.text_view)
private TextView textView;
@OnClick(R.id.text_view)
private void onClick(View view){
textView.setText("我是click后的textview");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ViewInjectUtils.inject(this);
textView.setText("我是click前的textview");
}
}
上面是这篇文章最后的实现,自从用了注解后,妈妈再也不用担心我一遍一遍地写findViewById和setOnClickListener了。
编码
首先我们要先定义我们要用的接口,哦不!是注解。注意和接口不一样哦!这里我们先实现@ContentView
的功能,再来实现@ViewInject
和 @OnClick
package com.qhung.annotation.ioc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by qhung on 2016/5/3.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
int value();
}
啊,这里的@Target和@Retention大家应该都清楚是什么意思了哈,定义注解的方式就是@interface
和接口的定义方式就少一个@哦,不要搞混了。里面有一个变量value,就是我们使用的时候@ContentView(R.layout.activity_main)
指定的R.layout.activity_main
布局文件,旨在自动注入布局文件。因为这里只有一个变量value,所以不用写成name=value的形式。
然后大家还记得我们在Activity里面调用的ViewInjectUtils.inject(this);
?哈哈!其实我们处理注解的逻辑全在这个里面,那么我们就看看这个里面又是一番什么天地:
package com.qhung.annotation.ioc.annotation;
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
*
* Created by qhung on 2016/5/3.
*/
public class ViewInjectUtils {
public static void inject(Activity activity) {
injectContentView(activity);
}
private static void injectContentView(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView != null) {
//如果这个activity上面存在这个注解的话,就取出这个注解对应的value值,其实就是前面说的布局文件。
int layoutId = contentView.value();
try {
Method setViewMethod = clazz.getMethod("setContentView", int.class);
setViewMethod.invoke(activity, layoutId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
原来ViewInjectUtils.inject(this)
里面调用了injectContentView(activity)
,在injectContentView(activity)
里面,我们拿到了Activity的Class,然后在第23行,我们拿到了这个class的ContentView注解,然后再通过反射调用setContentView方法,完成注入。其实这里是在运行的时候完成的,所以我们在定义注解的时候,设置为Retention为RUNTIME。
好了,这个功能到这里就完了。下面继续完成第二个功能:ViewInject
同样先贴上ViewInject类:
package com.qhung.annotation.ioc.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by qhung on 2016/5/3.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();
}
其实这个定义和上面ContentView的定义一样,然后我们再看看我们是怎么处理的:
public static void inject(Activity activity) {
injectContentView(activity);
injectView(activity);
}
private static void injectView(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//获得activity的所有成员变量
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//获得每个成员变量上面的ViewInject注解,没有的话,就会返回null
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
int viewId = viewInject.value();
View view = activity.findViewById(viewId);
try {
field.setAccessible(true);
field.set(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
获得所有属性,然后遍历带有ViewInject注解的属性,然后拿到ViewInject注解的View的id,然后通过activity.findViewById(viewId);
获得这个View。然后设置给field。
最后一个功能:EventInject
这个功能稍微麻烦一点,因为我们平时设置的点击时间是用setiOnClickListener();然后View.OnClickListener是一个接口,不能用反射来获得他的实例,那么怎么办呢?
其实我们这里可以巧妙地用动态代理来完成,当view被点击的时候,我们通过动态代理来调用onclick就行。
下面是代码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
int[] value();
}
public class ViewInjectUtils {
public static void inject(Activity activity) {
injectContentView(activity);
injectView(activity);
injectEvent(activity);
}
private static void injectEvent(final Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (final Method method2 : methods) {
OnClick click = method2.getAnnotation(OnClick.class);
if (click != null) {
int[] viewId = click.value();
method2.setAccessible(true);
Object listener = Proxy.newProxyInstance(View.OnClickListener.class.getClassLoader(),
new Class[]{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method2.invoke(activity, args);
}
});
try {
for (int id : viewId) {
View v = activity.findViewById(id);
Method setClickListener = v.getClass().getMethod("setOnClickListener", View.OnClickListener.class);
setClickListener.invoke(v, listener);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
listener是一个代理对象,然后我们调用setOnClickListener的时候,把这个代理对象传进去。当发生点击的时候,就会invoke方法,这时我们就可以调用带有onClick注解的method方法了。
下面看一下运行结果:
源代码已上传到github上面,需要的可以去看看。
现在我们就完成了类似butterknife的功能了,不过butterknife最新版本不是通过反射来完成的,因为反射会有性能问题,虽然现在对性能影响不大,但是作为程序员,能优化的就要尽量优化,不能只停留在能用的基础上面。