IOC概述
IOC是一种设计原则,它的常见实现方式为DI。
- IOC——Inversion of Control,控制反转
- DI——Dependency Injection,依赖注入
通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。简单的说,不需要直接new出一个对象来进行赋值了,可以依赖一个系统生成对象,然后通过注入进行对象的赋值。依赖注入有如下实现方式:
- 基于接口——实现特定接口以供外部容器注入所依赖类型的对象。
- 基于 set 方法——实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
- 基于构造函数——实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
- 基于注解
本文的主要目的是讲解如何基于注解实现IOC,因此不会具体讲解什么是IOC。对于做过java spring的一般来说会比较熟悉,如果没有做过也没关系,具体可见:
注解的保留策略(Retention Policy)
注解的保留策略,我们也可以称为注解的生命周期。按照枚举类RetentionPolicy
的定义,可以将注解的保留策略划分为以下3种形式:
-
RetentionPolicy.SOURCE
——注解只保留在Java源文件,当Java源文件被编译器编译成class文件的时候,注解就会被遗弃,通常用于源码级别的检查性操作(比如@override
或者IDE的代码提示) -
RetentionPolicy.CLASS
——默认的生命周期,注解将会被保留到class文件,当class文件被JVM加载的时候被遗弃,通常会被用于编译时的注解处理(注解处理器) -
RetentionPolicy.RUNTIME
——注解始终存在,通常被用于运行时的注解处理(反射)
上述三种策略按照生命周期的长短可用下列表达式来说明:
- SOURCE < CLASS < RUNTIME
IOC——注解运行时(Runtime)处理
进入正题前,先说下我们运行时IOC的目标——实现Activity的成员变量view和string的依赖注入。那么实现该目标,我们将需要用到注解和发射。
对于注解和反射不熟悉的可详见以下Java官方指南文档:
自定义注解
自定义两个运行时注解——StringInject和ViewInject:
/**
* 用于字符串的运行时注入
* <ul>
* <li>编译完成后,在运行时,注解依然存在</li>
* </ul>
*
* @author xpleemoon
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface StringInject {
String value() default "IOC——运行时注解处理\n既要会用洋枪洋炮\n又要会造土枪土炮";
}
/**
* 用于view的运行时注入
* <ul>
* <li>编译完成后,在运行时,注解依然存在</li>
* </ul>
*
* @author xpleemoon
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface ViewInject {
@IdRes int id() default 0;
}
@Target(ElementType.FIELD)
限定了它们的使用范围——成员变量,同时注解的属性都有默认值——StringInject
的默认值为“IOC——运行时注解处理\n既要会用洋枪洋炮\n又要会造土”,ViewInject
的默认id
为0。
@Retention(RetentionPolicy.RUNTIME)
表明注解的保留策略为runtime,即注解的生命周期一直到runtime都存在。只有这样,才能在运行时通过反射的方式获取注解信息。
注解的运行时IOC处理
/**
* 运行时IOC注入工具,使用方式见下面代码:
* <pre><code>
* public class ExampleActivity extends Activity {
* {@literal @}ViewInject(R.id.title) TextView titleView;
*
* {@literal @}Override protected void onCreate(Bundle savedInstanceState) {
* super.onCreate(savedInstanceState);
* setContentView(R.layout.example_activity);
* InjectUtils.inject(this);
* }
* }
* </code></pre>
*
* @author xpleemoon
*/
final class InjectUtils {
private static void check(Activity activity) {
if (activity == null) {
throw new IllegalStateException("依赖注入的activity不能为null");
}
Window window = activity.getWindow();
if (window == null || window.getDecorView() == null) {
throw new IllegalStateException("依赖注入的activity未建立视图");
}
}
public static void inject(@NonNull Activity target) {
check(target);
Class<? extends Activity> targetClz = target.getClass();
Field[] fields = targetClz.getDeclaredFields(); // 获取target中的所有字段
for (Field field : fields) {
StringInject stringInject = field.getAnnotation(StringInject.class);
if (stringInject != null) {
String str = stringInject.value();
if (!TextUtils.isEmpty(str)) {
try {
field.setAccessible(true);
field.set(target, str); // 为StringInject修饰的字段注入字符串
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
continue;
}
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
int viewId = viewInject.id();
if (viewId > 0) {
try {
Method findViewByIdMethod = targetClz.getMethod("findViewById", int.class);
View view = (View) findViewByIdMethod.invoke(target, viewId); // 反射调用,获取view对象
field.setAccessible(true);
field.set(target, view); // 为ViewInject修饰的字段注入view
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
continue;
}
}
}
}
我们重点关注inject(target)方法
首先,
Field[] fields = targetClz.getDeclaredFields()
,通过反射获取target类的所有成员变量作为数组-
其次,for循环遍历数组
fields
数组,分别解析StringInject
和ViewInject
注解。我们以ViewInject
注解的解析为例:-
ViewInject viewInject = field.getAnnotation(ViewInject.class)
,通过反射尝试去获取成员变量的注解,如果viewInject
不为null
,那么表明field
是被ViewInject
注解修饰的 -
int viewId = viewInject.id()
,获取注解声明的id值 -
View view = (View) findViewByIdMethod.invoke(target, viewId)
,反射调用findViewById(id)
方法,获取view
对象 -
field.set(target, view)
,通过反射为ViewInject
修饰的字段注入view
-
说白了,注解的运行时IOC处理就是反射:通过反射获取对应元素,然后获取元素上面的注解,接着得到注解的属性值,最后把得到的属性值通过反射set到注解修饰的目标上。
注解的使用
/**
* 注意,当前类中使用的注解和{@link com.xpleemoon.annotations.demo.annotationprocess.compile.CompileIOCActivity}的不一致.</br>
* 当前使用的是runtime包下的
*
* @author xpleemoon
*/
public class RuntimeIOCActivity extends AppCompatActivity {
/**
* 使用默认值注入
*/
@StringInject
String mTextStr;
@StringInject("运行时注解处理,IOC就这么简单")
String mToastStr;
@ViewInject(id = R.id.runtime_inject_text)
private TextView mText;
@ViewInject(id = R.id.runtime_inject_button)
private Button mBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_runtime_ioc);
InjectUtils.inject(this);
mText.setText(mTextStr);
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getApplicationContext(), mToastStr, Toast.LENGTH_LONG).show();
}
});
}
}
很简单,就是对类型为View
或者String
的成员变量声明一下注解,然后调用一下InjectUtils.inject(this)
,这样就为注解修饰的变量完成了数据注入。于是乎,我们就可以愉快的使用这些变量了,比如mText.setText(mTextStr)
:
-
mText
和mTextStr
在RuntimeIOCActivity
都是没有经过直接赋值的,它们的赋值通过控制反转的形式交给了InjectUtils.inject(this)
点击源码,该份源码中既有运行时和编译时的处理。当前文章所讲的只需要关注app module下的runtime包即可。