Android中手撸IOC框架

在刚接触安卓的第二天 , 自己最熟悉的代码 , 就是那句findViewById. 记得当时特别舒服的啪啪啪敲完一行有用的代码 , 心里美滋儿啊 , 心里想着 , 啥时候写其他的逻辑就像这段啪啪啪就能完成的代码一样 , 那多舒服 。

但是吧 , 人都是有惰性的 , 我说的是偷懒的那种惰性:
在面对一个比较复杂的界面的时候 , 你需要机械化的吧所有的组件统统findViewById找出来 , 然后再去做相关操作 ; 有时候仅仅是为了设置一个点击事件 , 但却必须要先声明 , 查找 , 才能继续完成接下来的工作 .于是 , 我想偷个懒了.....

抽取方法

当然了,最先想到的,当然是想吧这些麻烦的重复性操作抽出来,其实也就是少写了findViewById的这么几个字,本质上还是和以前的逻辑一样:

protected final <T extends View> T $(@IdRes int id) {
        return (T) view.findViewById(id);
}

这样,就可以吧代码简化为iv_head = $(R.id.iv_head);
emmm,好像意义不是很大,仍然是重复的工作。

IOC的出现

偶然的机会,接触到了ButterKnife的框架,这个框架极大地简化了组件查找,事件等一系列的操作,只需要一个注解就可以轻松搞定那些繁杂的工作。

@Bind(R.id.toolbar)
protected Toolbar toolbar;

这样,通过注解就可以完成定义到查找的两个工作,恩~舒服,但是这个注解的背后又存在着怎么样的实现呢?

略微看一下BK的源码吧

  1. 首先,我们点进去@Bind这个注解,来到了响应的注解接口
@Retention(CLASS) @Target(FIELD)
public @interface Bind {
  /** View ID to which the field will be bound. */
  int[] value();
}

哇什么鬼,还有@Interface,是不是没见过的科技? 不要急 , 慢慢来看
这里一共有三个注解

  • Rentation: Reteniton的作用是定义被它所注解的注解保留多久,它的取值是一个枚举类型,有三种:
    SOURCE 被编译器忽略
    CLASS 注解将会被保留在Class文件中,但在运行时并不会被VM保留。这是默认行为
    RUNTIME 保留至运行时。所以我们可以通过反射去获取注解信息。

  • Target 用于设定注解使用范围,接收参数为ElementType的枚举
    METHOD 可用于方法上
    TYPE 可用于类或者接口上
    ANNOTATION_TYPE 可用于注解类型上(被@interface修饰的类型)
    CONSTRUCTOR 可用于构造方法上
    FIELD 可用于域上
    LOCAL_VARIABLE 可用于局部变量上
    PACKAGE 用于记录java文件的package信息
    PARAMETER 可用于参数上

  • interface: 用于自定义注解
    自定义注解也就是可以自己写需要的注解


    image.png
  1. 使用@interface关键字定义注解,注意关键字的位置
  2. 成员以无参数无异常的方式声明,注意区别一般类成员变量的声明
  3. 可以使用default为成员指定一个默认值,如上所示
  4. 成员类型是受限的,合法的类型包括原始类型以及String、Class、Annotation、Enumeration (JAVA的基本数据类型有8种:byte(字节)、short(短整型)、int(整数型)、long(长整型)、float(单精度浮点数类型)、double(双精度浮点数类型)、char(字符类型)、boolean(布尔类型)
  5. 注解类可以没有成员,没有成员的注解称为标识注解
  6. 如果注解只有一个成员,并且把成员取名为value(),则在使用时可以忽略成员名和赋值号“=” ,例如JDK注解的@SuppviseWarnings ;如果成员名不为value,则使用时需指明成员名和赋值号"="

打造手撸的IOC框架

什么,才刚刚不到5分钟就可以开始手撸了么,别慌, 刚!
首先 , 我们模拟三种需求:

  1. 使用注解设置布局,代替setContentView
  2. 使用ViewInject代替findViewById
  3. 使用@OnClick和@OnLongClick代替点击与长按

IOC的本质就是控制反转 , 也就是将A要做的事情, 委托给B来做 , 所以我们需要一个第三方的容器类来完成这些工作
首先做好准备工作, 将BaseActivity搭建好 , 在里面调用容器的初始化 , 这样让每一个Activity去继承改基类就可以完成初始化的操作

public abstract class BaseActivity extends AppCompatActivity {


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //将当前对象注入到第三方的容器
        InjectUtils.inject(this);
        increate(savedInstanceState);
    }

    public abstract void increate(Bundle savedInstanceState);
}

然后新建InjectUtils类 , 编写静态的Inject方法 , 这里传入上下文对象.

public static void inject(Context context) {
        //先注入视图,再注入控件
        injectLayout(context);
        injectView(context);
        injectEvents(context);
    }

