美文网首页
Android IOC注入框架

Android IOC注入框架

作者: 酷酷的Demo | 来源:发表于2019-07-15 11:51 被阅读0次

    什么是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<? 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一样这里就不在赘述了:

    InjectView

    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。

    相关文章

      网友评论

          本文标题:Android IOC注入框架

          本文链接:https://www.haomeiwen.com/subject/iqdgkctx.html