IoC 在 Android 中的应用 Posted 2021-04-27 Android技术之家
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IoC 在 Android 中的应用相关的知识,希望对你有一定的参考价值。
控制反转 IoC(Inversion of Control) 意思是把创建对象的权利交给框架,是框架的重要特性,并非面向对象编程的专业术语。—-百度百科
1. IoC 简介
IoC 控制反转不是一种技术,而是一种编程思想,体现着“不要找我们,我们找你”的思想。
在传统的编程写法中,大多都是在类内部通过关键字 new
主动创建类的实例对象并赋给引用,但是这样写会造成类和类之间耦合度过高的后果,难于测试。有了 IoC 容器之后,创建对象和查找依赖的工作就交给框架完成,并由容器注入对象,这样通过 IoC 容器实现了类和类之间的解耦。
IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源。 —- 谈谈对Spring IOC的理解
提到 IoC 就不得不提到另一个概念:依赖注入(Dependence Injection,简称DI),DI 的概念是指组件之间的依赖关系由容器在运行期间决定。看完对 DI 的定义也许觉得很模糊,不清楚到底讲了什么意思。其实 IoC 和 DI 表达的是同一个意思,只不过是从不同的角度去描述。
1.1 IoC
在传统程序的编写过程中,在一个类中如果需要另外一个对象,就需要通过关键字 new
出另一个类的对象,然后才可以开始使用这个对象,使用完成之后,再将这个对象销毁,在这个过程中对象和对象之间耦合度很高。
有了 IoC 容器之后,所有的类都会在 IoC 容器中注册,并告诉 IoC 容器我是什么类,我需要什么对象,并且在运行到适当的时刻,将需要的对象创建并赋给我的引用,在适当的时刻再将该对象进行销毁。同时也会将创建我的对象,并赋给需要我的对象的类中。
在使用 IoC 容器框架编程时,对象的创建、销毁都是由 IoC 容器控制,而不是由引用它的对象控制。对于某个对象来说,以前是由它控制某个对象,现在是所有的对象都由 IoC 容器控制,所以叫控制反转。
1.2 DI
IoC 是指在程序运行期间,动态地向某个对象提供它所需要的对象,而 DI 就是实现这个思想的方法。比如,在 User 对象中需要使用到 Address 对象,我只需要告诉 IoC 容器,我需要一个 Address 对象,然后 IoC 容器会创建一个 Address 对象,然后像打针一样注入到 User 对象中,依赖注入就是这样来的。
在 Java 1.3 之后,可以通过反射在运行期间动态的创建对象、调用对象的方法、改变对象的属性,DI 就是通过反射实现注入的。
2. IoC 相关知识
在学习 IoC 之前,需要具备几点基础的 Java 知识,分别是注解、反射和动态代理,下面分别介绍。
2.1 注解
在平时写代码的时候,多多少少都会接触到注解,比如:@Override
、@Deprecated
等等,还有一些第三方库中也会有,比如 ButterKnife、EventBus、Dagger2 等。
用完之后只有一个感受:真是太方便了,其实注解并没有多么复杂,和写普通的类(Class)、接口(Interface)差不了多少。
首先,看一下我们平时使用注解的时候的代码,比如使用 ButterKnife 绑定一个控件或为一个控件添加点击事件的代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ExampleActivity extends Activity {
@BindView (R.id.user) EditText username;
@BindView (R.id.pass) EditText password;
@BindString (R.string.login_error) String loginErrorMessage;
@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...
}
}
那如果我们自己想写一个这样的注解,该怎么写呢?代码如下:
1
2
3
4
5
@Target (ElementType.FIELD)
@Retention (RetentionPolicy.RUNTIME)
public @interface BindView {
int value () ;
}
如果对注解不熟悉,看见上面的代码可能会感觉到懵逼,没关系,我们一一介绍其中的含义:
@Target(ElementType.FIELD)
:表示该注解的作用域,其取值有以下几个:
1
2
3
4
5
6
7
8
9
10
public enum ElementType {
TYPE, // 类、接口(包括注解类型)、枚举类,
FIELD, // 属性
METHOD, // 方法
PARAMETER, // 参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解
PACKAGE, // 包
}
@Retention(RetentionPolicy.RUNTIME)
:表示在什么级别保留此信息:
1
2
3
4
5
6
7
public enum RetentionPolicy {
SOURCE, // 源码注解,注解仅存在源码级别,在编译的时候丢弃该注解
CLASS, // 编译时注解,注解会在 class 文件中存在,但是在运行时会被丢弃
RUNTIME // 运行时注解,注解会被记录在 class 文件中,在 VM 运行时也会保留该注解,所以可以通过反射获得该注解
}
@interface
:表示此文件是一个注解,和 Class
、Interface
一个级别
int value();
:表示此注解的一个方法,可以通过注解的这个方法得到一个 int
注解值
2.2 反射
通俗的讲,反射就是把一个类、类的属性和类的方法当做对象一样来操作,在运行态的时候可以动态地创建对象、调用对象的方法、修改对象的属性。
比如现在有一个类,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.lijiankun24.iocpractice;
public class User {
private String name;
private int age;
public User (String name, int age) {
this .name = name;
this .age = age;
}
public String getName () {
return name;
}
public void setName (String name) {
this .name = name;
}
public int getAge () {
return age;
}
public void setAge (int age) {
this .age = age;
}
}
通过普通的 new
的方式创建一个 User 的对象,并使用它,这种方式应该都非常熟悉了,但是用反射的方式该怎么实现呢?可以通过如下代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class Main {
public static void main (String[] args) {
User user = newInstanceUser();
invokeMethod(user);
setField(user);
}
/**
* 通过反射创建一个 User 对象
*
* @return 返回该 User 对象
*/
private static User newInstanceUser () {
Class<?> clazz = User.class;
User user = null ;
try {
Constructor<?>[] constructors = clazz.getConstructors();
user = (User) constructors[0 ].newInstance("lijiankun" , 20 );
System.out.println("The name is " + user.getName());
} catch (SecurityException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return user;
}
/**
* 通过反射调用 User 中的方法
*
* @param user User 对象
*/
private static void invokeMethod (User user) {
Class<?> clazz = user.getClass();
try {
Method method = clazz.getDeclaredMethod("setName" , String.class);
method.invoke(user, "24" );
System.out.println("The name is " + user.getName());
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* 通过反射修改 User 对象中的私有属性
*
* @param user User 对象
*/
private static void setField (User user) {
Class<?> clazz = user.getClass();
try {S
Field field = clazz.getDeclaredField("name" );
field.setAccessible(true ); // 如果方法或者属性是私有的,则需要设置它的访问性为 true
field.set(user, "newName" );
System.out.println("The name is " + user.getName());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
上述代码简单讲了下通过反射怎么动态地创建对象、调用对象的方法、修改对象的属性,至于更详细的反射则需要看书去学习。
2.3 动态代理
代理模式的概念就是:为原对象提供一个代理对象,原对象和代理对象对外提供相同的 API 方法,客户通过代理对象间接地操作原对象,但是客户并不知道它操作的是代理对象。
如上图所示,简单示意了代理模式的概念,其中
Subject:表示一个接口类,有一个公共的方法 request()
RealObject:实现了 Subject
接口,向外提供 request()
方法,是原对象(我们把它称之为委托对象)
ProxyObject:也实现了 Subject
接口,向外提供 request()
方法,是代理对象
Client:表示一个客户对象,想要调用 RealObject
的 request()
方法,但是并不是直接调用 RealObject
的 request()
方法,而是通过 ProxyObject
的 request()
方法间接地调用 RealObject
的 request()
方法
代理的概念如上所述,但是有不同的实现方式,分为静态代理和动态代理:
动态代理
在 IoC 中会用到 Java 的动态代理,所以在这里也是重点讲一个 Java 动态代理。
在 Java 的动态代理中一定会涉及到一个接口和一个类,分别是:InvocationHandler(Interface)
和 Proxy(Class)
。下面我们通过一个例子,更加形象地学习 Java 的动态代理。
首先定义一个接口如下所示:
1
2
3
4
5
public interface Subject {
void request () ;
int add (int a, int b) ;
}
接着定义一个类实现这个接口,这个类就是我们的委托对象,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
public class RealObject implements Subject {
@Override
public void request () {
System.out.println("RealObject request invoked" );
}
@Override
public int add (int a, int b) {
return a + b;
}
}
然后有一个调用处理器的类,这个调用处理器的类需要实现 InvocationHandler
接口,代码如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ProxyHandler implements InvocationHandler {
private Subject mRealObject = null ;
public ProxyHandler (Subject realObject) {
mRealObject = realObject;
}
@Override
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("ProxyHandler PreProcess" );
method.invoke(mRealObject, args);
System.out.println("ProxyHandler PostProcess" );
return null ;
}
}
最后就是一个 Client 类
1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main (String[] args) {
Subject realObject = new RealObject();
InvocationHandler handler = new ProxyHandler(realObject);
Subject proxyObject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realObject.getClass().getInterfaces(), handler);
proxyObject.request();
}
}
通过上面的例子,我想大家对 Java 中的动态代理心里大概已经有概念,其实也不难,可以分为以下几个步骤:
首先需要有一个接口,其中定义了委托类和代理类都会实现的方法
定义委托类,实现该接口
定义调用处理器的类,实现 InvocationHandler
接口,InvocationHandler
的源码如下所示,其中只有一个方法,这个方法非常重要:
1
2
3
4
5
6
7
8
9
10
11
public interface InvocationHandler {
/**
*
* @param proxy 指委托对象
* @param method 指调用的委托对象的方法的对象
* @param args 指调用的委托对象的方法的参数
*/
public Object invoke (Object proxy, Method method, Object[] args)
throws Throwable;
}
通过 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
方法创建一个代理对象,就可以调用代理对象的方法了,如下所示:
1
2
3
Subject proxyObject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(),
realObject.getClass().getInterfaces(), handler);
proxyObject.request();
好了,以上就介绍了 IoC 在 android 中应用所需要用到的 Java 知识,下面就通过实现一个简单的 IoC 框架,更加深入地理解 IoC 在 Android 中的应用吧。
3. IoC 在 Android 中的应用
如果大家接触过 ButterKnife 框架的话,对下面这样的代码一定不陌生,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ExampleActivity extends Activity {
@BindView (R.id.user) EditText username;
@BindView (R.id.pass) EditText password;
@BindString (R.string.login_error) String loginErrorMessage;
@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...
}
}
在传统的写法中,我们要向获得在 xml 文件中声明的控件实例,需要通过 findViewById
来实现,但是 ButterKnife 只是用 @BindView(R.id.xxx)
就实现了同样的效果。同样的,ButterKnife 也用 OnClick(R.id.xxx)
为某个控件设置了监听回调事件,用 BindString(R.string.xxx)
实现了为某个 String 对象赋值,那我们也尝试着用 IoC 实现这样的一个框架。
3.1 注入布局文件
首先编写一个注解文件
1
2
3
4
5
@Target (ElementType.TYPE)
@Retention (RetentionPolicy.RUNTIME)
public @interface ContentView {
int value () ;
}
然后编写 ButterKnife 管理文件,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ButterKnife {
private static final String METHOD_SET_CONTENTVIEW = "setContentView" ;
public static void inject (Activity activity) {
injectContentView(activity);
}
private static void injectContentView (Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 在 Activity 类上查找 ContentView.class 的注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView != null ) {
// 得到 布局文件的 Id 值
int layoutId = contentView.value();
if (layoutId != -1 ) {
try {
// 通过反射调用 Activity 的 setContentView(int layoutId) 方法
Method method = clazz.getMethod(METHOD_SET_CONTENTVIEW, int .class);
method.setAccessible(true );
method.invoke(activity, layoutId);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
在 Activity
中就可以使用它注入布局文件了
1
2
3
4
5
6
7
8
9
@ContentView (R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate (Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
ButterKnife.inject(this );
}
}
3.2 注入 Views
首先编写注入 Views 的注解文件
1
2
3
4
5
@Target (ElementType.FIELD)
@Retention (RetentionPolicy.RUNTIME)
public @interface BindView {
int value () ;
}
在 ButterKnife
中同样的去实现这个注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ButterKnife {
private static final String METHOD_SET_CONTENTVIEW = "setContentView" ;
private static final String METHOD_FIND_VIEW_BY_ID = "findViewById" ;
public static void inject (Activity activity) {
injectContentView(activity);
injectView(activity);
}
......
private static void injectView (Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 首先获取到 Activity 中所有的属性
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 遍历查找每个属性上是否存在 BindView.class 注解
BindView viewInject = field.getAnnotation(BindView.class);
if (viewInject != null ) {
// 得到控件的 id 值
int viewId = viewInject.value();
if (viewId != -1 ) {
try {
// 通过反射调用 Activity 的 findViewById(int id) 方法
Method method = clazz.getMethod(METHOD_FIND_VIEW_BY_ID, int .class);
Object resView = method.invoke(activity, viewId);
// 并将得到的控件对象赋予 Activity 中的该属性
field.setAccessible(true );
field.set(activity, resView);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
}
然后在 Activity
中也可以使用这个注解
1
2
3
4
5
6
7
8
9
10
11
12
13
@ContentView (R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@BindView (R.id.tv1)
private TextView TV1;
@Override
protected void onCreate (Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
ButterKnife.inject(this );
TV1.setText("Change the text" );
}
}
3.3 注入 Views 的事件监听
和上面有点不同的是,为了扩展的方便,需要为监听注解编写一个注解,如下所示
1
2
3
4
5
6
7
8
9
@Target (ElementType.ANNOTATION_TYPE)
@Retention (RetentionPolicy.RUNTIME)
public @interface EventType {
Class<?> listenerType(); // 监听接口的类型
String listenerSetter () ; // 设置监听接口的 set 方法
String methodName () ; // 监听接口中的回调方法名称
}
然后就是编写事件监听注解
1
2
3
4
5
6
7
@Target (ElementType.METHOD)
@Retention (RetentionPolicy.RUNTIME)
@EventType (listenerType = View.OnClickListener.class, listenerSetter = "setOnClickListener" ,
methodName = "onClick" )
public @interface OnClick {
int [] value();
}
编写在 ButterKnife 中的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class ButterKnife {
private static final String METHOD_SET_CONTENTVIEW = "setContentView" ;
private static final String METHOD_FIND_VIEW_BY_ID = "findViewById" ;
public static void inject (Activity activity) {
injectContentView(activity);
injectView(activity);
injectEvent(activity);
}
......
private static void injectEvent (Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
// 得到 Activity 中所有的方法
Method[] methods = clazz.getMethods();
for (Method method : methods) {
// 遍历所有的方法,并得到方法上所有的注解对象
Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
// 遍历每一个方法的所有注解
Class<? extends Annotation> annotationType = annotation.annotationType();
EventType eventBase = annotationType.getAnnotation(EventType.class);
// 查找是否存在 EventType.class 注解
if (eventBase != null ) {
String listenerSetter = eventBase.listenerSetter();
String methodName = eventBase.methodName();
Class listenerType = eventBase.listenerType();
try {
// 通过反射调用注解的 value() 方法得到 viewIds 值
Method method1 = annotationType.getDeclaredMethod("value" );
int [] viewIds = (int []) method1.invoke(annotation, null );
// 通过 Java 动态代理的方式为每个 View 设置监听器
DynamicHandler handler = new DynamicHandler(activity);
handler.addMethod(methodName, method);
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader()
, new Class[]{listenerType}, handler);
for (int viewId : viewIds) {
View view = activity.findViewById(viewId);
Method method2 = view.getClass().getMethod(listenerSetter, listenerType);
method2.invoke(view, listener);
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
}
}
在上述注入事件监听的过程中,用到了 Java 的动态代理,所以需要额外的编写一个 InvocationHandler
调用处理器类,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class ButterKnifeDynamicHandler implements InvocationHandler {
private WeakReference<Object> mObjectWR = null ;
private final HashMap<String, Method> mMethodHashMap = new HashMap<>();
ButterKnifeDynamicHandler(Object object) {
mObjectWR = new WeakReference<>(object);
}
void addMethod (String methodName, Method method) {
mMethodHashMap.put(methodName, method);
}
@Override
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
Object handler = mObjectWR.get();
String name = method.getName();
method = mMethodHashMap.get(name);
if (handler != null && method != null ) {
return method.invoke(mObjectWR.get(), args);
}
return null ;
}
}
最后在 Activity
中使用它即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@ContentView (R.layout.activity_main)
public class MainActivity extends AppCompatActivity {
@BindView (R.id.tv1)
private TextView TV1;
@Override
protected void onCreate (Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
ButterKnife.inject(this );
TV1.setText("Change the text" );
}
@OnClick ({R.id.tv1, R.id.tv2})
public void onClick (View view) {
switch (view.getId()) {
case R.id.tv1:
Toast.makeText(MainActivity.this , "IOC For Test tv1" , Toast.LENGTH_SHORT).show();
break ;
case R.id.tv2:
Toast.makeText(MainActivity.this , "IOC For Test tv2" , Toast.LENGTH_SHORT).show();
break ;
}
}
}
这样,一个使用 IoC 思想实现的仿 ButterKnife 框架就实现了,上面涉及到的代码都在 Github 上面:IoCPractice.
代码链接 https://github.com/lijiankun24/IOCPractice
原文链接http://www.lijiankun24.com/IOC-%E5%9C%A8-Android-%E4%B8%AD%E7%9A%84%E5%BA%94%E7%94%A8/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
以上是关于IoC 在 Android 中的应用的主要内容,如果未能解决你的问题,请参考以下文章
Android - 片段中的联系人选择器
如何在android的片段中设置应用程序上下文?
片段中的Android按钮单击方法(崩溃)
片段中的Android webView显示空白页面
片段中的Android致命异常
Android片段中的问题:仍然单击上一个片段