定义好三个方法之后 , 我们的基础框架就基本完成了 , 接下来实现一下具体的注解实现细节

任务1. 使用注解设置布局

@ConvertView(R.layout.activity_second)

首先 , 既然是自定义的注解 , 当然要先有一个类似于刚刚上面的自定义的@inteface , 然后默认的方法参数为int类型 , 代码如下:

//运行是也存在,用于注解
@Retention(RetentionPolicy.RUNTIME)
//用在类上的注解, 写在类的上方
@Target(ElementType.TYPE)
public @interface ConvertView {
    int value();
}

定义好了注解之后 , 就要完成第三方容器中的方法了 , 这里我们使用反射的方法去找到注解, 然后反射到响应的方法 , 并调用方法本身 , 就完成了容器的作用 , 也就是上面准备的injectLayout方法

public static void injectLayout(Context context) {

        int layoutId = 0;
        Class clazz = context.getClass();

        //从注解出拿到注解中的值
        ConvertView view = (ConvertView) clazz.getAnnotation(ConvertView.class);

        if (null != view) {
            //从接口的value函数中获取id值
            try {
                layoutId = view.value();
                //利用反射获取需要的方法
                Method method = clazz.getMethod("setContentView", int.class);

                //拿到setContentView后调用函数
                method.invoke(context, layoutId);

            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

至此 , 我们完成了第一个需求 , 以后不再有setContentView, 只需要注解就可以了:

@ConvertView(R.layout.activity_second)
public class SecondActivity extends BaseActivity {
    @Override
    public void increate(Bundle savedInstanceState) {
       
    }
}

任务2. 使用ViewInject代替findViewById

有了布局的经验之后 , 我们可以轻车熟路的按照流程来实现这个方法
首先是自定义的注解

//运行时也存在
@Retention(RetentionPolicy.RUNTIME)
//用在域上的注解
@Target(ElementType.FIELD)
public @interface ViewInject {
    int value();
}

接下来在容器中实现委托的操作

public static void injectView(Context context) {

        Class<? extends Context> aClass = context.getClass();
        //获取到上下文中所有成员变量
        Field[] declaredFields = aClass.getDeclaredFields();

        for (Field f : declaredFields) {
            //获取有注解的控件,注意注解之后没有加分号,意味着注解和类型声明是同一行语句 , 这里利用注解获取控件的本质是通过反射到成员变量
            ViewInject annotation = f.getAnnotation(ViewInject.class);
            //如果有注解 , 则获取注解的值
            if (null != annotation) {
                int value = annotation.value();

                try {
                    //调用了Activity的findViewById方法,context中没有该方法,需要反射获取
                    Method findViewById = aClass.getMethod("findViewById", int.class);

                    View view = (View) findViewById.invoke(context, value);
                    //允许反射私有变量
                    f.setAccessible(true);
                    //为反射到的变量赋值
                    f.set(context, view);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }

好像比设置布局要复杂一点?其实是差不多的 , 只不过这里在拿到方法之后, 有设置了返回值并实例化(改变字段) , 懂了第一个之后 , 也不是很难理解吧 .

任务3. 使用@OnClick和@OnLongClick代替点击与长按

这个相比于前两个就要复杂一些了 ,

  • setContentView()只需要调用方法传入参数即可
  • findViewById(), 需要传入参数并拿到返回值即可
  • setOnclickListener()需要传入一个接口并实现其方法 .
    纳尼?要传入一个方法,也就是View.OnclickListener的实现方法。我们知道这个方法是在View中的,这里难道还要传入一个View的参数么?没必要这么麻烦的。当我们需要访问某个对象但存在困难时,可以通过一个代理对象去间接的访问,所以就需要绕个小弯子: 使用代理模式

在定义注解之前我们先想一下 , 如果要用注解编写点击事件的话 , 我们会省略掉一下几个步骤:

  1. 点击事件的方法 被省略
  2. 点击事件的参数: 被省略
  3. 匿名内部类的回调方法的方法回调 被省略
    所以我们在注解中需要三个参数 。而且后期我们会添加各种各样的监听事件,所以在点击事件上做一个封装,用来管理所有的事件
@Retention(RetentionPolicy.RUNTIME)
//用于注解上的注解
@Target(ElementType.ANNOTATION_TYPE)
public @interface EventBase {

    //设置监听方法
    String listenerSetter();

    //事件类型
    Class listenerType();

    //事件回调
    String callBackMethod();
}

可以理解为注解的基类。我们的点击事件也好 , 长按事件也好 , 都基于该基类扩展 , 所以该基类会作为一个参数出现在另一个注解中 , 所以我们的@Target必须为ANNOTATION_TYPE , 也就是注解中的注解. 并且,将参数设置为String而不是Method,也是为了反射与扩展的方便。

首先我们编写OnClick注解,使用基类来传递参数:

@Retention(RetentionPolicy.RUNTIME)
//用于方法上的注解
@Target(ElementType.METHOD)
@EventBase(listenerSetter = "setOnClickListener",
      listenerType = View.OnClickListener.class,
      callBackMethod = "onClick")
public @interface OnClick {

    int[] value() default -1;
}

现在明白基类的作用了吧,其实就是限制了参数的传递规范(这个规范使用枚举会更有可读性,这里就不浪了,大家自己来吧)。


敲重点,在委托容器中完成注入操作之前 , 我们先要编写代理 , 在代理中调用方法:

public class ListenerInvactionHandler implements InvocationHandler {
    //被代理的真实对象的应用
    private Context context;
    //保存方法名与方法体, 用来判断是否需要被代理
    private Map<String, Method> methodMap;

    public ListenerInvactionHandler(Context context, Map<String, Method> methodMap) {
        this.context = context;
        this.methodMap = methodMap;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //首先获取到方法名
        String name = method.getName();
        //根据方法名找方法,看是否需要代理
        Method metf = methodMap.get(name);

        if (metf != null) {
            //需要代理,使用代理调用
            return metf.invoke(context, args);
        } else {
            return method.invoke(proxy, args);
        }
    }
}

还算好理解吧 , 上面这个方法中, 集合时用来保存类中所有的方法, 然后根据方法名来查看该方法是否需要被代理 , 如果需要的话使用代理来调用 , 否则使用本身来调用.
接下来是容器中的操作先贴上代码,在代码中吧重点都注释了.

@SuppressLint("NewApi")
    private static void injectEvents(Context context) {
        Class<? extends Context> aClass = context.getClass();
        //拿到类中所有的方法
        Method[] declaredMethods = aClass.getDeclaredMethods();

        //遍历所有的方法并查找带注解的方法
        for (Method m : declaredMethods) {

            Annotation[] annotations = m.getAnnotations();
            for (Annotation a : annotations) {
                //获取注解  annoType:OnClick
                Class<? extends Annotation> annoType = a.annotationType();
                //获取注解的值,onClick注解上面的EventBase
                EventBase base = annoType.getAnnotation(EventBase.class);
                if (null == base) {
                    continue; //跳出本轮循环
                }

                /**
                 *拿到带注解的方法, 开始获取事件三要素 , 通过反射注入进去拿到真正的方法
                 */
                //1. 返回setOnclickListener字符串
                String listenerSetter = base.listenerSetter();
                //2. 返回View.OnClickListener字节码
                Class<?> listenerType = base.listenerType();
                //3. 返回onClick字符串
                String callMethod = base.callBackMethod();

                //保存方法名与方法的应映射, 在接下来的操作中方便使用
                Map<String, Method> methodMap = new HashMap<>();
                methodMap.put(callMethod, m);

                try {
                    //拿到注解中的value方法
                    Method value = annoType.getDeclaredMethod("value");
                    //对应value方法的返回值,这里通过反射是为了通用性,如果指定具体的类可以直接获取,但是扩展性很低
                    int[] ids = (int[]) value.invoke(a);

                    //注入事件
                    for (int viewId : ids) {
                        Method findv = aClass.getMethod("findViewById", int.class);
                        //通过反射拿到View
                        View view = (View) findv.invoke(context, viewId);
                        if (null == view) continue;

                        /**
                         * @Param listenerSetter: setOnClickListener的字符串,可以反射出方法
                         * @Param listenerType: 参数类型,为View.OnClickListener.class
                         */
                        Method setOnclick = view.getClass().getMethod(listenerSetter, listenerType);

                        ListenerInvactionHandler handler = new ListenerInvactionHandler(context, methodMap);
                        //设置返回对象的类型: proxyy是实现了OnclickListener接口,也就是listenerType接口的代理对象
                        Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(),
                                new Class[]{listenerType},
                                handler);

                        //利用代理设置监听
                        setOnclick.invoke(view,proxy);
                    }

                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }

    }

逻辑是复杂了一点 , 这里只是有一个三重的for循环 , 慢慢拆解下来 , 其实也不算很难 , 重要的注释都写在方法中了 , 去尝试一下吧 .


但是 , 懂的老铁们会说 , 你这个是运行时注解,在使用的时候,大量的反射会影响性能,是的,这个确实是一个很大的缺点,所以我们可以参考ButterKnife的做法,打造自己的编译时注解框架。

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,832评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,778评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,637评论 18 139
  • 转眼间,做大时代已经7个月了,从VIP起步做到现在的总代级别,一路顺畅,没有阻碍!很庆幸加入了大时代,因一...
    林夕冉阅读 293评论 0 0
  • 《一首诗的斟酌》 坐在桌前 迟迟写不出一个字 疲劳的心,身 磨灭了我的灵感 也许是树不够高 永远触碰不到天 诗不够...
    金书js阅读 208评论 0 0