Android應用開發中控制反轉IoC設計模式使用教程

Android應用開發中控制反轉IoC設計模式使用教程
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

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); 
} 

效果圖:

https://codertw.com/wp-content/uploads/2018/07/20180703134749-5b3b7e85de671.gif (382×658)

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是用於寫在Activity的某個方法上的:


@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; 
} 
} 

好了,程式碼就這麼多,這樣我們就實現了,我們事件的注入~~
效果圖:

https://codertw.com/wp-content/uploads/2018/07/20180703134750-5b3b7e8615824.gif (382×658)

效果圖其實沒撒好貼的,都一樣~~~
(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這個框架,畢竟想很完善的實現一個完整的注入不是一兩篇部落格就可以搞定,但是核心和骨架已經實現了~~大家有興趣的可以繼續去完善~

您可能感興趣的文章:

android設計模式之單例模式詳解Android開發中的MVC設計模式淺析基於Android設計模式之–SDK原始碼之策略模式的詳解Android設計模式之介面卡(Adapter)模式Android 單例模式 Singleton 簡單例項設計模式解析Android設計模式系列之組合模式Android設計模式系列之工廠方法模式Android中的設計模式android開發設計模式之——單例模式詳解Android設計模式之代理模式Proxy淺顯易懂的詳細說明

相關文章

Android 開發 最新文章