在android开发中我们经常用到xUtils,eventBus,ButterKnife等第三方框架来帮助我们快捷的实现布局绑定,数据传递等功能,对于这类框架的使用相信大多数的开发者都已轻车熟路,但对于其中的实现机制,可能还不是很熟悉。今天我们来简单解析下这类框架的内部实现机制,并实现类似xUtils中的布局,控件,事件注入功能。
对于这类框架其实内部都是用IOC注入技术实现的: IOC是原来由程序代码主动获取的资源,转变由第三方获取并使原来的代码被动接收的方式,以达到解耦的效果,称为控制反转;
IOC技术有三种类型: 1.运行时注入 xUtils,eventBus,springMVC 2.源码时注入 android studio插件 3.编译时注入 butterknife dagger2;
少说多写,下面我们来用代码来实现一个简单的布局注入: 通常情况下我们在新建Activity时,自动会使用:
setContentView(R.layout.activity_main);
进行布局绑定。而现在我们希望做到的是使用
@ContentView(R.layout.activity_main)
这种以注解的形式进行绑定布局;
1.首先我们新建一个InjectUtils工具类:
public class InjectUtils {
public static void inject(Object context){
//布局的注入
injectLayout(context);
}
2.新建注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
int value();
}
注:@Target指的是注解的作用目标
- @Target(ElementType.TYPE)//接口、类、枚举、注解
- @Target(ElementType.FIELD) //字段、枚举的常量
- @Target(ElementType.METHOD) //方法
- @Target(ElementType.PARAMETER) //方法参数
- @Target(ElementType.CONSTRUCTOR) //构造函数
- @Target(ElementType.LOCAL_VARIABLE)//局部变量
- @Target(ElementType.ANNOTATION_TYPE)//注解
- @Target(ElementType.PACKAGE) ///包
我们的注解目标是类(指的是activity)所以此处选择的是 ElementType.TYPE
@Retention:注解的保留位置
- @Retention(RetentionPolicy.SOURCE) //注解仅存在于源码中,在class字节码文件中不包含
- @Retention(RetentionPolicy.CLASS) // 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
- @Retention(RetentionPolicy.RUNTIME) // 注解会在class字节码文件中存在,在运行时可以通过反射获取到
这里选择的是运行时注解
还需要注意的是注解的写法@interface,和接口写法一字之差,int value()是指定注解的里值的类型;
然后我们回到InjectUtils里
private static void injectLayout(Object context) {
int layoutId = 0;
//通过反射拿到需要注入的Activity;
Class<?> clazz = context.getClass();
//在clazz上面去执行setContentView
ContentView contentView = clazz.getAnnotation(ContentView.class);
//获取注解括号后面的内容;
if (contentView != null){
layoutId= contentView.value();
//反射去执行setContentView;
try {
//获取反射类里的方法;
Method method = context.getClass().getMethod("setContentView",int.class);
//调用方法
method.invoke(context,layoutId);
}catch (Exception e){
e.printStackTrace();
}
}
}
这样我们就可以在activity里进行引用了;
接下来我们在实现我控件绑定,这里的步骤和布局绑定一样:
1.新建注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
int value();
}
2.同样在InjectUtils里写上控件注入的方法:
//控件的注入;
private static void injectView(Object context) {
Class<?> clazz = context.getClass();
//通过注解获取到反射类的所有的字段(此处是Activity中所有的控件)
Field[] fields= clazz.getDeclaredFields();
for (Field field : fields) {
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null){
//获取控件的id
int valued = viewInject.value();
//反射执行findViewById方法;
try {
Method method = clazz.getMethod("findViewById",int.class);
View view = (View) method.invoke(context,valued);
//AccessibleTest类中的成员变量为private,故必须进行此操作
field.setAccessible(true);
field.set(context,view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
这样我们就可以在activity中使用了:
@ViewInject(R.id.btn1)
Button btn1;
@ViewInject(R.id.btn2)
Button btn2;
最后我们来实现一个稍微有点难度的,事件注解 先看下我们要实现的效果:
@OnClick({R.id.btn1,R.id.btn2})
public void onclick(View view){
}
@OnLongClick({R.id.btn2})
public boolean onLongClick(View view){
return false;
}
这里看到我们这里使用了不同的事件注解,所以在这里我们需要考虑的是android中23种事件,我不可能在InjectUtils类中写23个方法去实现这些事件;这里采用的是注解的多态,和代理来实现事件的注入。来看下我们具体的实现步骤:
1.需要创建一个EventBase注解:
//此处使用ANNOTATION_TYPE,表示该注解在其他注解之上
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
//setOnClickListener 订阅关系
String listenerSetter();
//new View.OnClickListener() 事件本身
Class<?> listenerType();
//3.事件处理程序
String callbackMethod();
}
-
创建具体的事件注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @EventBase(listenerSetter = "setOnClickListener" ,listenerType = View.OnClickListener.class ,callbackMethod = "OnClick") public @interface OnClick { int[] value() default -1; //此处表示赋值-1; }
3.创建代理类
public class ListenerInvocationHandler implements InvocationHandler {
//需要在onClick中执行Activity.click();
private Object activity;
private Method activityMethod;
public ListenerInvocationHandler(Object activity, Method activityMethod) {
this.activity = activity;
this.activityMethod = activityMethod;
}
/**
* 表示onClick的执行;
* 程序执行onClick方法时,就会转到这里;
* 框架中不直接执行onClick;
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//此处调用被注解的click();
return activityMethod.invoke(activity,args);
}
}
这个类用来代理,new View.OnClickListener()对象; 并执行这个对象的onClick方法
4.在InjectUtils中处理具体的事件:
private static void injectClick(Object context) {
//需要一次性注入android的23种事件;
Class<?> clazz = context.getClass();
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods)
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations){
//annotation 是事件比如onClick 就去取对应的注解;
Class<?> annotationClass = annotation.annotationType();
EventBase eventBase = annotationClass.getAnnotation(EventBase.class);
//如果没有eventBase 则表示当前方法不是一个事件处理的方法
if (eventBase == null)
//不是事件处理方法;
continue;
//开始获取事件处理的相关信息;
String listenerSetter = eventBase.listenerSetter();
Class<?> listenerType = eventBase.listenerType();
//事件处理程序
String callBackMethod = eventBase.callbackMethod();
Method valueMethod = null;
try {
//反射得到Id,再根据ID号得到对应的VIEW;
valueMethod = annotationClass.getDeclaredMethod("value");
int[] viewId = (int[]) valueMethod.invoke(annotation);
for (int id : viewId) {
//为了得到Button对象;
Method findViewById = clazz.getMethod("findViewById",int.class);
View view = (View) findViewById.invoke(context,id);
if (view == null){
continue;
}
//activity == context; click = method;
ListenerInvocationHandler listenerInvocationHandler =
new ListenerInvocationHandler(context,method);
//建立代理关系;
Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(),
new Class[]{listenerType},listenerInvocationHandler);
//让proxy去执行的onClick();
Method onClickMethod = view.getClass().getMethod(listenerSetter,listenerType);
onClickMethod.invoke(view,proxy);
//此时,点击按钮就会去执行代理中invoke,方法;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
这样我们就可以实现事件绑定了; 这里只是IOC的简单实现。