一、簡述
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不是這麼實現的,這只是我個人的想法而已。
- 控制元件注入:實際上是框架呼叫了activity的findViewById()方法拿到id對應的控制元件,再通過反射的方式,對控制元件(類欄位)進行賦值。
- 事件繫結:實際上也是框架呼叫了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"。
三、擴充
上面的測試很成功啊,不過,這個框架目前只能給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);
}
複製程式碼
相當清晰,而且是可以成功的,這裡就不測試了。