什么是IOC注入框架
ButterKnife大家都应该使用过,对于view的注入减少了大量篇幅的findViewById操作,而注解注入的方式也显得更加优雅。这里介绍一下我的IOC简单注入框架,项目地址移步这里
IOC使用简单介绍
添加依赖
项目根目录下的build.gradle文件添加如下内容
allprojects {
repositories {
...
maven { url "https://raw.githubusercontent.com/demoless/ioc/master/repo" }
}
}
然后在app模块的build.gradle文件添加如下内容
implementation 'com.demoless:ioc:1.0.0'
这里我贴出demo的调用示例代码看看如何使用:
要实现这样一套IOC框架我们还要先注册一下,看BaseActiivty的代码:
可以看到相比于传统的Activity的写法,IOC注入框架颇具诱惑,下面我就带大家了解一下我的IOC实现思路。
如何实现IOC
IOC是一套注解注入框架,所以主要是通过Java的反射与注解来实现的,这里就不介绍了,不了解的可以看看这篇文章。
布局注入
@ContentView(R.layout.activity_main)
首先创建ContentView这个注解
创建过程跟创建类的过程是一样的,只需要将Kind选择为Annotation即可:
注解的编写
package com.demo.iocinject.ioc;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Create by Zhf on 2019/7/13
**/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ContentView {
int value();
}
我们可以看到在这个注解的上面还有两个注解,这是两个元注解,首先@Target(ElementType.TYPE)代表这个ContentView注解作用在类上面,然后@Retention(RetentionPolicy.RUNTIME)表示注解在运行时执行,因为一个类只会有一个布局文件,所以这里value方法的返回值为int,而不是数组。
Java实现注解的执行逻辑
定义好了这个注解之后,我们就要考虑如何将ContentView里传入的布局文件设置给Activity,我们知道传统的activity是通过在onCreate方法里的setContentView来将布局文件设置给activity的,那我们也只需要通过反射将传入注解的布局再传入setContentView并且让它自动执行不就可以实现了嘛,思路好像没错,我们来实现以下:
//布局注入
private static void injectLayout(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//获取类之上的注解
ContentView contentView = clazz.getAnnotation(ContentView.class);
if (contentView != null){
//获取注解的返回值
int layoutId = contentView.value();
//第一种方法
//activity.setContentView(layoutId);
//第二种方法
try {
Method setContentView = clazz.getMethod("setContentView", int.class);
setContentView.invoke(activity,layoutId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这段代码首先通过activity.getClass()拿到这个Class对象,再通过clazz.getAnnotation(ContentView.class)获取类上的注解,拿到注解之后自然要获取他的返回值,所以再调用他的value方法,这样我们就拿到了对应的布局文件,最好要完成的是传入setContentView这个布局方法并执行。这里我给出了两种方法,第一种很简单直接调用activity的setContentView方法,第二种通过反射拿到setContentView这个Method对象,在调用invoke方法自动执行。这样一个简易布局注入就实现了。
控件注入
@InjectView
注解文件
与ContentView一样这里就不在赘述了:
Java执行逻辑
//控件的注入
private static void injectViews(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//获取类的全部属性
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
//获取属性上的注解
InjectView injectView = field.getAnnotation(InjectView.class);
if (injectView != null){
//获取注解的值
int viewId = injectView.value();
//View view = activity.findViewById(viewId);
try {
//获取findViewById方法
Method findViewById = clazz.getMethod("findViewById", int.class);
//执行findViewById方法
Object view = findViewById.invoke(activity, viewId);
//设置访问权限 private
field.setAccessible(true);
//为属性赋值
field.set(activity,view);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
这段代码也跟布局注入的实现很相似,这是回去类的全部属性的时候,需要调用的是clazz.getDeclaredFields()方法,如果用getFields方法程序会崩溃,这个很容易想到,因为在父类和子类中可能会有相同命名的一个控件,就行这样:private Button mButton;,所以就造成崩溃,这只能获取类自身的属性;第二个需要注意的地方是,通常我们声明一个控件,都是用的private关键字,所以这里还需要设置一下访问权限,调用 field.setAccessible(true),这样对稀有属性进行操作;另一个与布局注入实现不同的是,setContentView方法没有返回值,而findViewById则相反,所以我们需要为属性(这里就是一些View)赋值,调用的是field.set(activity,view)。
事件的注入
@InjectEvent
Android事件监听规律
事件的注入相比之前的布局和控件注入,难度和复杂度大大提高了。通过对Android中的事件监听代码的观察,我们得出如下三部曲:
- setListener
- new Listener
- doCallback
就像View的点击事件和长按时间监听那样,首先setListener:View.setOnClickListener(),然后new 一个Listener传入,View.setOnClickListener(new OnClickListener(View v){}),最后执行回调方法:
onClick(View v){...}
定义事件监听规律的注解
@EventBase
通过上述规律总结,我们要先定义这个注解:
/**
* Create by Zhf on 2019/7/13
**/
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBase {
//setListener
Class<?> listenerType();
//new View.OnxxxListener
String listenerSetter();
//回调 最终执行方法
String callBackListener();
}
我们看到这个注解是放在注解类之上的,那么这个注解怎么使用呢,就以View的长按事件监听为例:
/**
* Create by Zhf on 2019/7/13
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBase(listenerSetter = "setOnLongClickListener",
listenerType = View.OnLongClickListener.class,
callBackListener = "onLongClick")
public @interface OnLongClick {
int[] value();
}
在这个注解上面调用了刚才定义的EventBase注解,根据传入的值大家似乎就什么都看明白了吧,没错这里传入了View.OnLongClickListener事件监听三部曲,因为在一个类中可能不止一个控件会设置长按事件监听,所以这里的返回值是数组。
事件注入的逻辑
//事件的注入
private static void injectEvents(Activity activity) {
Class<? extends Activity> clazz = activity.getClass();
//获取一个类的所有方法
Method[] methods = clazz.getDeclaredMethods();
//遍历所有方法
for (Method method : methods) {
Annotation[] annotations = method.getAnnotations();
//遍历所有注解
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType != null) {
EventBase eventBase = annotationType.getAnnotation(EventBase.class);
if (eventBase != null) {
String listenerSetter = eventBase.listenerSetter();
Class<?> listenerType = eventBase.listenerType();
String callBackListener = eventBase.callBackListener();
try {
Method valueMethod = annotationType.getDeclaredMethod("value");
int[] viewIds = (int[]) valueMethod.invoke(annotation);
//设置private权限可见
method.setAccessible(true);
//AOP切面
ListenerInvocationHandler handler = new ListenerInvocationHandler(activity);
handler.addMethods(callBackListener, method);
//代理模式
Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(),
new Class[]{listenerType}, handler);
for (int viewId : viewIds) {
View view = activity.findViewById(viewId);
Method setter = view.getClass().getMethod(listenerSetter, listenerType);
setter.invoke(view, listener);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
这里其他的不多介绍了,主要不同的就是这里使用了动态代理和AOP切面技术:
import android.util.Log;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
/**
* Create by Zhf on 2019/7/13
**/
public class ListenerInvocationHandler implements InvocationHandler {
private final static long QUICK_EVENT_TIME_SPAN = 300;
private long lastClickTime;
private Object target;//需要拦截的对象
private HashMap<String, Method> map = new HashMap<>();
public ListenerInvocationHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (target != null){
String methodName = method.getName();
method = map.get(methodName);
long timeSpan = System.currentTimeMillis() - lastClickTime;
if (timeSpan < QUICK_EVENT_TIME_SPAN){
Log.e("点击阻塞,防止误点", String.valueOf(timeSpan));
return null;
}
lastClickTime = System.currentTimeMillis();
if (method != null){
if (method.getGenericParameterTypes().length == 0) return method.invoke(target);
return method.invoke(target,args);
}
}
return null;
}
public void addMethods(String methodName, Method method){
map.put(methodName, method);
}
}
这个类实现了InvocationHandler接口,可以实现点击事件不传参数以及点击阻塞,防误点,具体的逻辑比较简单,可以看看代码以及注释。
站在巨人的肩膀上
该IOC实现参考网易云课堂,github地址本文开篇已经给出,欢迎大家star与fork。