使用註解打造本身的IOC框架

1、簡述

IoC和AOP可謂是後臺開發入門必學的知識(Spring相關),但這二者都僅僅只是概念而已,並不是具體技術實現,一樣的,Android也可使用IoC和AOP,以前已經寫過如何在Android開發中使用AOP了,有興趣的朋友能夠看我以前的博客(順便點個關注吧),因此,本文主題即是IoC。android

控制反轉(Inversion of Control,英文縮寫爲IoC)是框架的重要特徵,並不是面向對象編程的專用術語。它包括依賴注入(Dependency Injection,簡稱DI)和依賴查找(Dependency Lookup)。git

上述源至百度百科,對於第一次接觸IoC的人可能有些晦澀難懂,其實,通俗來說,就是原本我能夠作的事我如今不想作了,交給框架來作。舉個實際的例子,就是ButterKnife,它就是Android上IoC的典型,實現了控件的動態注入及點擊事件的綁定。因此,下面咱們就來打造一個相似ButterKnife的IoC框架吧。github

2、框架實現

下面是ButterKnife在GitHub上的代碼示例:編程

class ExampleActivity extends Activity {
  @BindView(R.id.user) EditText username;
  @BindView(R.id.pass) EditText password;

  @OnClick(R.id.submit) void submit() {
    // TODO call server...
  }

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}
複製代碼

它包含3部分:app

  • 控件注入使用@BindView註解
  • 點擊事件的綁定使用@OnClick註解
  • 在onCreate()方法中調用ButterKnife.bind(this)

因此,咱們要模仿ButterKnife,先從@BindView和@OnClick這兩個註解入手。框架

一、註解

注意,不論是控件注入仍是點擊事件綁定,都必須跟控件的id扯上關係,因此這兩個註解中都會有一個屬性用於表示控件的id。代碼以下:ide

// 控件注入註解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}

// 控件點擊事件註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClickView {
    int value();
}
複製代碼

由於我不想事件綁定的註解名爲OnClick,因此這裏的註解命名爲ClickView,效果同樣的。佈局

其中,BindView註解用於控件的注入,即類字段,因此其Target取值ElementType.FIELD,而ClickView註解用於控件的點擊事件綁定,即方法,因此其Target取值ElementType.METHOD;而且,這兩個註解都是在App運行期間被框架所使用,即運行時可見,因此,Retention取值爲RetentionPolicy.RUNTIME。這倆註解在編碼上的使用見以下代碼:測試

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.btn_hello)
    Button mBtnHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @ClickView(R.id.btn_hello)
    public void sayHello() {
        Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show();
    }
}
複製代碼

但這樣是不夠的,由於註解能夠認爲只是一個標記,是靜態的,它並無實現控件注入與事件綁定的功能,控件的獲取實際上仍是須要findViewById()來實現,而事件的綁定一樣也須要setOnClickListener()來實現,這也正是框架要爲咱們所作的工做。this

二、控件注入與事件綁定的實現

ButterKnife不是這麼實現的,這只是我我的的想法而已。

  1. 控件注入:其實是框架調用了activity的findViewById()方法拿到id對應的控件,再經過反射的方式,對控件(類字段)進行賦值。
  2. 事件綁定:實際上也是框架調用了activity的findViewById()方法拿到id對應的控件,再調用控件的setOnClickListener()設置控件的點擊事件,在這個點擊事件裏經過反射調用Activity中被ClickView註解的sayHello()方法而已。

下面就來動手實現它吧:

public class ViewUtil {

