使用註解打造自己的IOC框架

GitLqr發表於2017-11-28

一、簡述

IoC和AOP可謂是後臺開發入門必學的知識(Spring相關),但這兩者都僅僅只是概念而已,並非具體技術實現,同樣的,Android也可以使用IoC和AOP,之前已經寫過如何在Android開發中使用AOP了,有興趣的朋友可以看我之前的部落格(順便點個關注吧),所以,本文主題便是IoC。

控制反轉(Inversion of Control,英文縮寫為IoC)是框架的重要特徵,並非物件導向程式設計的專用術語。它包括依賴注入(Dependency Injection,簡稱DI)和依賴查詢(Dependency Lookup)。

上述源至百度百科,對於第一次接觸IoC的人可能有些晦澀難懂,其實,通俗來講,就是本來我可以做的事我現在不想做了,交給框架來做。舉個實際的例子,就是ButterKnife,它就是Android上IoC的典型,實現了控制元件的動態注入及點選事件的繫結。所以,下面我們就來打造一個類似ButterKnife的IoC框架吧。

二、框架實現

下面是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部分:

  • 控制元件注入使用@BindView註解
  • 點選事件的繫結使用@OnClick註解
  • 在onCreate()方法中呼叫ButterKnife.bind(this)

所以,我們要模仿ButterKnife,先從@BindView和@OnClick這兩個註解入手。

1、註解

注意,不管是控制元件注入還是點選事件繫結,都必須跟控制元件的id扯上關係,所以這兩個註解中都會有一個屬性用於表示控制元件的id。程式碼如下:

// 控制元件注入註解
@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()來實現,這也正是框架要為我們所做的工作。

2、控制元件注入與事件繫結的實現

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();
                        }
                    }
                });
            }
        }
    }
}
複製程式碼

3、試試

功能既已實現,下來就來試試看,是否真的有效,在原先程式碼的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”。

使用註解打造自己的IOC框架

三、擴充

上面的測試很成功啊,不過,這個框架目前只能給Activity使用,而ButterKnife可不只如此,不管Activity還是Fragment都能通吃,所以,我們這個框架也要適用於Fragment。

1、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()方法來獲取。

2、程式碼抽取

回顧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…

相關文章