IoC 在 Android 中的应用

Posted 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:表示此文件是一个注解,和 ClassInterface 一个级别

  • 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() 方法

代理的概念如上所述,但是有不同的实现方式,分为静态代理和动态代理:

  • 静态代理:代理类在编译后就已经实现好了,也就是在编译之后,代理类就是一个 class 文件

  • 动态代理:代理类是在运行期间生成的。也就是说,在编译之后,并没有代理类对应的 class 文件,而是在运行期间动态的生成字节码文件,并加载到虚拟机中的。

动态代理

在 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片段中的问题:仍然单击上一个片段