	public static void inject(final Activity activity) {
		// 拿到Activity的class對象
        Class clazz = activity.getClass();

        // 遍歷屬性
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
			// 找到有BindView註解的屬性
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null) {
                try {
					// 讓屬性可被訪問(若是屬性使用final和jprivate,則必須使其可訪問,不然如下操做會報錯)
					field.setAccessible(true);
					// 經過id獲取到View,再對屬性賦值
                    field.set(activity, activity.findViewById(bindView.value()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }

        // 遍歷方法
        Method[] methods = clazz.getDeclaredMethods();
        for (final Method method : methods) {
			// 找到有ClickView註解的方法
            ClickView clickView = method.getAnnotation(ClickView.class);
            if (clickView != null) {
				// 經過id獲取到View,再對view設置點擊事件
                activity.findViewById(clickView.value()).setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        try {
                            method.setAccessible(true);
                            // 調用這個被ClickView註解的方法
                            method.invoke(activity);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        }
    }
}
複製代碼

三、試試

功能既已實現,下來就來試試看,是否真的有效,在原先代碼的onCreate()方法中加上ViewUtil.inject(this):

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.btn_hello)
    Button mBtnHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewUtil.inject(this);
    }

    @ClickView(R.id.btn_hello)
    public void sayHello() {
        Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show();
    }
}
複製代碼

若是控件注入成功,則當點擊控件時,會吐司"hello"。

3、拓展

上面的測試很成功啊,不過,這個框架目前只能給Activity使用,而ButterKnife可不僅如此,無論Activity仍是Fragment都能通吃,因此,咱們這個框架也要適用於Fragment。

一、Activity與Fragment獲取控件的不一樣

不論是控件注入仍是事件綁定,都離不開最初始的一點,那就是控件的獲取,即findViewById()。Activity獲取控件只須要調用本身的findViewById()方法便可,但Fragment可不是這樣,先來看看Fragment是如何設置佈局的:

public class MyFragment extends Fragment {
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        if (mRootView == null) {
            mRootView = inflater.inflate(R.layout.fragment_my, null, false);
        }
        return mRootView;
    }
}
複製代碼

之因此Activity能夠調用本身的findViewById()方法來獲取控件,是由於Activity自己就是佈局,而Fragment則不是這樣的,Fragment的佈局是它本身的一個View(mRootView),因此要獲取Fragment中的控件,就必須調用mRootView的findViewById()方法來獲取。

二、代碼抽取

回顧ViewUtil的inject(Activity activity)方法,其實這個activity參數在這個方法中是擔任兩個角色的,一個是類(容器),另外一個是佈局。看成爲容器這個角色時,是爲了使用反射得到其中的字段和方法並賦值或執行。而做爲佈局這個角色時,是爲了經過id獲取佈局控件(findViewById)。再看看Fragment,是否是有點端倪了呢?Fragment就是容器角色,而它的mRootView則是佈局角色,因此,inject()的方法體能夠這麼抽:

private static Context context;
private static void injectReal(final Object container, Object rootView) {
    if (container instanceof Activity) {
        context = (Activity) container;
    } else if (container instanceof Fragment) {
        context = ((Fragment) container).getActivity();
    } else if (container instanceof android.app.Fragment) {
        context = ((android.app.Fragment) container).getActivity();
    }

    Class clazz = container.getClass();
    // 遍歷屬性
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields) {
        BindView bindView = field.getAnnotation(BindView.class);
        if (bindView != null) {
            try {
                field.setAccessible(true);
                field.set(container, findViewById(rootView, bindView.value()));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }

    // 遍歷方法
    Method[] methods = clazz.getDeclaredMethods();
    for (final Method method : methods) {
        ClickView clickView = method.getAnnotation(ClickView.class);
        if (clickView != null) {
            findViewById(rootView, clickView.value()).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    try {                            
                        method.setAccessible(true);
                        method.invoke(container);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

private static View findViewById(Object layout, int resId) {
    if (layout instanceof Activity) {
        return ((Activity) layout).findViewById(resId);
    } else if (layout instanceof View) {
        return ((View) layout).findViewById(resId);
    }
    return null;
}
複製代碼

如此抽取以後,Activity與Fragment對應的inject()方法就能夠共同使用這個injectReal()方法了:

// Activity
public static void inject(Activity activity) {
    injectReal(activity, activity);
}

// v4 Fragment
public static void inject(Fragment container, View rootView) {
    injectReal(container, rootView);
}

// app Fragment
public static void inject(android.app.Fragment container, View rootView) {
    injectReal(container, rootView);
}
複製代碼

至關清晰,並且是能夠成功的,這裏就不測試了。

最後貼下Demo地址

github.com/GitLqr/IocD…

相關文章
相關標籤/搜索