前言
之前对注解着一块的知识一直很少使用,只知道基本概念,需要用反射操作,恰好最近的项目中有使用ButterKnife这种注解框架,感觉好用很多,当然,我这里写的跟ButterKnife不太一样,ButterKnife用的是编译时注解,我这里用的是运行时注解,不过学东西,总要一步一步来嘛,先可以应用上,后续再考虑性能问题。
注解
讲到注解,就不得不谈谈注解了,注解是什么,例如一个重写的方法,上面会声明@Override,那么我们就知道这是一个重写的方法了。
Java中有四个元注解,分别是:
@Target,@Retention,@Documented,@Inherited ,是Java 的api提供的,是专门用来定义注解的注解,其作用分别如下:
@Target 表示该注解用于什么地方,可能的值在枚举类 ElemenetType 中,包括:
ElemenetType.CONSTRUCTOR—————————-构造器声明
ElemenetType.FIELD ————————————–域声明(包括 enum 实例)
ElemenetType.LOCAL_VARIABLE————————- 局部变量声明
ElemenetType.METHOD ———————————-方法声明
ElemenetType.PACKAGE ——————————— 包声明
ElemenetType.PARAMETER ——————————参数声明
ElemenetType.TYPE————————————— 类,接口(包括注解类型)或enum声明
@Retention 表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy 中,包括:
RetentionPolicy.SOURCE ———————————注解将被编译器丢弃
RetentionPolicy.CLASS ———————————–注解在class文件中可用,但会被VM丢弃
RetentionPolicy.RUNTIME VM——-将在运行期也保留注释,因此可以通过反射机制读取注解的信息。
@Documented 将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@see,@param 等。
@Inherited 允许子类继承父类中的注解。
常用的注解一般会声明两个注解:
@Target和@Retention,来确定我们定义的注解的作用域和注解的作用时间,像之前的@Override我们点进注解中可以看到:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
作用域是针对方法,而@Retention这个注解是RetentionPolicy.SOURCE,也就是只是做个区分,最终会被编译器丢弃掉。
我们在点击进入ButterKnifer的任意一个注解库:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package butterknife;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD})
public @interface InjectViews {
int[] value();
}
可以看到,这里的@Retention注解内容都是RetentionPolicy.CLASS,也就是会在生成的class文件中,android这边应该就是会编译成.dex文件,但最终还是会被虚拟机丢弃,这边在往深的我就不懂了,所以这里也就不搬门弄斧了。
我这里用到的@Retention注解内容是RetentionPolicy.RUNTIME ,也就是在运行时会保留。
对View的绑定
用到注解的话就要用到反射,所以大家还是需要对反射常用的类和方法有一定的了解。
我们平时写界面的时候,总是不断的,每个界面都要写上setContentView,findView等这些不断重复的代码,这些代码对我们的水平提升不大,但是我们还要反复的去敲,很浪费开发时间,所以,我们平时会用一些注解库等来解决这些问题,当然在kotlin里这种操作很简单(题外话),但是我们对这些原理还是要有一些了解,所以,为了避免这种重复的劳动力操作,我们还是要简化这种操作的。
我们参考ButterKnife的写法,在activity中也以这样的一种方式来写:
setContentView的注解
package com.example.panhao.retrofittest;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Switch;
import static android.content.ContentValues.TAG;
/**
* Created by panhao on 17-6-17.
*/
@ContentView(R.layout.activity_login)
public class LoginActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtil.inject(this);
}
}
可以看到,在activity类外声明了一个注解,要想这个注解起作用,首先我们得声明一个注解:
package com.example.panhao.retrofittest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by panhao on 17-6-17.
*/
@Target(ElementType.TYPE)//在类的作用范围内
@Retention(RetentionPolicy.RUNTIME)//运行时注解
public @interface ContentView {
int value();
}
有了注解,当然要有注解的解释方法,我们在activity的onCreate方法里调用的InjectUtil的inject方法,这个方法里就对注解做了解析,代码如下:
public static void inject(Activity context) {
injectLayout(context);
}
/**
* 注入界面布局
*
* @param context
*/
public static void injectLayout(Activity context) {
Class<? extends Activity> clazz = context.getClass();
//拿到注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView == null) {
Log.i(TAG, "injectLayout: ======");
return;
}
int layoutId = contentView.value();
context.setContentView(layoutId);
}
我们传入一个activity,通过Class的getAnnocation方法获得activity类上的注解,判断注解是否是我们需要的注解,是的话就找到注解传入的layoutId进行setContentView操作,我这里没有安装模拟器所以没有截图,真机上是可以绑定到view的大家可以试一下。
findView的注解
package com.example.panhao.retrofittest;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Switch;
import static android.content.ContentValues.TAG;
/**
* Created by panhao on 17-6-17.
*/
@ContentView(R.layout.activity_login)
public class LoginActivity extends Activity {
@InjectView(R.id.btn)
Button btn;
@InjectView(R.id.switches)
Switch switches;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtil.inject(this);
btn.setText("哈哈哈哈哈哈哈哈");
switches.setChecked(true);
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="測試注解" />
<Switch
android:id="@+id/switches"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
在布局文件中声明一个button和一个switch,在activity中通过注解进行绑定,在onCreate时改变内容看是否完成了view的绑定,如果没完成肯定会报空指针。
package com.example.panhao.retrofittest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by panhao on 17-6-17.
*/
@Target(ElementType.FIELD)//字段中声明有效
@Retention(RetentionPolicy.RUNTIME)//编译时注解
public @interface InjectView {
int value();
}
/**
* 注入界面内容的布局
*
* @param context
*/
public static void injectView(Activity context) {
Class<? extends Activity> clazz = context.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
InjectView injectView = field.getAnnotation(InjectView.class);//获取所有标明该注解的对象
//injectView注解不为空时
if (injectView != null) {
int viewId = injectView.value();
View view = context.findViewById(viewId);
try {
field.set(context, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
这里是对activity里的字段进行注入,所以要获得activity里所有的字段进行遍历判断是否含有我们的注解,含有我们的注解的话就通过findView操作绑定view到activity中。
对事件监听的注入
android 中有很多种事件监听,所以这里如果只是对单个事件的监听绑定定还好处理,但是我们要针对不同事件的监听进行绑定,参考ButterKnife源码,要用到注解的注解,来标明一些信息:
package com.example.panhao.retrofittest;
/**
* Created by panhao on 17-6-17.
*/
public @interface EventControl {
/**
* 设置事件监听的方法
* @return
*/
String listenerMethod();
/**
* 设置事件回调的方法
* @return
*/
String callBack();
/**
* 获取监听类型
* @return
*/
Class<?> getClazzType();
}
package com.example.panhao.retrofittest;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Switch;
import static android.content.ContentValues.TAG;
/**
* Created by panhao on 17-6-17.
*/
@ContentView(R.layout.activity_login)
public class LoginActivity extends Activity {
@InjectView(R.id.btn)
Button btn;
@InjectView(R.id.switches)
Switch switches;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtil.inject(this);
btn.setText("哈哈哈哈哈哈哈哈");
switches.setChecked(true);
}
@OnClick(R.id.btn)
public void handler(View view) {
Log.i(TAG, "onClick: 我被点击了哦~~~");
}
}
package com.example.panhao.retrofittest;
import android.view.View;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by panhao on 17-6-17.
*/
@Target(ElementType.METHOD)//访问权限
@Retention(RetentionPolicy.RUNTIME)//运行时执行
@EventControl(
listenerMethod = "setOnClickListener",
callBack = "onClick",
getClazzType = View.OnClickListener.class)
public @interface OnClick {
int[] value();//对一组数据添加监听
}
在OnClick注解上声明了刚刚定义的对注解的注解,标明了它的监听方法,回调方法和监听类类型,同样我们也可以声明OnItemClick,OnLongClick等其它注解,这也是所以我们要加上注解的注解的原因。
private static void injectEvent(final Activity context) {
Method[] methods = context.getClass().getDeclaredMethods();
for (Method method : methods) {
//1.获取方法上的所有注解
Annotation[] annotations = method.getAnnotations();
//2判断注解是否是我们需要的注解:也就是注解上是否包含event
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
//获取注解上是否有我们约定好的注解
EventControl eventControl = annotationType.getAnnotation(EventControl.class);
if (eventControl == null) {
//说明不是我们想要的注解,跳出此次循环
continue;
}
//获取注解上声明好的内容
String listener = eventControl.listenerMethod();
String callback = eventControl.callBack();//要拦截的方法
Class<?> listenerType = eventControl.getClazzType();
final Map<String, Method> methodMap = new HashMap<>();
methodMap.put(callback, method);
try {
Method valueMethod = annotationType.getDeclaredMethod("value");
//第一个参数是接受者,也就是这里的注解对象,也就是调用annotation的value方法,如果后面有参数
//则在后面声明不定参数
int[] viewIds = (int[]) valueMethod.invoke(annotation);
//遍历id找到view
for (int viewId : viewIds) {
final View view = context.findViewById(viewId);
if (view != null) {
//给view添加监听
if (view == null) {
continue;
}
//获取设置监听的方法,但是不知道对什么设置监听:onClick,onItemClick...
Method setListenerMethod = view.getClass().getMethod(listener, listenerType);
//这里只有listener的类型
//根据对应类型生成对应的代理类对象
Object proxyInstance = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (methodMap != null) {
//获取对应方法在map中是否存在,这里的是我们的handle方法
Method m = methodMap.get(method.getName());
if (m != null) {
//调用存入map中的方法,也就是我们在activity中声明监听注解的方法
return m.invoke(context, args);
}
}
return method.invoke(proxy, args);
}
});
//给view设置监听
setListenerMethod.invoke(view, proxyInstance);
}
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
这里的逻辑处理就要麻烦一点了,但只要理清思路还是很容易理解的
1. 获取activity的所有方法
2. 获取每个方法上的所有注解
3. 遍历注解并判断注解上是否含有我们声明的注解的注解
4. 拿到注解的注解的基本信息
5. 拿到annotation上的value包含的所有id
6. 通过id获得view
7. 通过动态代理来生成需要的指定类型的代理对象,并拦截在注解的注解上声明好的方法,反射执行我们activity包含该注解的方法。
8. 调用setXXListener方法来给所有声明id的view添加该监听。
最后测试下对事件的绑定是否有效,效果如控制台打印所示
结语
结语这里本来像发一些感慨的,最近确实很忙,是我的事,不是我的事都往我身上推,我真的满烦的,不过想想也没什么,能力越大,责任越大,就算不能在当中学到什么,也能让我意识到我不是一无是处的存在,有的人说我太老实了,容易被别人占便宜,怎么说呢,我觉的这并不是什么老不老实的,只是做人的基本原则吧,答应别人的就好好做,可是有时候确实,一个人的力量真的很有限,还是希望大家不说多做什么,至少在团队中尽到自己的义务吧,如果都想着破罐子破摔,这个团队是很失败的,没有老实人该替你干活,你就算占了一时便宜,也并不能怎么样,你反而丧失了学习最好的时间,其实这反而是得不偿失的。好了,烦躁的胡乱bb了一堆,还是别胡思乱想了,洗洗睡了吧。