IOC技術-運行時注入(View注入,Event注入的原理)

什麼是IOC?

官方定義:IOC是原來由程序中主動獲取的資源,轉變爲由第三方提供,並有原來的程序被動接受的方式,已達到解耦的效果.

當然,這種以專業名詞解釋專業名詞的方式,看了還是一頭霧水.

直白點說:Inversion of control ,直接翻譯過來就是控制翻轉,這個理論主要用於實現對象之間的解耦.

也叫依賴注入,就是IOC容器在運行過程中,動態的將某種依賴關係注入到對象之中.

比如說對象A依賴對象B,通常我們使用new關鍵字來實現兩個對象之間組合,這樣耦合性就很高,引入IOC之後,當A對象需要B對象時,IOC容器就會創建一個對象B送給對象A,而A對象不需要去關心B對象是怎麼創建的,該怎麼銷燬,這些都由IOC去完成.


IOC最核心的技術就是反射.

Android中有不少應用了IOC技術的框架,如xUtils,EventBus.

這裏不是介紹那些框架怎麼使用, 而是以一種簡單的方式,來理解這些的框架的思想.理解了它的思想,不管是去使用,還是去開發新的框架,都會更容易打開思路.

下面結合代碼,來看運行時注入,在android中是怎麼使用的.

首先,看View的注入,也包括layout,contentView的注入,代碼中加了註釋,應該很容易看明白,

//定義一個類似IOC容器的類,這個類通常是一個基類中使用,比如在BaseActivity中使用,
// 那麼所以繼承自BaseActivity的類都具有了這個功能

//這個工具類中實現了,怎樣去注入一個view,及注入佈局資源。

public class IOCInjectUtils {
    public static void inject(Object context) {
        injectLayout(context);
        injectView(context);
    }

