Android 注解
元注解
元注解是由 Java 提供的基础注解,负责注解其他注解,常用的元注解有如下:
- @Retention:注解对象的生命周期
- @Target:注解对象的作用范围
- @Inherited:作用在其他注解上,表示其他注解能否被继承
@Retention
标明注解保留的生命周期,对应 RetentionPolicy 枚举类,共三种类型:
public enum RetentionPolicy {
// 只在源码中有效,编译时抛弃,不记录在 .class 文件中
SOURCE,
// 只有编译 class 文件时生效,注解将记录在 .class 文件中,但运行时无法获得
CLASS,
// 运行时生效,注解将记录在 .class 文件中并且在运行时被 JVM 保留,可以通过反射读取它们
RUNTIME
}
@Target
标明注解的作用范围,对应 ElementType 枚举类,共十种类型:
public enum ElementType {
// 类,接口,注解类型,枚举
TYPE,
// 成员变量,枚举常量
FIELD,
// 方法
METHOD,
// 参数
PARAMETER,
// 构造函数
CONSTRUCTOR,
// 局部变量
LOCAL_VARIABLE,
// 注解
ANNOTATION_TYPE,
// 包
PACKAGE,
// 类型参数
TYPE_PARAMETER,
// 类型使用声明
TYPE_USE
}
@Inherited
注解所作用的类,在继承时默认无法继承父类的注解,除非注解声明了 @Inherited ,@Inherited 只对类有效,对方法/属性无效。
运行时注解具体案例
接下来,我们用注解简单实现一下 Activity 布局、控件和事件注入。首先定义注解类。
定义布局注解类,如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
// 获取 int 值(这里是布局资源ID)
int value() default -1;
}
上述注解类 ContentView 作用在类上,在运行时生效,使用时需要传入 Int 值。
定义控件注解类,如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
// 获取 int 值(这里是控件资源ID)
int value() default -1;
}
上述注解类 InjectView 作用在属性上,在运行时生效,使用时需要传入 Int 值。
定义事件注解基类,如下:
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BaseEvent {
String listenerSetter();
Class<?> listenerType();
String callBackListener();
}
上述注解类 BaseEvent 作用在其他注解上,在运行时生效,使用时需要传入 String 、 Class<?> 、String 值。
定义事件注解类,如下:
@BaseEvent(listenerSetter = "setOnClickListener", listenerType = View.OnClickListener.class, callBackListener = "onClick")
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
// 获取 int 数组(这里是控件资源ID)
int[] value();
}
上述注解类 OnClick 作用在方法上,在运行时生效,使用时需要传入 Int 数组。
定义注解管理类,如下:
public class InjectManager {
public static void inject(Activity activity) {
// 布局注入
injectLayout(activity);
// 控件的注入
injectViews(activity);
// 事件的注入
injectEvents(activity);
}
}
上述 InjectManager 类定义了一个静态的 inject 方法用于注解绑定,在 Activity 上调用该方法后,上述所定义的注解将会生效,具体的注入逻辑先跳过,接下来先看一下使用。
注解在 Activity 的使用
首先,在 Activity 的 onCreate()方法中调用 InjectManager.inject(this)。接着,在对应的位置上加上注解即可,如下:
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@InjectView(R.id.tv_des)
private TextView mDes;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 进行布局、控件、事件的注入
InjectManager.inject(this);
}
@Override
protected void onResume() {
super.onResume();
// Toast.makeText(this, mDes.getText().toString(), Toast.LENGTH_SHORT).show();
}
@OnClick({R.id.btn_ioc, R.id.tv_des})
public void onCLick(View view) {
switch (view.getId()) {
case R.id.tv_des:
Toast.makeText(this, "tv_des", Toast.LENGTH_SHORT).show();
break;
case R.id.btn_ioc:
Toast.makeText(this, "btn_ioc", Toast.LENGTH_SHORT).show();
break;
}
}
}
注解的注入逻辑
布局注入
private static void injectLayout(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 先判断注解类是否存在
if (clazz.isAnnotationPresent(ContentView.class)) {
// 获取指定的注解类
ContentView contentView = clazz.getAnnotation(ContentView.class);
// 获取注解中传入的值
int layoutId = contentView.value();
// 方式一
activity.setContentView(layoutId);
// 方式二
try {
// 反射获取 setContentView 方法,这里不能用 getDeclaredMethod,因为 setContentView 方法是父类中的公共方法
Method method = clazz.getMethod("setContentView", int.class);
// 反射调用 setContentView 方法
method.invoke(activity, layoutId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
控件注入
private static void injectViews(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 拿到本类的所有属性,无论 public 、protected 、private
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 拿到属性上指定的注解
InjectView injectView = field.getAnnotation(InjectView.class);
// 注解不存在,继承遍历
if (injectView == null) {
continue;
}
try {
// 反射获取 findViewById 方法,这里不能用 getDeclaredMethod,因为 findViewById 方法是父类中的公共方法
Method method = clazz.getMethod("findViewById", int.class);
// 允许修改反射属性,即改变权限(因为属性可能是 private 的)
field.setAccessible(true);
// 给属性赋值
field.set(activity, method.invoke(activity, injectView.value()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
事件注入
private static void injectEvents(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 获取本类的所有方法
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
// 获取每个方法的所有注解
Annotation[] annotations = method.getAnnotations();
// 遍历注解
for (Annotation annotation : annotations) {
Class<? extends Annotation> type = annotation.annotationType();
if (type != null) {
// 这里不能用 getDeclaredAnnotation ,因为要获取父注解 BaseEvent
BaseEvent baseEvent = type.getAnnotation(BaseEvent.class);
if (baseEvent != null) {
String listenerSetter = baseEvent.listenerSetter();
Class<?> listenerType = baseEvent.listenerType();
String callBackListener = baseEvent.callBackListener();
try {
// 找到注解对应的 value 方法
Method valueMethod = type.getDeclaredMethod("value");
// 执行 注解的 value 方法,获取注解的值
int[] viewIds = (int[]) valueMethod.invoke(annotation);
ListenerInvocationHandler handler = new ListenerInvocationHandler(activity);
// 拦截 callBackListener 方法,调用开发者自定义的 method 方法
handler.addMethod(callBackListener,method);
// 动态代理对应接口类
Object listener = Proxy.newProxyInstance(activity.getClassLoader(), new Class[]{listenerType}, handler);
for (int viewId : viewIds) {
// 获取当前的控件
View view = activity.findViewById(viewId);
// 获取指定的方法,比如(setXXXListener)
Method setter = view.getClass().getMethod(listenerSetter, listenerType);
// 执行 View 的相应方法
setter.invoke(view, listener);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
public class ListenerInvocationHandler implements InvocationHandler {
private final static long QUICK_EVENT_TIME_SPAN = 300;
private long lastClickTime;
private Object target ; // 需要拦截的对象,比如 Activity / Fragment
private HashMap<String,Method> mMap ;
public ListenerInvocationHandler(Object target) {
this.target = target;
mMap = new HashMap<>();
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (target != null) {
// 获取需要拦截的方法名
String methodName = method.getName();
// 用传进来的方法来替换该方法
method = mMap.get(methodName);
long timeSpan = System.currentTimeMillis() - lastClickTime;
if (timeSpan < QUICK_EVENT_TIME_SPAN) {
Log.d("test", "阻尼事件");
return null;
}
lastClickTime = System.currentTimeMillis();
// 如果有开发者自定义的方法
if (method != null) {
// 判断参数的长度
if (method.getGenericParameterTypes().length == 0) {
return method.invoke(target);
}
// 调用 Activity / Fragment 里的自定义方法,方法参数一样(偷天换日概念)
return method.invoke(target, args);
}
}
return null;
}
/**
* 添加拦截方法
*/
public void addMethod(String methodName, Method method) {
mMap.put(methodName, method);
}
}
事件注入比较复杂一些,这里简单介绍一下处理步骤:
- 获取本类的所有方法并获取每个方法的所有注解。
- 获取我们自定义的 BaseEvent 注解和传入的所有参数。
- 根据参数,利用动态代理技术,构建出相应的实现类。
- 利用反射技术,获取控件ID,并进一步获取控件,最后调用控件的注解控件方法。
- 在动态代理的 InvocationHandler 类中替换之前的方法,通过反射调用用户自定义的方法。