自己撸一个基于运行时注解的简单IOC框架

概述

日常开发中的各种注解还是比较常见的,比如代码里面的各种@Override,@Nullable,后端的话Spring里面的各种注入以及依赖注入框架Dagger.对于更关注于界面的客户端,大名鼎鼎的ButterKnife以及XUtils中的ViewInject也是基于注解实现和使用的。先不去纠结运行效率,自己来写一段简单的注解代码,了解背后的原理。

基本原理

基于注解的开发,有一个基本概念IOC(Inverse Of Control):控制反转,简单说就是new对象的事情交给框架完成。但实际上类似ButterKnife等框架要做的事情是不让开发人员写一堆的findViewById、成员变量和类型转换(Android API26以后findViewById的返回值有变化,不用再强转),要达到此效果还需要配合ButterKnife在Android Studio中的插件使用。而实现注入的基础就是反射+自定义注解。

注入实现的基础步骤

先定义一个注解,取名Inject.通常自定义注解里面要声明两个关键字@Rentention(该注解何时生效)、@Target(该注解的适用范围)。注解内部可以定义其可以接受的类型,名称名可以随意,但如果是value的话,可以省略。这里我定义为name

@Retention(RetentionPolicy.RUNTIME)     //该注解会被加载到JVM中
@Target(ElementType.FIELD)      //该注解只能用在字段上面(成员变量)
public @interface Inject {
    //代表该自定义注解类可以接受的类型
    String name();
}

新建一个JavaBean,取名Person,创建一个成员变量player,并且使用@Inject,其值为欧文。注意因为名称是name,所以不可以忽略。要用’名称=值’的形式,如果名称为value才是可以缺省的。

public class Person {

    @Inject(name="欧文")
    private String player;

    @Override
    public String toString() {
        return "Person{" +
            "player='" + player + '\'' +
            '}';
    }
}

在主方法中,通过暴力反射获取Person里的player字段,再获取该字段上的自定义注解对象(要传入具体的注解类,因为一个字段是可以跟多个注解的),进一步获得该注解对象的值,至此就可以将值赋给player字段,也就完成了对player的注入。

Person person = new Person();
Class clazz = Person.class;
Field field = clazz.getDeclaredField("name");
/*
 * 获取该字段上的自定义注解类对象
 */
Inject annotation = field.getAnnotation(Inject.class);
/*
* 获取Inject上的值
 */
String name1 = annotation.name();
field.setAccessible(true);
field.set(person, name1);
System.out.println(person);

类似的,如果要注入View到成员变量,也是这个路数,只不过相应的自定义注解的值类型是int(控件ID),当然反射获取的就是整个Activity中定义的成员变量了,因为我们对控件的声明都会写在这里

注入View到成员变量

1.自定义一个注解类ViewInject(随意命名),运行时获取控件的ID,声明在字段上:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ViewInject {
    int value();
}

2.以Activity组件示例,成员变量声明一个布局中的控件,并为其添加注解:

@ViewInject(R.id.text)
private TextView mTextView;

3.setContentView后,布局组件生效,将Activity作为参数,开始注入:
ViewUtils.inject(this);

4.开始完成ViewUtils类,也就是真正执行注入操作的地方:

private static void bindView(Activity activity) {
    //1.获取声明组件的字节码
    Class<? extends Activity> clazz = activity.getClass();
    //2.获取字节码中所有字段
    Field[] declaredFields = clazz.getDeclaredFields();
    for (Field field : declaredFields) {
        //3.遍历,筛选出只添加了@ViewInject的字段
        ViewInject annotation = field.getAnnotation(ViewInject.class);
        if (annotation != null) {
            //4.获取自定义注解上的id,通过id获取到该控件
            View view = activity.findViewById(annotation.value());
            if (view != null) {
                try {
                    //5.通过暴力反射将view赋值给该字段
                    field.setAccessible(true);
                    field.set(activity, view);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

5.Activity里验证:mTextView.setText("成功注入");

事件的绑定

以最简单的点击事件为例,展示如何通过注解实现事件的绑定。同样也是为了简单,先看怎么实现单个控件的事件绑定。

1.自定义一个注解类OnClick(随意命名),运行时获取控件的ID,声明在方法上:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnClick {

    int value();
}

2.以Activity组件示例,随便命名一个方法作为点击事件绑定的对应方法,并为其添加注解,这里的形参为空:

@OnClick(R.id.text)
private void clickView() {
}

3.完成ViewUtils类中进行事件绑定的代码部分

private static void bindOnClick(final Activity activity) {
    //1.获取声明组件的字节码
    Class<? extends Activity> clazz = activity.getClass();
    //2.获取所有声明的方法
    Method[] declaredMethods = clazz.getDeclaredMethods();
    for (final Method method : declaredMethods) {
        //3.遍历,筛选出只添加了@OnClick的字段
        OnClick annotation = method.getAnnotation(OnClick.class);
        if (annotation != null) {
            final int value = annotation.value();
            //4.获取自定义注解上的id,通过id获取到该控件
            final View view = activity.findViewById(value);
            //5.在该控件的点击事件中,通过暴力反射调用对应在Activity中声明的方法
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    try {
                        method.setAccessible(true);
                        method.invoke(activity);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

和View的注入类似,就是把本来在Activity做的setOnClickListener拿到这里面来,而通过反射调用在Activity里声明的方法,也就相当于绑定了事件。注意因为声明的方法是无参的,因此反射调用方法时可变参数的长度为0

4.Activity里验证:

@OnClick(R.id.text)
private void clickView() {
    Toast.makeText(this, "事件绑定成功", Toast.LENGTH_SHORT).show();
}
总结

一个很简单的IOC框架完成了,但是距离实际使用差的还多,一个是基于运行时的注解通过反射完成,效率还是多少受了影响,虽然实际上是感受不出来的。(SUN已经做了大量优化)。更主要的是这样在开发中只是换了中形式,如果布局中有一堆需要操作的控件,写一堆@ViewInject和各种控件Id,而且还要自己声明变量,并没有提高多少效率。看看耳熟能详的ButterKnife,其实是基于编译时解析的技术。后续

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值