在Android中使用IOC框架讓程式碼更清爽
控制反轉(Inversion of Control,英文縮寫為IoC)是一個重要的面向對角程式設計的法則來削減計算機程式的耦合問題,也是輕量級的Spring框架的核心。 控制反轉一般分為兩種型別,依賴注入(Dependency Injection,簡稱DI)和依賴查詢(Dependency Lookup)。依賴注入應用比較廣泛。
通俗點講什麼是IOC呢?
很久很久以前,我們建立某件物品都是用雙手在流水線上建立的,當我們有了機器後,這個機器就替代了人,幫助人創造物品,這個過程倒置了反轉。在Android中,你獲取控制元件,都需要自己手動獲取,那麼反轉過來顧名思義就是程式自動獲取控制元件。
相信在系統學習過Java Web的Spring框架的人對IOC應該一點也不陌生,那你們知道在Android中怎麼應用嗎,這可能設及的知識有點多,包括反射,Java註解類,設計模式之代理模式等知識。下面我將演示怎麼執行IOC來獲取控制元件,設定回撥方法的應用。
1.在Android中獲取控制元件
我們知道,Android手機介面的所有佈局都配置在資原始檔XML中,如果想要獲取某個佈局,一般情況下,我們是使用如下方法獲取的:
setContentView(R.layout.activity_main);
現在我們來顛覆你對這個的應用,下面來看看應用IOC框架是怎麼獲取這個應用的。
①首先我們建立我們的註解類ContentView。
在Android Studio的建立流程如下:
㈠點選包名建立類
㈡選中註解類並建立
㈢與一個方法,獲取等下傳進來的ID
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ContentView { int value(); }
@Target表示註解可以用於什麼地方,這裡用於類,其他引數如:FILED(成員),METHOD(方法),PACKAGE(包),ANNOTATION_TYPE(註解的註解),CONSTRUCTOR(建構函式),PARAMETER(引數),LOCAL_VARIABLE(區域性變更)
@Retention什麼時候載入註解類,一般都是RetentionPolicy.RUNTIME執行的時候。
②新建一個注入工具類InjectUtils.class:
public class InjectUtils { public static final String ACTIVITY_MAIN_CONTENTVIEW="setContentView"; /** * 注入所有 * @param activity */ public static void injectAll(Activity activity){ injectContentView(activity); } /** * 注入ContentView * @param activity */ public static void injectContentView(Activity activity){ Class<? extends Activity> clazz = activity.getClass();//獲取該類資訊 ContentView contentView = clazz.getAnnotation(ContentView.class);//獲取該類ContentView的註解 //如果有註解 if(contentView!=null){ int viewId=contentView.value();//獲取註解類引數 try { Method method=clazz.getMethod(ACTIVITY_MAIN_CONTENTVIEW,int.class);//獲取該方法的資訊 method.setAccessible(true);//獲取該方法的訪問許可權 method.invoke(activity,viewId);//呼叫該方法的,並設定該方法引數 } catch (Exception e) { e.printStackTrace(); } } } }
註解雖然詳細,有可能不寫點,有的人理解method.invoke有點困難。下面我來解釋一下。
method.invoke(activity,viewId);你可能這樣理解,activity呼叫了method設定了viewID,如果還不夠形象,看方法下面:
setContentView.invoke(MainActivity.this,R.layout.activity_main)這樣雖然不倫不類,但是你應該理解了,就是invoke的第一個引數呼叫method方法,該方法的引數就是invoke的第二個引數。
獲取註解類的引數,如下程式碼中:
@ContentView(R.layout.activity_main) public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); InjectUtils.injectAll(MainActivity.this); } }
@ContentView(R.layout.activity_main)中的R.layout.activity_main的值。然後用呼叫工具類呼叫設定其setContentView方法,這樣佈局檔案就載入進來了。
執行後得到如下圖所示:
2.用IOC載入控制元件
相信大家載入控制元件的方法,基本上都是千篇一律,如下所示:
this.but=(Button)findViewById(R.id.but);
假如現在有N個控制元件,估計你整個螢幕都被控制元件包圍,寫其他程式碼還需要滑動滾動條。這樣是不是重複勞動是不是不值得?
下面我們來使用IOC框架來獲取控制元件。
①建立註解類InjectControl,程式碼如下:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface InjectControl { int value(); }
相信經過上面的介紹,這個很容易理解。
②在MainActivity配置註解類。
程式碼如下:
@InjectControl(value = R.id.content) private TextView content; @InjectControl(value = R.id.myBut1) private Button myBut1; @InjectControl(value = R.id.myBut2) private Button myBut2;
③最後就是注入所有控制元件。
程式碼在InjectUtils中,如下:
public static final String ACTIVITY_MAIN_FINDVIEWBYID="findViewById"; /** *注入控制元件 * @param activity */ public static void injectControl(Activity activity){ Class<? extends Activity> clazz = activity.getClass();//獲取該類資訊 Field[] fields=clazz.getDeclaredFields();//獲致所有成員變更 for (Field field:fields) { InjectControl injectControl = field.getAnnotation(InjectControl.class); if(injectControl!=null){ int viewId=injectControl.value(); try { Method method=clazz.getMethod(ACTIVITY_MAIN_FINDVIEWBYID, int.class); method.setAccessible(true); field.setAccessible(true); Object object=method.invoke(activity, viewId); field.set(activity,object); } catch (Exception e) { e.printStackTrace(); } } } }
這裡幾點需要解釋一下,為什麼用getDeclaredFields()獲取成員變更,不用getFields(),因為getFields只能獲取類中公有的成員變數,如果要獲取包括私有的成員變數,就需要使用上面的方法,而我們定義的都是私有,故必須使用該方法,
這裡呼叫了再次invoke的方法,一次是方法呼叫的,一次是成員變數呼叫的,為什麼這樣設計,因為,這裡必須用二步才能完成。
我們就算直接呼叫findViewById也是兩步,首先我們要呼叫this.findViewById()等到View後才賦值給某個變數,雖然看上去只有一條語句,但是卻是兩個部分。同理,我們必須呼叫activity.findViewById()方法獲取到View後,才能把View賦值給field。所以這裡有兩步。
為了驗證程式碼,我在MainActivity的onCreate()方法裡面加入瞭如下程式碼:
if(myBut1!=null && myBut2!=null){ Toast.makeText(this,"按鈕已經獲取到了",Toast.LENGTH_SHORT).show(); }
執行結果如下:
其他的上面的解釋了,這裡就不在敘述了。下面難點來了。
3.給按鈕設定點選監聽事件
平常我們給按鈕設定監聽事件的程式碼如下:
this.myBut2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } });
馬上顛覆你的所見,步驟如下:
①建立註解類InjectOnClick。
程式碼如下:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface InjectOnClick { int[] value(); }
你會發現,少了什麼對嗎?沒錯,介面型別,需要實現的方法,以及要呼叫設定的方法,這就需要下面第二步。
②設定註解類的註解類OnClickEvent。
程式碼如下:
@Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface OnClickEvent { Class<?> listenerType();//介面型別 String listenerSetter();//設定的方法 String methodName();//接口裡面要實現的方法 }
修改InjectOnClick:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @OnClickEvent(listenerType = View.OnClickListener.class,listenerSetter = "setOnClickListener",methodName = "onClick") public @interface InjectOnClick { int[] value(); }
③在MainActivity裡設定註解。
程式碼如下:
@InjectOnClick({R.id.myBut1,R.id.myBut2}) public void setButtonOnClickListener(View view){ switch (view.getId()){ case R.id.myBut1: Toast.makeText(this,"你好",Toast.LENGTH_SHORT).show(); break; case R.id.myBut2: Toast.makeText(this,"我很帥",Toast.LENGTH_SHORT).show(); break; default: break; } }
④注入點選事件
程式碼在InjectUtils中,如下:
/** * 注入點選事件 * @param activity */ public static void injectOnClickListener(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();//獲取具體的註解類 OnClickEvent onClickEvent = annotationType.getAnnotation(OnClickEvent.class);//取出註解的onClickEvent註解 if(onClickEvent!=null){//如果不為空 try { Method valueMethod=annotationType.getDeclaredMethod("value");//獲取註解InjectOnClick的value方法 int[] viewIds= (int[]) valueMethod.invoke(annotation,null);//獲取控制元件值 Class<?> listenerType = onClickEvent.listenerType();//獲取介面型別 String listenerSetter = onClickEvent.listenerSetter();//獲取set方法 String methodName = onClickEvent.methodName();//獲取介面需要實現的方法 MyInvocationHandler handler=new MyInvocationHandler(activity);//自己實現的程式碼,負責呼叫 handler.setMethodMap(methodName,method);//設定方法及設定方法 Object object= Proxy.newProxyInstance(listenerType.getClassLoader(),new Class<?>[]{listenerType},handler);//建立動態代理物件類 for (int viewId:viewIds){//遍歷要設定監聽的控制元件 View view=activity.findViewById(viewId);//獲取該控制元件 Method m=view.getClass().getMethod(listenerSetter, listenerType);//獲取方法 m.invoke(view,object);//呼叫方法 } } catch (Exception e) { e.printStackTrace(); } } } } }
雖然註解很詳細,但還是有必要說明一下:
㈠關於因為我們用的是Java的動態代理,每一個動態代理類都必須要實現InvocationHandler這個介面,並且每個代理類的例項都關聯到了一個handler,當我們通過代理物件呼叫一個方法的時候,這個方法的呼叫就會被轉發為由InvocationHandler這個介面的 invoke 方法來進行呼叫。我們來看看InvocationHandler這個介面的唯一一個方法invoke方法:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
proxy: 指代我們所代理的那個真實物件
method: 指代的是我們所要呼叫真實物件的某個方法的Method物件
args: 指代的是呼叫真實物件某個方法時接受的引數
㈡Proxy這個類的作用就是用來動態建立一個代理物件的類,它提供了許多的方法,但是我們用的最多的就是newProxyInstance這個方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
loader: 一個ClassLoader物件,定義了由哪個ClassLoader物件來對生成的代理物件進行載入
interfaces: 一個Interface物件的陣列,表示的是我將要給我需要代理的物件提供一組什麼介面,如果我提供了一組介面給它,那麼這個代理物件就宣稱實現了該介面(多型),這樣我就能呼叫這組介面中的方法了
h: 一個InvocationHandler物件,表示的是當我這個動態代理物件在呼叫方法的時候,會關聯到哪一個InvocationHandler物件上
㈢getFields()與getDeclaredFields()區別:getFields()只能訪問類中宣告為公有的欄位,私有的欄位它無法訪問,能訪問從其它類繼承來的公有方法.getDeclaredFields()能訪問類中所有的欄位,與public,private,protect無關,不能訪問從其它類繼承來的方法
getMethods()與getDeclaredMethods()區別:getMethods()只能訪問類中宣告為公有的方法,私有的方法它無法訪問,能訪問從其它類繼承來的公有方法.getDeclaredFields()能訪問類中所有的欄位,與public,private,protect無關,不能訪問從其它類繼承來的方法
我們實現的InvocationHandler介面,程式碼如下:
public class MyInvocationHandler implements InvocationHandler { private Object object; private Map<String, Method> methodMap = new HashMap<String, Method>(1); public MyInvocationHandler(Object object) { this.object = object; } public void setMethodMap(String name, Method method) { this.methodMap.put(name, method); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (object != null) { String name = method.getName(); method= this.methodMap.get(name); if (method != null) { return method.invoke(object, args); } } return null; } }
這就不用我過多的註釋了,因為,大部分程式碼與前面的並無不同,代理顧名思義就是幫你呼叫一些方法。
其程式碼難點就在這個代理裡面,其他的程式碼與上面獲取佈局,控制元件一樣,唯一不同的是這個程式碼用到了註釋的註釋,巢狀層級有點多,所以用到了許兩層For迴圈,時間複雜度可能增加一個數量級,但這在手機上也是可以忽略不計的。
把方法加入InjectUtils的injectAll中。
/** * 注入所有 * @param activity */ public static void injectAll(Activity activity){ injectContentView(activity); injectControl(activity); injectOnClickListener(activity); }
執行效果如下:
4.看一下程式碼有多簡潔
程式碼如下:
@ContentView(R.layout.activity_main) public class MainActivity extends AppCompatActivity { @InjectControl(value = R.id.content) private TextView content; @InjectControl(value = R.id.myBut1) private Button myBut1; @InjectControl(value = R.id.myBut2) private Button myBut2; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); InjectUtils.injectAll(MainActivity.this); } @InjectOnClick({R.id.myBut1,R.id.myBut2}) public void setButtonOnClickListener(View view){ switch (view.getId()){ case R.id.myBut1: Toast.makeText(this,"你好",Toast.LENGTH_SHORT).show(); break; case R.id.myBut2: Toast.makeText(this,"我很帥",Toast.LENGTH_SHORT).show(); break; default: break; } } }
可能有的人會問,我用一句findViewById或者一句setOnClickListener就可以解決的事,沒事給自己找這複雜的程式碼寫,我是不是有病啊。可是你忽略了一點,一但我將這個程式碼打包,這程式碼就可能複用到我所有的專案中,如果,你只開發小專案或一個專案,這樣寫確實不划算,但是如果你總是換專案就算不換,增加新介面的時候,總是需要寫findViewById,那麼當累計的一定的數量時,這樣寫是一定節省很多時間的。