高仿XUtils搭建Android ioc框架

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

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