什么是IOC?即控制反转(Inversion of Control,英文缩写为IoC)是一个重要的面向对象编程的法则来削减计算机程序的耦合问题,也是轻量级的Spring框架的核心。 控制反转一般分为两种类型,依赖注入(Dependency Injection,简称DI)和依赖查找(Dependency Lookup)。依赖注入应用比较广泛。
本文主要是高仿XUtils注解方式进行依赖注入,包括对控件实例化注入、控件事件监听注入、布局注入等
一、布局注入
在activity中我们常常看到通过这样的方式,渲染布局
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_container);
}
通过这种方式当然是最简单最方便的,但这种方式是没有任何架构的,不利于拓展和管理,就举个最简单的例子,假设你的activity里面有很多代码,不说多,总代码量是400行还是情理之中的吧?如果要你找到这个activity的布局,然后对布局优化一下,你估计要找好一会儿,对吗?为什么?因为你代码量多,你不记得oncreate在什么位置,你不能保证你写的oncreate方法在第一行,或者是最后一行,对吧?
当然,我们可以通过依赖注入的方式,只需要这样配置,就可以渲染布局了,如下:
@ContentView(R.layout.content_main)
public class MainActivity extends BaseActivity {
}
代码是不是非常简洁了?OK这是必须的,如果产品说某个页面布局不好看,要去优化一下布局,找到activity,定位到类上面,是不是马上就找到了你引用的布局了?这就是依赖注入,非常方便,并且有简洁
当然,现在有些哥们等不及了,这种方式是如何实现对布局的注入的呢?其实非常简单,大家看到了MainActivity 是继承了BaseActivity 的了吗?我们的注入,实际上就是在BaseActivity 的oncreate方法里面就注入了,这样,以后每新增一个activity,就无需再次手动注入,直接通过配置类似@ContentView(R.layout.content_main)的 方式就可以轻松完成对布局的注入了。
OK,我们来看BaseActivity 的代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtils.inject(this);
}
对,就是通过InjectUtils.inject(this);实现注入的,完成注入的步骤如下:
1.新建一个注解类ContentView
2.通过反射获取activity配置的注解ContentView对应的值value,完成对布局的注入
哈哈,so easy,原来就是这么简单,关于注解的定义,不是本文讨论重点,需要了解的自行查找资料
步骤:
1.定义注解类ContentView
2.获取activity对象的字节码,通过字节码获取其配置的 注解类ContentView
3.对注解ContentView 取value值,然后利用activity对象调用setContentView完成布局的渲染
OK,代码如下
1.定义注解类ContentView
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ContentView {
int layoutId();
}
2.获取activity对象的字节码,通过字节码获取其配置的 注解类ContentView
Class extends BaseActivity> aClass = baseActivity.getClass();
ContentView annotation = aClass.getAnnotation(ContentView.class);
3.对注解ContentView 取value值,然后利用activity对象调用setContentView完成布局的渲染
int layoutId = annotation.value();
baseActivity.setContentView(layoutId);
整体demo:
/**
* 注入contentView
* @param baseActivity
*/
private static void injectContentView(BaseActivity baseActivity) {
Class extends BaseActivity> aClass = baseActivity.getClass();
ContentView annotation = aClass.getAnnotation(ContentView.class);
int layoutId = annotation.value();
baseActivity.setContentView(layoutId);
}
看到这里,现在有哥们等不及了?我直接使用setContentView不就好了,一行demo就搞定,你现在通过IOC框架的方式,要写那么多demo,并且最终还是调用setContentView完成,通过这个框架不仅demo量没有减少,反而多了,这样做到底有什么意义呢?
嗯嗯,哥们你说的不错,原理都是一样,都是通过调用setContentView完成对布局的渲染的。另外我们通过框架的方式,代码量也变多了一些,并且也会对性能有一定的影响。既然是框架,如果每次都需要编写这么多demo, 我想是换成谁都受不了,哪还会有人用呢~
其实嘛,框架是这么体现的,只需编写一次代码,后面都是复用,移植性好,容易拓展新功能,这就是框架的好处。
在XUtils中,我们常常看到布局中定义的控件,通过配置注解,就可以实例化控件了,如
@InjectView(R.id.zlv)
private ParallaxListView parallaxListView;
那么,这种方式是如何通过IOC实例化的呢?
步骤:
1.定义注解InjectView
2.获取activity字节码,通过反射得到所有声明的字段
3.获取声明字段声明的注解InjectView,获取其value(就是布局中控件对应的id)值
4.通过activity对象调用findViewById实例化控件,再通过字段字节码反射设置给声明的控件
1.定义注解InjectView
/**
* Created by harry on 2016/12/17.
* activity 实例化控件的注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectView {
int value();
}
2.获取activity字节码,通过反射得到所有声明的字段
Class
/**
* 实例化有注解的控件
* @param baseActivity
*/
private static void injectView(BaseActivity baseActivity) {
Class extends BaseActivity> aClass = baseActivity.getClass();
Field[] fields = aClass.getDeclaredFields();
if(fields!=null&&fields.length>0){
for (Field field:fields){
field.setAccessible(true);
InjectView annotation = field.getAnnotation(InjectView.class);
if(annotation!=null){
int id = annotation.value();
try {
View view = baseActivity.findViewById(id);
field.set(baseActivity,view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
代码还算是通俗易懂的,这里就不再写注释了,上述步骤就是注释了。
话说,如果我要给控件定义点击事件,这时候要怎么做呢?
通常,我们在activity中是这样设置控件的点击事件的,如:
view.setOnClickListener(new OnclickListener(){
public void onClick(View v){
}
}
);
view.setOnLongClickListener(new OnLongClickListener(){
public boolean onLongClick(View v){
}
}
);
等等,如果使用了IOC注入的方式,通常,我们只需这么定义
@OnClick(R.id.tv) //这id,就是在布局文件中定义的控件id,要设置的控件的点击事件
public void mac(View view){
}
我们在回顾传统的事件监听,它们都有一些共同的属性,譬如事件方法名称,事件类型,事件回调方法
由此,我们可以抽取出这些事件类型的共性,它包含的属性有
事件方法名称
事件类型
回调方法名称
步骤:
1.定义抽取出的事件基类注解EventBase,作为注解的注解
2.获取activity上声明的方法,并筛选出定义有EventBase注解的注解
3.得到注解的注解EventBase,获取其值
4.通过动态代理,为控件设置点击事件
1.定义抽取出的事件基类注解EventBase,作为注解的注解
/**
* Created by harry on 2016/12/17.
* 事件类型注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE) //注解的注解
public @interface EventBase {
/**
* 事件监听的set方法
* @return
*/
String listenerSetter();
/**
* 事件监听的类型
* @return
*/
Class> listenerType();
/**
* 事件被触发之后,执行的回调方法名称
* @return
*/
String callbackMethod();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter ="setOnClickListener",listenerType = View.OnClickListener.class,callbackMethod ="onClick" )
public @interface OnClick {
int[] value();
}
2.获取activity上声明的方法,并筛选出定义有EventBase注解的注解
3.得到注解的注解EventBase,获取其值
private static void injectEventBase(Object obj, Object view) {
Class> aClass = obj.getClass();
Method[] methods = aClass.getDeclaredMethods();
if(methods!=null&&methods.length>0){
for (Method method:methods){
//得到方法的注解
Annotation[] annotations = method.getAnnotations();
if(annotations!=null&&annotations.length>0){
for (Annotation annotation:annotations){
Class extends Annotation> annotationType = annotation.annotationType();
//得到基类注解
EventBase eventBase = annotationType.getAnnotation(EventBase.class);
if(eventBase!=null){
String listenerSetter = eventBase.listenerSetter();
Class> listenerType = eventBase.listenerType();
String callbackMethod = eventBase.callbackMethod();
//1.获取方法上的注解信息
try {
Method value = annotationType.getDeclaredMethod("value");
int[] ids = (int[]) value.invoke(annotation);
for (int id:ids){
//2.实例化控件
View fieldView=null;
if(view instanceof View){
fieldView = ((View) view).findViewById(id);
}else if(view instanceof Activity){
fieldView = ((Activity) view).findViewById(id);
}
if(fieldView==null){
return;
}
Class extends View> viewClass = fieldView.getClass();
Method setter = viewClass.getMethod(listenerSetter, listenerType);
//3有了setter方法,就可以代理监听事件了
Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(),new Class[]{listenerType} , new OnListenerInvocationHandler(obj,callbackMethod,method));
setter.invoke(fieldView,proxy);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
}
4.通过动态代理,为控件设置点击事件
/**
* Created by harry on 2016/12/18.
* 动态代理
*/
public class OnListenerInvocationHandler implements InvocationHandler {
private Method method;
private Object target;
private String callBackMethod;
public OnListenerInvocationHandler( Object target,String callBackMethod, Method method) {
this.target =target;
this.method=method;
this.callBackMethod=callBackMethod;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//传入的回调方法名称与需要调用的方法名称一致的时候,反射调用带有注解的方法
if(method.getName().equals(callBackMethod)&&this.method!=null){
return this.method.invoke(target,args);
}
return method.invoke(proxy,args);
}
}
到此,我们的事件注入就告一大段落了!
注入工具类完整代码:
package com.ishow.androidlibs.utils;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.view.View;
import com.ishow.androidlibs.activity.BaseActivity;
import com.ishow.androidlibs.annotation.ContainerView;
import com.ishow.androidlibs.annotation.ContentView;
import com.ishow.androidlibs.annotation.CreateView;
import com.ishow.androidlibs.annotation.EventBase;
import com.ishow.androidlibs.annotation.InjectView;
import com.ishow.androidlibs.fragment.RootFragment;
import com.ishow.androidlibs.ioc.proxy.OnListenerInvocationHandler;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Created by harry on 2016/12/17.
* 注入工具
*/
public class InjectUtils {
/**
* 注入fragment
* @param rootFragment
* @return
*/
public static View injectFragment(RootFragment rootFragment){
Class extends RootFragment> aClass = rootFragment.getClass();
CreateView annotation = aClass.getAnnotation(CreateView.class);
if(annotation!=null){
int layoutId = annotation.value();
View view = rootFragment.getActivity().getLayoutInflater().inflate(layoutId, null);
inject(rootFragment,view);
return view;
}
return null;
}
/**
* 根据指定对象及其view注入该对象下配置的控件
* @param obj 将要注入实例化的对象
* @param view view
*/
public static void inject(@NonNull Object obj,View view) {
injectView(obj,view);
injectEvent(obj, view);
}
private static void injectEvent(Object obj, View view) {
injectEventBase(obj, view);
}
private static void injectEventBase(Object obj, Object view) {
Class> aClass = obj.getClass();
Method[] methods = aClass.getDeclaredMethods();
if(methods!=null&&methods.length>0){
for (Method method:methods){
Annotation[] annotations = method.getAnnotations();
if(annotations!=null&&annotations.length>0){
for (Annotation annotation:annotations){
Class extends Annotation> annotationType = annotation.annotationType();
EventBase eventBase = annotationType.getAnnotation(EventBase.class);
if(eventBase!=null){
String listenerSetter = eventBase.listenerSetter();
Class> listenerType = eventBase.listenerType();
String callbackMethod = eventBase.callbackMethod();
//1.获取方法上的注解信息
try {
Method value = annotationType.getDeclaredMethod("value");
int[] ids = (int[]) value.invoke(annotation);
for (int id:ids){
//2.实例化控件
View fieldView=null;
if(view instanceof View){
fieldView = ((View) view).findViewById(id);
}else if(view instanceof Activity){
fieldView = ((Activity) view).findViewById(id);
}
if(fieldView==null){
return;
}
Class extends View> viewClass = fieldView.getClass();
Method setter = viewClass.getMethod(listenerSetter, listenerType);
//3有了setter方法,就可以代理监听事件了
Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(),new Class[]{listenerType} , new OnListenerInvocationHandler(obj,callbackMethod,method));
setter.invoke(fieldView,proxy);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
}
/**
* 注入控件
* @param obj
* @param view
*/
private static void injectView(Object obj, View view) {
//1.获取obj对象字节码,反射获取配置字段注解
Class> aClass = obj.getClass();
Field[] fields = aClass.getDeclaredFields();
if(fields!=null&&fields.length>0){
for (Field field:fields){
InjectView annotation = field.getAnnotation(InjectView.class);
if(annotation!=null){
int value = annotation.value();
field.setAccessible(true);
View viewById = view.findViewById(value);
try {
field.set(obj,viewById);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
//2.将注解值通过view来findViewById实例化字段
}
public static void inject(@NonNull BaseActivity baseActivity) {
injectContentView(baseActivity);
injectView(baseActivity);
injectEvent(baseActivity);
}
/**
* 注入事件
* @param baseActivity
*/
private static void injectEvent(BaseActivity baseActivity) {
injectEventBase(baseActivity,baseActivity);
}
/**
* 实例化有注解的控件
* @param baseActivity
*/
private static void injectView(BaseActivity baseActivity) {
Class extends BaseActivity> aClass = baseActivity.getClass();
Field[] fields = aClass.getDeclaredFields();
if(fields!=null&&fields.length>0){
for (Field field:fields){
field.setAccessible(true);
InjectView annotation = field.getAnnotation(InjectView.class);
if(annotation!=null){
int id = annotation.value();
try {
View view = baseActivity.findViewById(id);
field.set(baseActivity,view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 注入contentView
* @param baseActivity
*/
private static void injectContentView(BaseActivity baseActivity) {
Class extends BaseActivity> aClass = baseActivity.getClass();
ContentView annotation = aClass.getAnnotation(ContentView.class);
int layoutId = annotation.value();
baseActivity.setContentView(layoutId);
}
/**
* 获取fragment容器ID
* @param baseActivity
* @return int
*/
public static int getContainerViewId(@NonNull BaseActivity baseActivity){
Class extends BaseActivity> aClass = baseActivity.getClass();
ContainerView annotation = aClass.getAnnotation(ContainerView.class);
return annotation.value();
}
}
好了,看我是这么使用的,如果是activity中渲染布局,为了更好的体现出框架的好处,我们定义一个基类activity—BaseActivity
public abstract class BaseActivity extends Activity{
private StackManager stackManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtils.inject(this);
}
}
然后我们只需这么使用,就可以加载布局上来了,以后所有
@ContentView(R.layout.content_main)
public class MainActivity extends BaseActivity {
}
类似的,如果是fragment,我们也抽出BaseFragment
public abstract class BaseFragment extends Fragment implements ICompotCallBack {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
//注入view
return InjectUtils.injectFragment(this);
}
}
这样,子类只需进行如下配置,就可以直接使用framgent了
@CreateView(R.layout.home)
public class HomeFragment extends BaseFragment {
}
如果要在activity或者fragment中实例化布局中定义的控件,只需如下配置,就可以了
@InjectView(R.id.zlv)
ParallaxListView parallaxListView;
点击事件的设置,只需如下配置:
@OnClick(R.id.tv)
public void mac(View view){
//事件的业务逻辑...
}
如果要长按,我们可能再拓展,编写一个长按的注解,配置就好,如:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@EventBase(listenerSetter ="setOnLongClickListener",listenerType = View.OnLongClickListener.class,callbackMethod ="onLongClick" )
public @interface OnLongClick {
int[] value();
}
然后这样使用:
@OnLongClick (R.id.tv)
public boolean toast(View view){
//事件的业务逻辑...
return false;
}
注:方法参数和返回值必须要与需要设置的点击类型的回调方法一致,譬如控件的OnLongClickListener的回调方法是public boolean onLongClick(View view),所以这里的方法的返回值和参数必须与回调方法一致,否则配置的点击事件就不起作用
看起来虽然代码很多,但只需一次编写,重复调用,可拓展性好,优点还是蛮多的,因为这是在运行时注入的,会影响性能。现在很多好的框架都是在编译时注入的,这样对性能影响不大,譬如ButterKnife等