Android中手撸IOC框架

作者: MinuitZ | 来源:发表于2018-03-30 14:36 被阅读46次

在刚接触安卓的第二天 , 自己最熟悉的代码 , 就是那句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的做法,打造自己的编译时注解框架。

相关文章

网友评论

本文标题:Android中手撸IOC框架

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