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<? 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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容