控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,其中最常见的就是依赖注入(Dependency Injection, 简称DI)
现在市面上越来越多的开源框架使用了依赖注入技术。什么是依赖注入呢?其实就是使用注解的方式去实现某些功能。
比如:
1.运行时注入 xUtils,eventBus,springMVC
2.源码时注入 android studio插件
3.编译时注入 butterknife,dagger2
IOC是原来由程序代码中主动获取的资源,转变由第三方获取并使原来的代码被动接收的方式,以达到解耦的效果,称为控制反转
怎么理解呢?如上图,当我们还是一个单身狗时,出门的时候要自己去拿衣服,让后自己穿上才出门。自从有了女朋友,就再也不用这么麻烦了,让女朋友帮你拿衣服和穿衣服,完事直接出门就可以了。我们就相当于被动接收,女朋友做的就是第三方获取并使用原来的代码的一些行为。
在手撸代码之前,我们要先了注解注入的三种形式
运行时注入
运行时注入就是我们先在代码中定义好一个行为轨迹,程序运行时按照我们的轨迹来执行。今天我们主要使用运行时注入来实现一个ButterKnife框架(运行时注入性能欠佳,现在ButterKnife使用的是编译时注入的方式,不过为了更好的理解依赖注入,这里使用运行时注入来实现)。
源码注入
源码注入可以理解为as插件,比如gson format,让插件为我们生成一系列的需要的代码。
编译时注入
编译时注入就是在程序编译时生成一些列的代码,比如ARouter,GreenDao等框架,其实就是利用gradle插
件来hook住编译时的生命周期,处理我们需要的一些事情。
我么先拿最常见的Override注解来看
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
可以看到注解类上面有两个标记Target
和Retention
@Target
在定义注解类时候,需要标记个Target注解,里面传入一个ElementType枚举类,这个注解是为了标明我们定义的注解在什么地方使用。
public enum ElementType {
// 在class上面打标记
TYPE,
// 在成员变量打标记
FIELD,
// 在方法打标记
METHOD,
// 在参数打标记
PARAMETER,
// 在构造方法打标记
CONSTRUCTOR,
// 在本地变量打标记
LOCAL_VARIABLE,
// 在注解类打标记
ANNOTATION_TYPE,
}
比如我们标记了一个ElementType.Type,那么这个注解就只能用在在class上面,用在成员变量上面就会报错。
@Retention
这个标记有三个参数:
public enum RetentionPolicy {
// 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
SOURCE,
// 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
CLASS,
// 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
RUNTIME
}
可以看到除了RUNTIME,其他两个都最终会被抛弃,所以我们要实现功能性代码的时候,就需要使用RUNTIME这个类。
首先来写一下布局文件的注入,比如我们不想写烦人的setContentView
方法,直接用个注解来搞定
@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
布局文件只有一个textview
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.kevin.java.JavaActivity">
<TextView
android:id="@+id/tv_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="这是依赖注入的布局文件"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
androidx.constraintlayout.widget.ConstraintLayout>
下面开始定义这个注解类:
/**
* 我们在class上面定义该注解,就用ElementType.TYPE
* 实现功能性代码,不能将此注解抛弃,使用RetentionPolicy.RUNTIME
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectLayout {
int value();
}
现在知道我要出门了,衣服也有了,接下来就要创建一个女朋友,来帮我们拿衣服和穿衣服:
class InjectUtils {
/**
* 注入布局文件
*
* @param context 传入一个context,就是把activity传进来
*/
static void injectLayout(Context context) {
// 1. 获取当前class
Class<?> clazz = context.getClass();
// 2. 根据class获取class上面的InjectLayout注解
InjectLayout annotation = clazz.getAnnotation(InjectLayout.class);
// 判空
if (annotation == null) return;
// 3. 获取注解中的值,这里就是布局文件的id
int layoutId = annotation.value();
Log.i("kangf", "id === " + layoutId);
try {
// 4. 获取activity中的setContentView方法
Method method = clazz.getMethod("setContentView", int.class);
// 5. 执行setContentView方法,传入layoutId参数
method.invoke(context, layoutId);
} catch (Exception e) {
Toast.makeText(context, "找不到setContentView方法", Toast.LENGTH_LONG).show();
e.printStackTrace();
}
}
}
然后再activity中使用这个女朋友:
@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 让女朋友帮我穿衣服
InjectUtils.injectLayout(this);
}
}
其实布局文件注入大致分为5步,上面注释已经写清楚了,大致就是获取到注解类中的值,然后通过反射执行activity中的setContentView
方法,很简单,这里就不再多讲了,来看一下运行效果:
明白了layout注入的原理,View注入也是相同的道理,先来定义View的注解:
@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {
@InjectView(R.id.tv_test)
private TextView mTextView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtils.injectLayout(this);
// 别忘了创建自己的女朋友
InjectUtils.injectView(this);
if(mTextView == null) {
Toast.makeText(this, "没有找到textview",Toast.LENGTH_SHORT).show();
return;
}
mTextView.setText("这是通过注解获取了TextView");
}
}
布局文件没有变化,我们通过InjectView注解获取到TextView,并修改TextView的文字。
接下来创建一个InjectView注解类:
/**
* 因为是在成员变量上打的标记,所以使用ElementType.FIELD
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {
int value();
}
女盆友的创建:
static void injectView(Context context) {
// 1. 获取当前class
Class<?> clazz = context.getClass();
// 2. 获取activity中所有的成员变量
Field[] declaredFields = clazz.getDeclaredFields();
// 3. 开始遍历
for (Field field : declaredFields) {
field.setAccessible(true);
// 4. 获取字段上面的InjectView注解
InjectView annotation = field.getAnnotation(InjectView.class);
// 5. 如果字段上面没有注解,就不用处理了
if (annotation == null) {
return;
}
int viewId = annotation.value();
try {
// 6. 获取 findViewById 方法
Method findViewMethod = clazz.getMethod("findViewById", int.class);
// 7. 执行方法,获取View
View view = (View) findViewMethod.invoke(context, viewId);
// 8. 把view赋值给该字段
field.set(context, view);
} catch (Exception e) {
Toast.makeText(context, "没有找到findViewById方法", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
}
别忘了在activity中使用: InjectUtils.injectView(this);
思路跟注入layout差不多,获取到所有的字段,遍历并获取到字段上面的注解标记,通过注解的值获取到view, 最后把view赋值给这个字段。下面来看一下运行结果吧!
事件注入相对比较麻烦一些了 ,安卓中有26大事件,我们可以写很多注解类,但是女盆友是用户使用的,不能创建那么多女盆友啊,这怎么办呢?这时候我们就需要通过动态代理 + 注解的方式来解决了。
首先需要创建一个总线注解,给每一个事件注解使`用,这个总线包含了事件三要素:
TextView
、Button
等View.OnClickListener、View.OnLongClickListener
)setOnClickListener()
、setOnLongClickListener()
)@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBus {
// 事件类型
Class<?> eventType();
// 方法名
String eventMethod();
}
这个总线式应用到所有的事件注解类中的,所以这里使用了ElementType.ANNOTATION_TYPE
事件注解类就是这样的:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBus(eventType = View.OnClickListener.class, eventMethod = "setOnClickListener")
public @interface OnClick {
int[] value();
}
比如:
点击事件就用eventType = View.OnClickListener.class, eventMethod = “setOnClickListener”
长按事件用eventType = View.OnLongClickListener.class, eventMethod = “setOnLongClickListener”
接下来必不可少的当然是创建个贴心的女朋友啦~
static void injectEvent(final Context context) {
// 1. 开头是一样的,获取到class对象
Class<?> clazz = context.getClass();
// 2. 获取到所有的方法
Method[] declaredMethods = clazz.getDeclaredMethods();
// 3. 遍历所有的方法
for (final Method method : declaredMethods) {
method.setAccessible(true);
// 4. 获取方法上的注解, 因为一个方法上面可能会有多个注解,所以要获取所有的注解
Annotation[] annotations = method.getAnnotations();
// 5. 遍历方法上面的注解
for (Annotation annotation : annotations) {
// 6. 获取这个注解上面的注解类(也就是OnClick注解的class)
Class<? extends Annotation> aClass = annotation.annotationType();
// 7. 根据OnClick注解的class,获取EventBus注解
EventBus eventBus = aClass.getAnnotation(EventBus.class);
// 8. 判断如果有EventBus注解,才代表的是事件注解,进行处理
if (eventBus != null) {
// 9. 获取EventBus注解的值
Class<?> eventType = eventBus.eventType();
String eventMethodName = eventBus.eventMethod();
try {
// 10. 通过反射拿到方法注解中的值(这里就是所有view的id数组)
Method ids = aClass.getDeclaredMethod("value");
int[] viewIds = (int[]) ids.invoke(annotation);
if (viewIds == null) {
return;
}
// 11. 遍历id数组
for (int viewId : viewIds) {
// 12. 获取view
Method findViewMethod = clazz.getMethod("findViewById", int.class);
View view = (View) findViewMethod.invoke(context, viewId);
// 13. 如果有这个view,才进行处理
if (view != null) {
// 14. 动态代理,代理事件类型,交给我们的方法来处理
Object proxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{eventType},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method oldMethod, Object[] args) throws Throwable {
// 执行当前activity中的方法,参数不能少,需要跟原事件方法参数一样
return method.invoke(context, args);
}
});
// 获取activity中的事件方法
Method activityEventMethod = view.getClass().getMethod(eventMethodName, eventType);
// 15. 当这个方法执行的时候,自动执行代理方法
activityEventMethod.invoke(view, proxy);
}
}
} catch (Exception e) {
Toast.makeText(context, "找不到该value方法", Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
}
}
}
思路就是通过事件类型获取事件的类型和方法名,然后通过代理取到事件的方法,当执行事件的时候自动执行我们在activity中定义的事件方法。动态代理大家还不了解的话可以去学习一下,很简单。具体思路大家可以照着上面的注释缕一缕。
如果增加一个事件也很简单了,只需增加一个事件注解类:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBus(eventType = View.OnClickListener.class, eventMethod = "setOnClickListener")
public @interface OnClick {
int[] value();
}
在activity中使用:
@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {
@InjectView(R.id.tv_test)
private TextView mTextView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
InjectUtils.injectLayout(this);
InjectUtils.injectEvent(this);
}
@OnClick({R.id.tv_test})
private void clickText(View view) {
Toast.makeText(this, "点击了view", Toast.LENGTH_SHORT).show();
}
@OnLongClick({R.id.tv_test})
private boolean longClickText(View view) {
Toast.makeText(this, "长按点击了view", Toast.LENGTH_SHORT).show();
return true;
}
}
一个点击事件,一个长按事件,来看效果图:
代码都已经上传到了github,有兴趣的可以下载下来看看: