IOC思想,现在很多框架中都有使用,,如Butterknife、Dagge、Xutils、Retrofit等等
简介
IOC是原来由程序代码中主动获取的资源,转变由第三方获取并使原来的代码被动接收的方式,以达到解耦的效果,称为控制反转。
图解
IOC之前,就好像自己的事情自己主动做;
IOC之后,就好像自己的事情由别人去做,相当于被动接受;
代码中IOC,相信用过上面讲的框架的人,大概都知道了,注解标识…
移动端IOC框架中常用到的用途:布局注入、控件注入、事件注入
下面代码使用到的是运行时反射的方式
下面我们来看代码吧
布局注入
布局注入个人认为是这三个注入中最简单的;
像我们Activity中设置布局,是setContentView(id)
相信大家都不陌生吧?…
我们来通过IOC来做
自定义注解
1、自定义注解
我们自定义一个注解,这个注解我们作用在类上面,运行时可操作访问;
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到;
@Target(ElementType.TYPE) //注解所修饰的对象范围 ElementType.TYPE作用在类上面;
定义了一个int类型的value,用于接收布局
package com.lk.myioc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解 用来设置布局
*/
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.TYPE) //注解所修饰的对象范围 ElementType.TYPE作用在类上面
public @interface ContentView {
int value(); //注解里面的值 int类型
}
使用注解
2、在Activity上应用出这个注解
@ContentView(R.layout.activity_main)
自定义注解类@Target设置的是作用在类上面
括号里面传入的是布局
package com.lk.myioc;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
/**
* 我们自己编写的自定义注解类ContentView
*/
@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("MainActivity",btn1.toString());
Log.i("MainActivity",btn2.toString());
}
}
编写注入代码
3、接下来我们来编写注入的代码
1、通过反射获取到类对象;
2、得到这个类对象里面的ContentView注解;
3、判断了当前这个类是否有这个注解;
4、如果有这个注解,就获取这个注解里面定义的value的值(里面是我们传入进来的布局id);
5、反射获取到setContentView这个方法;
6、执行setContentView这个方法;
里面一些判断这里就省略了,自己在项目中用的时候,记得要加上各种判空…
package com.lk.myioc;
import android.view.View;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 注入的工具类
*/
public class InUtils {
public static void inContextView(Object context){
//反射获取类
Class<?> clazz = context.getClass();
//注入功能就在这里完成
//布局注入
initLayout(clazz,context);
}
/**
* 布局注入
* @param clazz
* @param context
*/
private static void initLayout(Class<?> clazz,Object context) {
int layoutId = 0;
//得到这个类的ContentView注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
//如果当前这个类里面有这个注解
if(contentView!=null){
//得到注解中的value值
layoutId = contentView.value();
//反射去执行setContentView(id)
try {
//获取到这个方法
//第一个参数是方法名;第二个参数是方法的参数类型
Method setContentView = clazz.getMethod("setContentView", int.class);
//执行这个方法
//第一个参数是方法的对象
//后面的参数是这个方法需要传递进去的参数
setContentView.invoke(context,layoutId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
控件注入
findViewById(id) 这个方法获取控件
相信大家都不陌生吧?…
我们来通过IOC来做
自定义注解
1、自定义注解
我们自定义一个注解,这个注解我们作用在字段变量上面,运行时可操作访问;
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到;
@Target(ElementType.FIELD) //注解所修饰的对象范围 ElementType.FIELD 作用在变量上面;
定义了一个int类型的value,用于接收控件id
package com.lk.myioc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解 用来findViewById
*/
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.FIELD) //注解所修饰的对象范围 ElementType.FIELD 作用在变量上面
public @interface FindView {
int value(); //注解里面的值 int类型
}
使用注解
2、在Activity上应用出这个注解
@FindView(R.id.app_btn)
自定义注解类@Target设置的是作用在字段变量上面
括号里面传入的是控件的id
@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {
@FindView(R.id.app_btn)
private Button btn1;
@FindView(R.id.app_btn1)
private Button btn2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("MainActivity",btn1.toString());
Log.i("MainActivity",btn2.toString());
}
}
编写注入代码
3、接下来我们来编写注入的代码
1、通过反射获取到类对象;
2、这里我们是要获取这个类里面的所有字段变量,这样我们才能一个个去找出标有FindView注解的字段变量
3、通过getDeclaredFields获取到所有的字段变量,所以得到的是个数组,所以我们需要遍历这个字段变量的数组;
4、老规矩,我们肯定是要挑选出标有FindView这个注解的字段变量,所以通过getAnnotation获取到注解,进行注解的判断;
5、如果有这个注解,就获取这个注解里面定义的value的值(里面是我们传入进来的控件id);
6、反射获取到findViewById这个方法;
7、执行findViewById这个方法,并返回了一个View;
8、下面我们就需要将这个获取到的view值设置到该字段变量上面去了;
9、因为有的可能是私有的字段变量,所以我们先忽略修饰符权限,setAccessible(true);
10、然后给字段变量赋值;
里面一些判断这里就省略了,自己在项目中用的时候,记得要加上各种判空…
package com.lk.myioc;
import android.view.View;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 注入的工具类
*/
public class InUtils {
public static void inContextView(Object context){
//反射获取类 因为这里会传入Activity的上下文 所以这里拿到的是Activity类
Class<?> clazz = context.getClass();
//注入功能就在这里完成
//控件注入
initView(clazz,context);
}
/**
* 控件注入
* @param clazz
* @param context
*/
private static void initView(Class<?> clazz,Object context) {
//拿到这个类所有的字段
Field[] fields = clazz.getDeclaredFields();
//遍历所有的变量字段
for (Field field : fields) {
//拿到变量字段上标识的FindView自定义注解
FindView findView = field.getAnnotation(FindView.class);
//如果字段上标有这个注解 我们就注入
if(findView!=null){
//如果有 我们就获取里面的value字段的值
int viewId = findView.value();
try {
//反射获取findViewById这个方法
Method findViewById = clazz.getMethod("findViewById", int.class);
//反射去执行findViewById这个方法
View view = (View) findViewById.invoke(context,viewId);
//在访问时会忽略访问修饰符,可以通过反射获取私有变量的值
field.setAccessible(true);
//给这个变量字段设置值
field.set(context,view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
事件注入
事件注入就比较的复杂了,因为有很多中事件,这里我们就拿两种事件来做个例子;
但是我们要考虑到以后的扩展性,我们不可能每新加一种事件,就去写一个注入吧?
接下来我们来看看代码
自定义注解
1、自定义注解
我们自定义一个注解,这个注解我们作用在字段变量上面,运行时可操作访问;
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到;
@Target(ElementType.METHOD) //注解所修饰的对象范围 ElementType.METHOD 作用在方法上面;
定义了一个int数组类型的value,用于接收控件id,记住这里可能是多个id
有没有发现与上面两个注解的不同?是不是多了一个**EventBase**注解?
点击事件的注解
package com.lk.myioc;
import android.view.View;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解 用来处理OnClick事件
*/
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.METHOD) //注解所修饰的对象范围 ElementType.METHOD 作用在方法上面
//自定义的父注解 应用在注解类上面的
//listenerSetter 订阅关系
//listenerType 事件类
//callBackMethod 事件处理的方法名
@EventBase(listenerSetter = "setOnClickListener",
listenerType = View.OnClickListener.class,
callBackMethod = "onClick")
public @interface OnClick {
int[] value() default -1; //注解里面的值 int类型数组,默认值为-1
}
长按事件的注解
package com.lk.myioc;
import android.view.View;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解 用来处理OnLongClick事件
*/
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.METHOD) //注解所修饰的对象范围 ElementType.METHOD 作用在方法上面
//自定义的父注解 应用在注解类上面的
//listenerSetter 订阅关系
//listenerType 事件类
//callBackMethod 事件处理的方法名
@EventBase(listenerSetter = "setOnLongClickListener",
listenerType = View.OnLongClickListener.class,
callBackMethod = "onLongClick")
public @interface OnLongClick {
int[] value() default -1; //注解里面的值 int类型数组,默认值为-1
}
我们再来看看这个EventBase这个注解
@Target(ElementType.ANNOTATION_TYPE) //注解所修饰的对象范围 ElementType.ANNOTATION_TYPE 作用在注解类上面
我们定义了三个属性:
1、****String listenerSetter(); 订阅关系,如**setOnClickListener**,但是我们为什么需要的是String字符串类型?
2、****Class<?> listenerType();事件, 如new View.OnClickListener(),我们这里Class类型;
3、****String callBackMethod();事件处理程序,比如onClick(View v),我们这里是String字符串类型;
package com.lk.myioc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 该注解在另一个注解上使用
* 相当于父注解
*/
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.ANNOTATION_TYPE) //注解所修饰的对象范围 ElementType.ANNOTATION_TYPE 作用在注解类上面
public @interface EventBase {
// btn1.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
//
// }
// });
//1、setOnClickListener 订阅关系
String listenerSetter();
//2、new View.OnClickListener() 事件
Class<?> listenerType();
//3、onClick(View v) 事件处理程序
String callBackMethod();
}
使用注解
2、在Activity上应用出这个注解
@OnClick({R.id.app_btn,R.id.app_btn1})是我们需要注入的点击事件,我们在注解中考虑了多个控件的点击事件,记得吗?当时定义的属性是个int类型的数组,所以这里可以传入多个控件的id;
@OnLongClick(R.id.app_btn)使我们需要注入的长按事件,注解里面定义的属性也是数组,但是数组可以有多个或者1个,所以我们这里可以只传1个控件的id;
package com.lk.myioc;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
/**
* 我们自己编写的自定义注解类ContentView
*/
@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {
@FindView(R.id.app_btn)
private Button btn1;
@FindView(R.id.app_btn1)
private Button btn2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("MainActivity",btn1.toString());
Log.i("MainActivity",btn2.toString());
}
@OnClick({R.id.app_btn,R.id.app_btn1})
public void click(View view){
switch (view.getId()){
case R.id.app_btn:
Toast.makeText(this,"第一个按钮点击了",Toast.LENGTH_LONG).show();
break;
case R.id.app_btn1:
// Toast.makeText(this,"第二个按钮点击了",Toast.LENGTH_LONG).show();
NewsDialog newsDialog = new NewsDialog(MainActivity.this);
newsDialog.show();
break;
}
}
@OnLongClick(R.id.app_btn)
public boolean longClick(View view){
Toast.makeText(this,"第一个按钮长按了",Toast.LENGTH_LONG).show();
return true;
}
}
编写注入代码
3、接下来我们来编写注入的代码
1、通过反射获取到类对象;
2、获取这个类对象的所有方法函数,得到的是个方法的数组,我们来遍历它;
3、因为一个方法上面可能会有多个注解,所以我们这里获取了方法上面的所有注解
Annotation[] annotations = method.getAnnotations();
4、遍历这个方法上面的所有注解
5、查看这个注解类里面是否标有我们的注解EventBase这个注解,因为标有这个EventBase注解的,才是我们自己定义的事件注解,如果没有标有这个注解的,我们不做处理直接continue进行下一个注解的查找;
6、如果是我们的事件注解,我们就获取里面的订阅关系的字符串、事件类对象、以及事件处理程序的字符串;
7、拿到这个事件注解里面的id数组进行遍历;
这里我们就得考虑个问题,怎么样才能够在执行这个事件的时候,能回调出发到我们自己的这个标有自定义事件注解的方法上面了?
答案是动态代理
我们先看看代理类
package com.lk.myioc;
import android.util.Log;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* 动态代理类
* 我们要代理事件
* 为什么要代理?
* 因为我们不需要将事件转嫁到Activity的方法上面去
* @OnClick({R.id.app_btn,R.id.app_btn1})
* public void click(View view){
*
* }
*/
public class ListenerInvocationHandler implements InvocationHandler {
//代理类
private Object activity;
//代理类里面的代理方法
private Method activityMethod;
public ListenerInvocationHandler(Object activity, Method activityMethod) {
this.activity = activity;
this.activityMethod = activityMethod;
}
/**
* 点了按钮后,执行这个方法 onClick
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在这里去调用被注解的事件方法 click
// @OnClick({R.id.app_btn,R.id.app_btn1})
// public void click(View view){
//
// }
return activityMethod.invoke(activity,args);
}
}
我们再来看看这个注入的方法
package com.lk.myioc;
import android.view.View;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 注入的工具类
*/
public class InUtils {
public static void inContextView(Object context){
//反射获取类 因为这里会传入Activity的上下文 所以这里拿到的是Activity类
Class<?> clazz = context.getClass();
//注入功能就在这里完成
//事件注入
initClick(clazz,context);
}
/**
* 事件注入
* @param clazz
* @param context
*/
private static void initClick(Class<?> clazz,Object context) {
//获取这个类里面的所有方法
Method[] methods = clazz.getDeclaredMethods();
//遍历所有方法
for (Method method : methods) {
//拿到方法上面所有的注解,因为一个方法上面可能会存在多个注解
Annotation[] annotations = method.getAnnotations();
//遍历方法上面的所有注解
for (Annotation annotation : annotations) {
//怎样找到这个注解标识是事件了?
//思路:查看这个注解类里面是否标有我们的父注解EventBase这个注解
//拿到这个注解类型
Class<?> annotationClass = annotation.annotationType();
EventBase eventBase = annotationClass.getAnnotation(EventBase.class);
//如果没有EventBase这个注解标识 我们不做处理 继续下一个注解
if(null==eventBase){
continue;
}
//否则就是事件注解
//获取 注解上 订阅关系的字符串
String listenerSetter = eventBase.listenerSetter();
//获取 注解上 事件的类
Class<?> listenerType = eventBase.listenerType();
//获取 注解上 事件处理程序的字符串
String callBackMethod = eventBase.callBackMethod();
Method valueMethoid = null;
try {
//反射得到id,再根据id得到对应的view
// @OnClick({R.id.app_btn,R.id.app_btn1})
valueMethoid =annotationClass.getDeclaredMethod("value");
int[] viewId = (int[]) valueMethoid.invoke(annotation);
//对每一个Id都需要事件绑定
for (int id : viewId) {
//找到id对应的对象
Method findViewById = clazz.getMethod("findViewById", int.class);
View view = (View) findViewById.invoke(context, id);
//如果view不存在 就不做操作
if(view==null){
continue;
}
//得到id对应的view以后
//开始在这个view上执行监听(使用代理,去代理事件)
//需要去执行activity上的时间处理方法
//activity==context 需要执行的方法==method
//拿到这个代理对象
ListenerInvocationHandler listenerInvocationHandler =
new ListenerInvocationHandler(context, method);
//开始代理这个接口
//比如如果是点击事件View.OnClickListener()===listenerType
// 第一个参数是需要代理的类的类加载器
// 第二个参数是动态代理类需要实现的接口
// 第三个参数是哪个类来代理 动态代理方法在执行时,会调用h里面的invoke方法去执行
Object proxy= Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, listenerInvocationHandler);
//根据字符串方法名获取到方法listenerSetter
//第一个参数是方法名setOnClickListener,第二个参数是方法的参数类型View.OnClickListener.class
//例如是这样:setOnClickListener(new View.OnClickListener() {})
Method listenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
//方法执行到代理类里面去
//这里代理的是new View.OnClickListener() {} ====proxy
listenerMethod.invoke(view,proxy);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
我们再来看看BaseAtivity的使用
package com.lk.myioc;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.Nullable;
public class BaseActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//调用注入的方法
InUtils.inContextView(this);
}
}
如果需要扩展添加其他事件的时候,自己再去编写其他的事件注解类;记得修改成对应的订阅关系字符串、事件类、事件处理程序
以上就是简单的IOC注入的代码实现思路,每行代码已经加好注释,如有问题欢迎指出,大神勿喷,谢谢