Android应用开发中控制反转IoC设计模式使用教程
1、概述
首先我们来吹吹牛,什么叫ioc,控制反转(inversion of control,英文缩写为ioc),什么意思呢?
就是你一个类里面需要用到很多个成员变量,传统的写法,你要用这些成员变量,那么你就new 出来用呗~~
ioc的原则是:no,我们不要new,这样耦合度太高;你配置个xml文件,里面标明哪个类,里面用了哪些成员变量,等待加载这个类的时候,我帮你注入(new)进去;
这样做有什么好处呢?
回答这个问题,刚好可以回答另一个问题,很多人问,项目分层开发是吧,分为控制层、业务层、dao层神马的。然后每一层为撒子要一个包放接口,一个包放实现呢?只要一个实现包不行么~刚好,如果你了解了ioc,你就知道这些个接口的作用了,上面不是说,你不用new,你只要声明了成员变量+写个配置文件,有人帮你new;此时,你在类中,就可以把需要使用到的成员变量都声明成接口,然后你会发现,当实现类发生变化的时候,或者切换实现类,你需要做什么呢?你只要在配置文件里面做个简单的修改。如果你用的就是实实在在的实现类,现在换实现类,你需要找到所有声明这个实现类的地方,手动修改类名;如果你遇到了一个多变的老大,是吧,呵呵~
当然了,很多会觉得,写个配置文件,卧槽,这多麻烦。于是乎,又出现了另一种方案,得,你闲配置文件麻烦,你用注解吧。你在需要注入的成员变量上面给我加个注解,例如:@inject,这样就行了,你总不能说这么个单词麻烦吧~~
当然了,有了配置文件和注解,那么怎么注入呢?其实就是把字符串类路径变成类么,当然了,反射上场了;话说,很久很久以前,反射很慢啊,嗯,那是很久很久以前,现在已经不是太慢了,当然了肯定达不到原生的速度~~无反射,没有任何框架。
如果你觉得注解,反射神马的好高级。我说一句:just do it ,你会发现注解就和你写一个普通javabean差不多;反射呢?api就那么几行,千万不要被震慑住~
2、框架实现
得进入正题了,android ioc框架,其实主要就是帮大家注入所有的控件,布局文件什么的。如果你用过xutils,afinal类的框架,你肯定不陌生~
注入view
假设:我们一个activity,里面10来个view。
传统做法:我们需要先给这个activity设置下布局文件,然后在oncreate里面一个一个的findviewbyid把~
目标的做法:activity类上添加个注解,帮我们自动注入布局文科;声明view的时候,添加一行注解,然后自动帮我们findviewbyid;
于是乎我们的目标类是这样的:
@contentview(value = r.layout.activity_main) public class mainactivity extends baseactivity { @viewinject(r.id.id_btn) private button mbtn1; @viewinject(r.id.id_btn02) private button mbtn2;
3、编码
(1)定义注解
首先我们需要两个注解文件:
package com.zhy.ioc.view.annotation; import java.lang.annotation.elementtype; import java.lang.annotation.retention; import java.lang.annotation.retentionpolicy; import java.lang.annotation.target; @target(elementtype.type) @retention(retentionpolicy.runtime) public @interface contentview { int value(); }
contentview用于在类上使用,主要用于标明该activity需要使用的布局文件。
@contentview(value = r.layout.activity_main) public class mainactivity package com.zhy.ioc.view.annotation; import java.lang.annotation.elementtype; import java.lang.annotation.retention; import java.lang.annotation.retentionpolicy; import java.lang.annotation.target; @target(elementtype.field) @retention(retentionpolicy.runtime) public @interface viewinject { int value(); }
在成员变量上使用,用于指定view的id
@viewinject(r.id.id_btn) private button mbtn1;
简单说一下注解:定义的关键字@interface ; @target表示该注解可以用于什么地方,可能的类型type(类),field(成员变量),可能的类型:
public enum elementtype { /** * class, interface or enum declaration. */ type, /** * field declaration. */ field, /** * method declaration. */ method, /** * parameter declaration. */ parameter, /** * constructor declaration. */ constructor, /** * local variable declaration. */ local_variable, /** * annotation type declaration. */ annotation_type, /** * package declaration. */ package }
就是这些个枚举。
@retention表示:表示需要在什么级别保存该注解信息;我们这里设置为运行时。
可能的类型:
public enum retentionpolicy { /** * annotation is only available in the source code. */ source, /** * annotation is available in the source code and in the class file, but not * at runtime. this is the default policy. */ class, /** * annotation is available in the source code, the class file and is * available at runtime. */ runtime }
这些个枚举~
(2)mainactivity
package com.zhy.zhy_xutils_test; import android.app.activity; import android.os.bundle; import android.view.view; import android.view.view.onclicklistener; import android.widget.button; import android.widget.toast; import com.zhy.ioc.view.viewinjectutils; import com.zhy.ioc.view.annotation.contentview; import com.zhy.ioc.view.annotation.viewinject; @contentview(value = r.layout.activity_main) public class mainactivity extends activity implements onclicklistener { @viewinject(r.id.id_btn) private button mbtn1; @viewinject(r.id.id_btn02) private button mbtn2; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); viewinjectutils.inject(this); mbtn1.setonclicklistener(this); mbtn2.setonclicklistener(this); } @override public void onclick(view v) { switch (v.getid()) { case r.id.id_btn: toast.maketext(mainactivity.this, "why do you click me ?", toast.length_short).show(); break; case r.id.id_btn02: toast.maketext(mainactivity.this, "i am sleeping !!!", toast.length_short).show(); break; } } }
注解都写好了,核心的代码就是viewinjectutils.inject(this)了~
(3)viewinjectutils
a、首先是注入主布局文件的代码:
/** * 注入主布局文件 * * @param activity */ private static void injectcontentview(activity activity) { class<? extends activity> clazz = activity.getclass(); // 查询类上是否存在contentview注解 contentview contentview = clazz.getannotation(contentview.class); if (contentview != null)// 存在 { int contentviewlayoutid = contentview.value(); try { method method = clazz.getmethod(method_set_contentview, int.class); method.setaccessible(true); method.invoke(activity, contentviewlayoutid); } catch (exception e) { e.printstacktrace(); } } }
通过传入的activity对象,获得它的class类型,判断是否写了contentview这个注解,如果写了,读取它的value,然后得到setcontentview这个方法,使用invoke进行调用;
有个常量:
private static final string method_set_contentview = "setcontentview";
b、接下来是注入views
private static final string method_find_view_by_id = "findviewbyid"; /** * 注入所有的控件 * * @param activity */ private static void injectviews(activity activity) { class<? extends activity> clazz = activity.getclass(); field[] fields = clazz.getdeclaredfields(); // 遍历所有成员变量 for (field field : fields) { viewinject viewinjectannotation = field .getannotation(viewinject.class); if (viewinjectannotation != null) { int viewid = viewinjectannotation.value(); if (viewid != -1) { log.e("tag", viewid+""); // 初始化view try { method method = clazz.getmethod(method_find_view_by_id, int.class); object resview = method.invoke(activity, viewid); field.setaccessible(true); field.set(activity, resview); } catch (exception e) { e.printstacktrace(); } } } } }
获取声明的所有的属性,遍历,找到存在viewinject注解的属性,或者其value,然后去调用findviewbyid方法,最后把值设置给field~~~
好了,把这两个方法写到inject里面就好了。
public static void inject(activity activity) { injectcontentview(activity); injectviews(activity); }
效果图:
4.view的事件的注入
光有view的注入能行么,我们写view的目的,很多是用来交互的,得可以点击神马的吧。摒弃传统的神马,setonclicklistener,然后实现匿名类或者别的方式神马的,我们改变为:
package com.zhy.zhy_xutils_test; import android.view.view; import android.widget.button; import android.widget.toast; import com.zhy.ioc.view.annotation.contentview; import com.zhy.ioc.view.annotation.onclick; import com.zhy.ioc.view.annotation.viewinject; @contentview(value = r.layout.activity_main) public class mainactivity extends baseactivity { @viewinject(r.id.id_btn) private button mbtn1; @viewinject(r.id.id_btn02) private button mbtn2; @onclick({ r.id.id_btn, r.id.id_btn02 }) public void clickbtninvoked(view view) { switch (view.getid()) { case r.id.id_btn: toast.maketext(this, "inject btn01 !", toast.length_short).show(); break; case r.id.id_btn02: toast.maketext(this, "inject btn02 !", toast.length_short).show(); break; } } }
直接通过在activity中的任何一个方法上,添加注解,完成1个或多个控件的事件的注入。这里我把oncreate搬到了baseactivity中,里面调用了viewinjectutils.inject(this);
(1)注解文件
package com.zhy.ioc.view.annotation; import java.lang.annotation.elementtype; import java.lang.annotation.retention; import java.lang.annotation.retentionpolicy; import java.lang.annotation.target; @target(elementtype.annotation_type) @retention(retentionpolicy.runtime) public @interface eventbase { class<?> listenertype(); string listenersetter(); string methodname(); } package com.zhy.ioc.view.annotation; import java.lang.annotation.elementtype; import java.lang.annotation.retention; import java.lang.annotation.retentionpolicy; import java.lang.annotation.target; import android.view.view; @target(elementtype.method) @retention(retentionpolicy.runtime) @eventbase(listenertype = view.onclicklistener.class, listenersetter = "setonclicklistener", methodname = "onclick") public @interface onclick { int[] value(); }
eventbase主要用于给onclick这类注解上添加注解,毕竟事件很多,并且设置监听器的名称,监听器的类型,调用的方法名都是固定的,对应上面代码的:
listenertype = view.onclicklistener.class, listenersetter = "setonclicklistener", methodname = "onclick"
@onclick({ r.id.id_btn, r.id.id_btn02 }) public void clickbtninvoked(view view)
如果你还记得,上篇博客我们的viewinjectutils.inject(this);里面已经有了两个方法,本篇多了一个:
public static void inject(activity activity) { injectcontentview(activity); injectviews(activity); injectevents(activity); }
(2)injectevents
/** * 注入所有的事件 * * @param activity */ private static void injectevents(activity activity) { class<? extends activity> clazz = activity.getclass(); method[] methods = clazz.getmethods(); //遍历所有的方法 for (method method : methods) { annotation[] annotations = method.getannotations(); //拿到方法上的所有的注解 for (annotation annotation : annotations) { class<? extends annotation> annotationtype = annotation .annotationtype(); //拿到注解上的注解 eventbase eventbaseannotation = annotationtype .getannotation(eventbase.class); //如果设置为eventbase if (eventbaseannotation != null) { //取出设置监听器的名称,监听器的类型,调用的方法名 string listenersetter = eventbaseannotation .listenersetter(); class<?> listenertype = eventbaseannotation.listenertype(); string methodname = eventbaseannotation.methodname(); try { //拿到onclick注解中的value方法 method amethod = annotationtype .getdeclaredmethod("value"); //取出所有的viewid int[] viewids = (int[]) amethod .invoke(annotation, null); //通过invocationhandler设置代理 dynamichandler handler = new dynamichandler(activity); handler.addmethod(methodname, method); object listener = proxy.newproxyinstance( listenertype.getclassloader(), new class<?>[] { listenertype }, handler); //遍历所有的view,设置事件 for (int viewid : viewids) { view view = activity.findviewbyid(viewid); method seteventlistenermethod = view.getclass() .getmethod(listenersetter, listenertype); seteventlistenermethod.invoke(view, listener); } } catch (exception e) { e.printstacktrace(); } } } } }
嗯,注释尽可能的详细了,主要就是遍历所有的方法,拿到该方法省的onclick注解,然后再拿到该注解上的eventbase注解,得到事件监听的需要调用的方法名,类型,和需要调用的方法的名称;通过proxy和invocationhandler得到监听器的代理对象,显示设置了方法,最后通过反射设置监听器。
这里有个难点,就是关于dynamichandler和proxy的出现,如果不理解没事,后面会详细讲解。
(3)dynamichandler
这里用到了一个类dynamichandler,就是invocationhandler的实现类:
package com.zhy.ioc.view; import java.lang.ref.weakreference; import java.lang.reflect.invocationhandler; import java.lang.reflect.method; import java.util.hashmap; public class dynamichandler implements invocationhandler { private weakreference<object> handlerref; private final hashmap<string, method> methodmap = new hashmap<string, method>( 1); public dynamichandler(object handler) { this.handlerref = new weakreference<object>(handler); } public void addmethod(string name, method method) { methodmap.put(name, method); } public object gethandler() { return handlerref.get(); } public void sethandler(object handler) { this.handlerref = new weakreference<object>(handler); } @override public object invoke(object proxy, method method, object[] args) throws throwable { object handler = handlerref.get(); if (handler != null) { string methodname = method.getname(); method = methodmap.get(methodname); if (method != null) { return method.invoke(handler, args); } } return null; } }
好了,代码就这么多,这样我们就实现了,我们事件的注入~~
效果图:
效果图其实没撒好贴的,都一样~~~
(3)关于代理
那么,本文结束了么,没有~~~关于以下几行代码,相信大家肯定有困惑,这几行干了什么?
//通过invocationhandler设置代理 dynamichandler handler = new dynamichandler(activity); handler.addmethod(methodname, method); object listener = proxy.newproxyinstance( listenertype.getclassloader(), new class<?>[] { listenertype }, handler);
invocationhandler和proxy成对出现,相信大家如果对java比较熟悉,肯定会想到java的动态代理~~~
关于invocationhandler和proxy的文章,大家可以参考:http://www.ibm.com/developerworks/cn/java/j-lo-proxy1/ ps:ibm的技术文章还是相当不错的,毕竟有人审核还有奖金~
但是我们的实现有一定的区别,我为什么说大家疑惑呢,比如反射实现:
mbtn2.setonclicklistener(this);这样的代码,难点在哪呢?
a、mbtn2的获取?so easy
b、调用setonclicklistener ? so easy
but , 这个 this,这个this是onclicklistener的实现类的实例,onclicklistener是个接口~~你的实现类怎么整,听说过反射newinstance对象的,但是你现在是接口!
是吧~现在应该明白上述几行代码做了什么了?实现了接口的一个代理对象,然后在代理类的invoke中,对接口的调用方法进行处理。
(4)代码是最好的老师
光说谁都理解不了,你在这xx什么呢??下面看代码,我们模拟实现这样一个情景:
main类中实现一个button,button有两个方法,一个setonclicklistener和onclick,当调用button的onclick时,触发的事件是main类中的click方法
涉及到4个类:
button
package com.zhy.invocationhandler; public class button { private onclicklistener listener; public void setonclicklisntener(onclicklistener listener) { this.listener = listener; } public void click() { if (listener != null) { listener.onclick(); } } }
onclicklistener接口
package com.zhy.invocationhandler; public interface onclicklistener { void onclick(); }
onclicklistenerhandler , invocationhandler的实现类
package com.zhy.invocationhandler; import java.lang.reflect.invocationhandler; import java.lang.reflect.method; import java.util.hashmap; import java.util.map; public class onclicklistenerhandler implements invocationhandler { private object targetobject; public onclicklistenerhandler(object object) { this.targetobject = object; } private map<string, method> methods = new hashmap<string, method>(); public void addmethod(string methodname, method method) { methods.put(methodname, method); } @override public object invoke(object proxy, method method, object[] args) throws throwable { string methodname = method.getname(); method realmethod = methods.get(methodname); return realmethod.invoke(targetobject, args); } }
我们的main
package com.zhy.invocationhandler; import java.lang.reflect.invocationtargetexception; import java.lang.reflect.method; import java.lang.reflect.proxy; public class main { private button button = new button(); public main() throws securityexception, illegalargumentexception, nosuchmethodexception, illegalaccessexception, invocationtargetexception { init(); } public void click() { system.out.println("button clicked!"); } public void init() throws securityexception, nosuchmethodexception, illegalargumentexception, illegalaccessexception, invocationtargetexception { onclicklistenerhandler h = new onclicklistenerhandler(this); method method = main.class.getmethod("click", null); h.addmethod("onclick", method); object clickproxy = proxy.newproxyinstance( onclicklistener.class.getclassloader(), new class<?>[] { onclicklistener.class }, h); method clickmethod = button.getclass().getmethod("setonclicklisntener", onclicklistener.class); clickmethod.invoke(button, clickproxy); } public static void main(string[] args) throws securityexception, illegalargumentexception, nosuchmethodexception, illegalaccessexception, invocationtargetexception { main main = new main(); main.button.click(); } }
我们模拟按钮点击:调用main.button.click(),实际执行的却是main的click方法。
看init中,我们首先初始化了一个onclicklistenerhandler,把main的当前实例传入,然后拿到main的click方法,添加到onclicklistenerhandler中的map中。
然后通过proxy.newproxyinstance拿到onclicklistener这个接口的一个代理,这样执行这个接口的所有的方法,都会去调用onclicklistenerhandler的invoke方法。
但是呢?onclicklistener毕竟是个接口,也没有方法体~~那咋办呢?这时候就到我们onclicklistenerhandler中的map中大展伸手了:
@override public object invoke(object proxy, method method, object[] args) throws throwable { string methodname = method.getname(); method realmethod = methods.get(methodname); return realmethod.invoke(targetobject, args); }
我们显示的把要执行的方法,通过键值对存到map里面了,等调用到invoke的时候,其实是通过传入的方法名,得到map中存储的方法,然后调用我们预设的方法~。
这样,大家应该明白了,其实就是通过proxy得到接口的一个代理,然后在invocationhandler中使用一个map预先设置方法,从而实现button的onclick,和main的click关联上。
现在看我们injectevents中的代码:
//通过invocationhandler设置代理 dynamichandler handler = new dynamichandler(activity); //往map添加方法 handler.addmethod(methodname, method); object listener = proxy.newproxyinstance( listenertype.getclassloader(), new class<?>[] { listenertype }, handler);
是不是和我们init中的类似~~
好了,关于如何把接口的回调和我们activity里面的方法关联上我们也解释完了~~~
注:部分代码参考了xutils这个框架,毕竟想很完善的实现一个完整的注入不是一两篇博客就可以搞定,但是核心和骨架已经实现了~~大家有兴趣的可以继续去完善~