    private static void injectLayout(Object context) {
        //通過反射去調用MainActivity的setContentView。
        int layoutId = 0;
        Class<?> mainActivityClass = context.getClass();
        //通過MainActivity上的註解,拿到佈局id
        InjectContentView injectContentView = mainActivityClass.getAnnotation(InjectContentView.class);
        if (null != injectContentView) {
            layoutId = injectContentView.value();
            //反射調用setContentView
            try {
                Method method = mainActivityClass.getMethod("setContentView", int.class);
                method.invoke(context, layoutId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static void injectView(Object context) {
        Class<?> mainActivityClass = context.getClass();
        //拿到MainActivity中所有的屬性變量
        Field[] fields = mainActivityClass.getDeclaredFields();
        for (Field field : fields) {
            //找到被InjectViewComponent註解過的組件
            InjecViewComponent injecViewComponent = field.getAnnotation(InjecViewComponent.class);
            if (null != injecViewComponent) {
                int componentId = injecViewComponent.value();
                try {
                    //通過反射調用findViewById爲組件賦值
                    Method method = mainActivityClass.getMethod("findViewById", int.class);
                    View view = (View) method.invoke(context, componentId);
                    field.setAccessible(true);
                    //修改MainActivity中控件的值爲view對象。
                    field.set(context, view);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

這個工具類,在基類中使用。

public class IOCBaseActivity  extends AppCompatActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        IOCInjectUtils.inject(this);
    }
}

MainActivity繼承使用了注入工具的基礎類,

//資源文件的注入
@InjectContentView(R.layout.activity_main)
public class MainActivity extends IOCBaseActivity {

   // 控件的注入
    @InjecViewComponent(R.id.button)
    Button btn;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //通過注入實現佈局資源設置
        //setContentView(R.layout.activity_main);

        // 控件的注入
        btn.setText("IOC注入");
    }
}

對佈局文件的註解,要寫在類上的,對Button view的註解寫在具體控件上,

這裏定義了兩個註解,InjectContentView用來注入佈局文件,InjectViewComponent用來注入view

@Target(ElementType.TYPE)//註解應用在什麼地方,
@Retention(RetentionPolicy.RUNTIME)//註解的生命週期
public @interface InjectContentView {
    int value();
}
@Target(ElementType.FIELD)//註解應用在什麼地方,
@Retention(RetentionPolicy.RUNTIME)//註解的生命週期
public @interface InjecViewComponent {
    int value();
}

接着,看下對事件的注入是怎麼實現的.

先看常規的事件處理代碼:

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //
            }
        });

這段代碼中,有幾個關鍵因素必不可少,一,btn對象,事件作用對象,二,onClickListener這是一個什麼類型的事件,三,onClick,具體處理事件的函數,四,setOnClickListener,綁定事件訂閱關係的方法.

在事件注入時,這三個關鍵因素都是會變化的,所以,針對事件的註解的定義,要考慮到這幾個因素.

這裏用到了註解的多態,我們先定義一個基礎的註解,讓他作用在別的註解上面,也即是寫在具體的事件註解類上面.

@Target(ElementType.ANNOTATION_TYPE)//註解應用在什麼地方,
@Retention(RetentionPolicy.RUNTIME)//註解的生命週期
public @interface InjectBaseEvent {
    String listenerSetter();//表示訂閱關係
    Class<?> listenerType();//表示事件本身
    String callbackMethod();//表示事件處理的方法名
}

定義一個針對事件類型的註解,基礎的註解,可以作用在這個註解上,並且給事件相關的因素賦值。

//可能會有多個控件,同時應用了這樣的事件注入,所以value屬性定義成了數組。
@Target(ElementType.METHOD)//註解應用在什麼地方,
@Retention(RetentionPolicy.RUNTIME)//註解的生命週期
@InjectBaseEvent( listenerSetter = "setOnClickListener",
                listenerType = View.OnClickListener.class,
                callbackMethod = "onClick")
public @interface InjectOnClick {
    int[] value() default -1;
}

在MainActivity中的使用方式,這裏可以填入多個控件的id,

    @InjectOnClick(R.id.button)
    public void injectClickEvent(View view) {

    }

如果要添加對長按的注入,可以仿照InjectOnClick註解,同時修改相應的事件因素,這樣就很容易增加多種類型的事件注入。這個做法就是註解的多態。

最後,看下在注入工具類中,是怎麼實現事件注入的。我把註釋加在了代碼中,跟隨代碼更容易說清楚。

public class IOCInjectUtils {
    public static void inject(Object context) {
        //injectLayout(context);
        //injectView(context);
        injectEvent(context);
    }

    private static void injectEvent(Object context) {
        Class<?> mainActivityClass = context.getClass();
        //拿到所有的方法
        Method[] methods = mainActivityClass.getDeclaredMethods();
        for (Method method : methods) {
            //方法上所有的註解
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                //拿到註解對應的class,來判斷它是不是一個需要處理事件的方法
                Class<?> annotationClass = annotation.annotationType();
                InjectBaseEvent annotationBaseEvent = annotationClass.getAnnotation(InjectBaseEvent.class);
                //如果沒有InjectBaseEvent註解,說明它不是一個需要處理事件的方法。
                if (null == annotationBaseEvent) {
                    continue;
                }
                //否則獲取事件處理的因素,確定是那種事件
                String listenerSetter = annotationBaseEvent.listenerSetter();
                Class<?> listenerType = annotationBaseEvent.listenerType();
                String callbackMethod = annotationBaseEvent.callbackMethod();

                Method value = null;
                try {
                    //拿到註解(如InjectOnClick註解)中的value方法,
                    // 是爲了拿到value中都有哪些控件ID,需要應用這個註解。
                    value = annotationClass.getDeclaredMethod("value");
                    //獲取控件id
                    int[] viewIds = (int[])value.invoke(annotation);
                    for (int viewId : viewIds) {
                        //拿到id對應的控件對象, 如Button btn
                        Method findViewById = mainActivityClass.getMethod("findViewById", int.class);
                        View view = (View) findViewById.invoke(context,viewId);
                        //下面就可以執行控件對象身上的事件處理方法了,如onClick方法,
                        // 但是因爲onClick中,具體的業務代碼是未知的,不確定的,
                        // 所以這裏用到了動態代理,去執行程序員定義的業務代碼,
                        // 讓這個動態代理去告訴MainActivity,有事件(如onClick)被執行了
                        if (null == view) {
                            continue;
                        }
                        //現在讓動態代理跟事件處理的方法(如onClick)綁定起來,
                        // 然後讓代理類去執行MainActivity中真實的業務處理方法,如果injectClickEvent
                        ListenerInvocationHandler listenerInvocationHandler =
                                new ListenerInvocationHandler(context, method);
                        //生成代理對象,代理對象,代理的是View.OnClickListener,
                        // 執行的它的onClick方法
                        Object proxyInstance = Proxy.newProxyInstance(listenerType.getClassLoader(),
                                new Class[]{listenerType}, listenerInvocationHandler);
                        //讓proxy去執行View上的onClick方法。
                        //viewMethod拿到的是button控件上的setOnClickListener方法,
                        Method viewMethod = view.getClass().getMethod(listenerSetter, listenerType);
                        //執行控件button的setOnClickListener方法,
                        // 然後由proxy執行MainActivity中使用事件註解的方法,
                        // 也就是MainActivity中自定義的injectClickEvent方法
                        viewMethod.invoke(view, proxyInstance);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

下面是動態代理類的實現代碼:

//讓這個代理類去實現 事件本身定義的接口,如View.OnClicklistener,
// 執行接口中方法,也即是事件處理中的方法,如onClick
//在事件處理方法中(onClick),實際要執行的是MainActivity中的injectClickEvent這個自定義的方法,
// 也即是添加了註解@InjectOnClick(R.id.button)
public class ListenerInvocationHandler implements InvocationHandler {
    //要執行的是那個類中的那個方法,如果MainActivity中的injectClickEvent方法,
    private Object activity;
    private Method methodInActivity;

    public ListenerInvocationHandler(Object activity, Method methodInActivity) {
        this.activity = activity;
        this.methodInActivity = methodInActivity;
    }

    //這裏要執行的實際是事件處理方法,如onClick,也即是說,當點擊button時,
    // framework層執行的onClick方法將會跳轉到這裏來,
    // 所以我們的注入框架中,會去處理invoke和onClick的關聯
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //這裏去調用真實要去執行的方法,如MainActivity中的injectClickEvent。
        return methodInActivity.invoke(activity, args);
    }
}

到這裏就完成了了事件注入的代碼實現。

 

IOC概念的理解,參考博文:

https://www.cnblogs.com/DebugLZQ/archive/2013/06/05/3107957.html

https://segmentfault.com/a/1190000014803412

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章