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
下面是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
因此,咱們要模仿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不是這麼實現的,這只是我我的的想法而已。
下面就來動手實現它吧:
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"。
上面的測試很成功啊,不過,這個框架目前只能給Activity使用,而ButterKnife可不僅如此,無論Activity仍是Fragment都能通吃,因此,咱們這個框架也要適用於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);
}
複製代碼
至關清晰,並且是能夠成功的,這裏就不測試了。