Android IOC注入框架

什么是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的代码:

IOC注册

可以看到相比于传统的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 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一样这里就不在赘述了:

InjectView

Java执行逻辑

//控件的注入
    private static void injectViews(Activity activity) {
        Class 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 clazz = activity.getClass();

        //获取一个类的所有方法
        Method[] methods = clazz.getDeclaredMethods();
        //遍历所有方法
        for (Method method : methods) {
            Annotation[] annotations = method.getAnnotations();
            //遍历所有注解
            for (Annotation annotation : annotations) {
                Class 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 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。

你可能感兴趣的:(Android IOC注入框架)