前言
在讲IOC(依赖注入)前 先来看一段代码 或者下载demo 进行细看
@ContentView(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
private final static String TAG=MainActivity.class.getSimpleName();
@ViewInject(R.id.textview)
private TextView textview;
@ViewInject(R.id.button)
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtils.inject(this);
}
@OnClick({R.id.button})
public void onClick(View view){
Log.e(TAG,"点击事件");
Toast.makeText(this,"我点击了button",Toast.LENGTH_SHORT).show();
}
@OnLongClick({R.id.textview})
public void onLongClick(View view){
Log.e(TAG,"长按事件");
Toast.makeText(this,"我点击了textview",Toast.LENGTH_SHORT).show();
return true;
}
}
是不是很熟悉的感觉,目前网上能实现该功能比较火的库有:
XUtils
butterknife
dagger
dagger和butterknife是使用APT将.java文件在编译成.class文件的时候将注解对象添加到.class字节码当中 号称是无性能消耗的 这个不是本章重点 可以的话会在下一章进行介绍
XUtils是使用IOC(依赖注入)在运行时动态地将某种依赖关系注入到对象之中,因为使用到反射所以在性能上有点不及APT,本章将介绍如何使用IOC实现View事件对象的注入进行简单的案例讲解
IOC
什么是IOC呢?通俗的来讲:
指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。
我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言的的反射编程,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。
实际应用:
在实际项目开发过程中很多页面(Activity)都要去将各种View进行findViewById(...)将花费大量的时间去写重复性的代码而且代码整体看上去不够简洁不方便维护 利用IOC的原理我们可以通过注入的方式去将各个控件注入到每个Activity中 其好处有:
- 整体代码变得简洁 容易维护
- IOC是工厂模式的升华 是一种容器 把工厂和对象生成这两者独立分隔开来 通过注解的方式生成对象
使用
下面就教大家如何使用IOC架构进行事件注入,同时本篇使用到的相关知识有:
注解,反射,动态代理 若有某个知识点不会的朋友,因篇幅有限 请自行百度或Gooogle,同时我也会尽量把注释写明白,到这里那就开车吧:
自定义注解
//自定义ContentView注解 参数为int类型
@Retention(RetentionPolicy.RUNTIME)//运行时
@Target(ElementType.TYPE)//该注解使用在类上
public @interface ContentView {
int value();
}
@Retention(RetentionPolicy.RUNTIME)//运行时
@Target(ElementType.FIELD)//应用在成员变量上
public @interface ViewInject {
int value();
}
创建InjectUtils工具类做一些初始化和反射的操作
public class InjectUtils {
public static void inject(Context context){
injectLayout(context);
injectView(context);
}
/**
* 注解Layout
* @param context
*/
private static void injectLayout(Context context) {
int layoutId=0;
Class> clazz=context.getClass();
//获取类对象的 ContentView注解
ContentView contentView=clazz.getAnnotation(ContentView.class);
if (contentView!=null){
//获取ContentView注解的返回值--->R.layout.xxx
layoutId=contentView.value();
try {
//反射获取 context下的setContentView方法
Method method=clazz.getMethod("setContentView",int.class);
//执行该方法 ---->setContentView(layoutId)
method.invoke(context,layoutId);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
/**
* 注解view
* @param context
*/
public static void injectView(Context context){
Class> aClass=context.getClass();
//获取该类下的所有成员变量
Field[] fields=aClass.getDeclaredFields();
for(Field field:fields){
//遍历所有带有ViewInject注解的成员变量
ViewInject annotation = field.getAnnotation(ViewInject.class);
if(annotation!=null){
//获取注解的返回值---》R.id.xxx
int valueID = annotation.value();
try {
//反射获取findViewById函数
Method findViewById = aClass.getMethod("findViewById", int.class);
//执行findViewById函数获取--->View=findViewById(R.id.xxx)
View view = (View) findViewById.invoke(context, valueID);
//允许改变该成员变量的值
field.setAccessible(true);
//给该成员变量赋值--->xxxActivity.textView=view
field.set(context,view);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
使用
@ContentView(R.layout.activity_main)//自定义注解
public class MainActivity extends AppCompatActivity {
@ViewInject(R.id.textview)//自定义注解
private TextView textview;
@ViewInject(R.id.button)//自定义注解
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//初始化
InjectUtils.inject(this);
textview.setText("IOC控制反转");
button.setText("IOC依赖注入");
}
}
一个简单的IOC架构就写好了 其原理就是InjectUtils去找到我们的自定义的注解@ContentView获取返回值(布局ID)去反射执行setContentView(layoutId)方法 同时获取所有含有@ViewInject注解的成员变量 并进行反射调用findViewById(R.id.xxx)为控件赋值 通过下图就可以看出 TextView和Button通注解实现了对象的创建
下面将来点有难度的 将View的事件进行注解注入 首先我们需要了解一个事件对象执行的要素:
- 事件监听对象
- 事件类型(点击还是长按?)
- 监听方法(setOnLongClickListener/setOnClickListener)
- 监听回调以及是否有返回值(onLongClick/onClick)
问题分析:
-
事件监听对象 由上面的小例子可以想到我们可以通过自定义注解来获取到需要进行事件监听的View同时也可以通过反射调用该View的事件监听函数 区别在于怎么去判断调用哪一个监听函数(点击/长按?)
//(getMethod)类的所有共有方法 获取findViewById函数
Method findViewById = clazz.getMethod("findViewById", int.class);
//执行findViewById得到View
View view = (View) findViewById.invoke(context, viewId);
//反射获取view的事件监听方法(事件函数,事件类型)
Method setListener = view.getClass().getMethod(listenerSetter, listenerType);
- 同时因为不同的事件监听函数,所需要的事件对象类型也不同 以及不同的回调函数和返回值等这些都要考虑解决
解决思路:
在开篇的那段代码中可以看到最理想的解决方案是这样的 通过定义不同的自定义注解比如**@OnClick({R.id.button})**或**@OnLongClick({R.id.textview})**来区分事件类型(点击/长按),这样我们可以拿到需要监听事件的View对象:
@OnClick({R.id.button})
public void onClick(View view){
Log.e(TAG,"点击事件");
}
@OnLongClick({R.id.textview})
public void onLongClick(View view){
Log.e(TAG,"长按事件");
return true;
}
到这里我们就有2条思路去解决后面的问题:**如何判断反射调用哪一个监听函数(点击/长按?)**
- 根据注解名称的不同来区分反射调用哪一个监听函数
........
String name=annotation.getClass().getSimpleName()
swicth(name){
case OnClick:
.........
break;
case OnLongClick:
.........
break;
}
- 通过注解特性我们知道注解不但可以用在 类,方法,成员变量等上面 还可以用在注解上面 而且还可以可以设置元数据供程序读取 所以可以根据注解函数返回值来做区别 开始写代码:
//自定义EventBase 并且有3个返回值函数(事件监听的方法、事件类型、事件回调方法)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)//定义在注解上使用
public @interface EventBase {
/**
* 事件监听的方法
* @return
*/
String listenerSetter();
/**
* 事件类型
* @return
*/
Class> listenerType();
/**
* 事件回调方法
* 事件触发后 执行回调方法
* @return
*/
String callBackMethod();
}
使用@EventBase自定义注解到自定义注解上面 并且根据不同的自定义事件注解 设置不同的注解函数返回值 (这样的扩展性非常好)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnClickListener",//点击事件
listenerType = View.OnClickListener.class,//接口
callBackMethod = "onClick")//接口回调
public @interface OnClick {
/**
* 需要点击事件的View
* @return
*/
int[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnLongClickListener",
listenerType = View.OnLongClickListener.class,
callBackMethod = "onLongClick")
public @interface OnLongClick {
/**
* 需要长按事件的View
* @return
*/
int [] value() default -1;
}
到这里问题解决一大半 还有最后一个问题就是回调函数的执行 这里需要用到动态代理去实现
public class InjectUtils {
public static void inject(Context context){
injectLayout(context);//--->setContentView(layoutId)
injectView(context);//--->findViewById(R.id.xxx)
injectEvents(context);
}
private static void injectEvents(Context context) {
Class> clazz=context.getClass();
//保存函数对应的事件回调方法
Map methodMap=new HashMap<>();
//获取Activtiy里面所有的方法
Method[] methods=clazz.getDeclaredMethods();
//遍历
for(Method method:methods){
//获取该函数上的所有注解
Annotation[] annotations=method.getAnnotations();
//遍历该函数注解
for (Annotation annotation:annotations){
//获取该注释的注释类型
Class> anntionType=annotation.annotationType();
//获取注解上面的EventBase的注解
EventBase eventBase=anntionType.getAnnotation(EventBase.class);
//判空
if(null==eventBase) {continue;}
//获取EventBase注解3个函数的返回值
// 也就是事件三要素(监听的方法,事件类型,回调函数)
String listenerSetter = eventBase.listenerSetter();
//事件类型 长按 还是点击
Class> listenerType = eventBase.listenerType();
//事件回调--onClick()
String backMethod = eventBase.callBackMethod();
//将该函数与对应的事件回调方法保存到map中
methodMap.put(backMethod,method);
try {
//(getDeclaredMethod)获取的是类自身声明的所有方法,包含public、protected和private方法
Method valueMethod = anntionType.getDeclaredMethod("value");
//获取函数注解的返回值(view id)
int[] viewIds = (int[]) valueMethod.invoke(annotation);
for(int viewId:viewIds){
//(getMethod)类的所有共有方法 获取findViewById函数
Method findViewById = clazz.getMethod("findViewById", int.class);
//执行findViewById得到View
View view = (View) findViewById.invoke(context, viewId);
if (null==view){continue;}
//反射获取view的事件监听方法(事件函数,事件类型)
Method setListener = view.getClass().getMethod(listenerSetter, listenerType);
//创建代理类对象
ListenerInvocationHandler handler=new ListenerInvocationHandler(context,methodMap);
//proxyInstance实现listenerType(事件类型)接口
Object proxyInstance = Proxy.newProxyInstance(anntionType.getClassLoader(), new Class[]{listenerType}, handler);
//执行事件回调方法
setListener.invoke(view,proxyInstance);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
其中这段代码使用了动态代理 关于动态代理这里不做详细介绍:
//创建代理类对象
ListenerInvocationHandler handler=new ListenerInvocationHandler(context,methodMap);
//proxyInstance实现listenerType(事件类型)接口
Object proxyInstance = Proxy.newProxyInstance(anntionType.getClassLoader(), new Class[]{listenerType}, handler);
//执行事件回调方法
setListener.invoke(view,proxyInstance);
动态代理类:
public class ListenerInvocationHandler implements InvocationHandler {
private Context context;
private Map
public ListenerInvocationHandler(Context context, Map
this.context = context;
this.methodMap = methodMap;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName=method.getName();//获取绑架事件监听的函数
Method med= methodMap.get(methodName);//根据函数名获取事件监听方法
if(null!=med){
Log.e(TAG,"代理开始");
return med.invoke(context,args);
}else {
return method.invoke(proxy,args);
}
}
}
大概的执行流程图:
![](http://upload-images.jianshu.io/upload_images/2158254-12a1628c27dc3290.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
Activtiy中使用:
@OnClick({R.id.button})
public void onClick(View view){
Log.e(TAG,"点击事件");
Toast.makeText(this,"我点击了button",Toast.LENGTH_SHORT).show();
}
@OnLongClick({R.id.textview})
public boolean onLongClick(View view){
Log.e(TAG,"长按事件");
Toast.makeText(this,"我长按了textview",Toast.LENGTH_SHORT).show();
return true;
}
最后看效果:
![](http://upload-images.jianshu.io/upload_images/2158254-7e27ac440f1ce747.gif?imageMogr2/auto-orient/strip)