什麼是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等