• IOC注入框架设计<一>-------IOC思想实例剖析


    关于IOC这个概念在当年学习J2EE时早就听说过了,经典的框架当属于Spring,而在Android当中的一些现代的框架中这种思想也大量被使用着,所以接下来会对IOC的整体思想做一下认识。

    核心思想:

    概念:

    IOC是原来由程序代码中主动获取的资源,转变由第三方获取并使原来的代码被动接收的方式,以达到解耦的效果,称为控制反转。

    图解:

    对于上面的文字概念说明用一个形象的图再来阐述一下:这里举一个早上起床穿衣服的生活化的例子为例,通常情况下我们起床之后穿衣服都是自己来穿的,如下:

     

    也就相当于没有使用IOC的情况,而假如你有个小姐姐服侍你~~早上起床装衣服的流程就会变为:

    也就是此时穿衣服的动作转由小姐姐来为你执行了,这就是典型IOC的一个思想。

    实践直观了解IOC:

    在了解了IOC的概念之后, 接下来则通过具体实践来体验下IOC的魅力,ButterKnife我想人人皆皆知,它的思想其实跟IOC类似,但是是一个伪IOC,这里准备从0来写一个IOC思想的框架,其效果跟ButterKnife其实很类似,也就是对于View的初始化不用我们人工来手写findViewById了,对于事件的处理也不用手动写了,都是通过各种注解,谈到注解,肯定会用到注解处理器编译期生成代码或利用反射解析注解这俩技术之一,接下来咱们会用到哪种注解技术呢?下面开始。

    布局注入:

    这里先来弄一上简单的布局文件:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/btn_test1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:minHeight="50dp"
            android:text="test1" />
    
        <Button
            android:id="@+id/btn_test2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:minHeight="50dp"
            android:text="test2" />
    
    </LinearLayout>

    对于这个View绑定到Activity默认是用它:

    这里咱们采用注解的方式来设置布局,来体现IOC的思想,所以先添加一个注解:

    此时咱们就可以将其标到我们的Activity上,如下:

    好,照着ButterKnife的思想,此时则需要在onCreate()中进行注解解析了:

     

    而为了方便,咱们抽取一个Base,将这个注入的代码提到Base中:

     

    好,接一下对于这个注册的方法逻辑很简单,通过反射来解析这个注解再来调用设置布局的方法,如下:

    其实很简单的,但是这是IOC思想的一种体现。

    控件注入:

    当布局设置好之后,接下来则需要来解析各个布局中的控件了,接下来我们不用findViewbyId的传统方式来初始化控件,而是采用ButterKnife的方式,下面开始:

    当然还是先定义注解喽:

    然后接下来咱们使用一下该注解:

    此时则来处理这个ViewInject注解的解析了,其思想还是用反射,这里直接贴出代码了,比较简单:

    package com.android.iocstudy;
    
    import android.view.View;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    
    public class InjectUtils {
    
        public static void inject(Object context) {
            injectLayout(context);
            injectView(context);
        }
    
        /**
         * 布局注入
         */
        private static void injectLayout(Object context) {
            int layoutId = 0;
            Class<?> clazz = context.getClass();
            ContentView contentView = clazz.getAnnotation(ContentView.class);
            if (contentView != null) {
                layoutId = contentView.value();
                try {
                    Method method = context.getClass().getMethod("setContentView", int.class);
    
                    method.invoke(context, layoutId);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    
        /**
         * 控件注入
         */
        private static void injectView(Object context) {
            Class<?> aClass = context.getClass();
            Field[] fields = aClass.getDeclaredFields();
    
            for (Field field : fields) {
                ViewInject viewInject = field.getAnnotation(ViewInject.class);
                if (viewInject != null) {//如果是写了注解则开始进行控件初始化
                    int valueId = viewInject.value();
                    try {
                        Method method = aClass.getMethod("findViewById", int.class);
                        View view = (View) method.invoke(context, valueId);
    //                    View view= mainActivity.findViewById(valueId);
                        field.setAccessible(true);
                        field.set(context, view);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
    
        }
    }

    此时运行试一下效果:

    事件注入【重点】:

    回看一下目前我们的代码:

    如ButterKnife框架那样,这事件的机械式代码也可以通过注解的方式来代替,但是呢?对于控件的事件会有多种类型,如setOnClickListener()、setOnLongClickListener()、setOnItemClickListener()........那用一种什么方式能达到未来无限新增另的事件的一个完全兼容呢?说明实这块并非像上面布局和控件的注入那么简单,需要有一定的设计技巧,那下面一步步来实现我们的目的:

    首们先来实现onClick的事件,还是先来新建一个注解:

    咱们使用一下:

    好,接下来则需要将事件的行为通用化,也就是还需要将事件的行为加到onClick这个注解上,可能这句话说了等于白说,其实是对于事件的具体代码到底是setOnClickListener()还是setOnLongClickListener(),我们应该将这种事件的信息标注到这个onClick注解上,此时就需要知道事件的三要素了,如下:

     

    而如果对于长按事件呢,也可以用这三要素进行套:

    所以此时咱们再建一个注解,将上面事件的三要素定义到上面,如下:

    好,定义它在哪使用呢?当然是在我们具体事件类型的注解之上了,修改OnClick注解如下:

    是不是此时OnClick的注解信息就完全能知道事件到底是怎么来做监听的了?接下来则来处理该处理的解析了:

    接下来就是重点中的重点啦,到底解析的逻辑是怎么样的呢?下面一步步来处理:

    首先肯定得遍历所有的方法,然后判断注解上有木有EventBase这个注解,有的话则代表是事件注解,没有则不处理,如下:

    好,接下来则来获取事件三要素:

    接下来则需要根据上面的事件三要素来回调咱们自定义的方法了,这块有些小技术,下面具体来看一下:

    首先要根据我们注册事件的ViewId来获取到View,因为要给它设置事件嘛:

    这里当然还是通过反射来获取了:

    接下来咱们则需要根据事件的三要素来给要添加事件的View设置监听,也就是类似于:

    这块比较麻烦,因为最终得要调用到我们自定义的方法上来:

    这里动态代理就派上用场了,我们可以将最终要执行的方法通过代理来进行中转调用,啥意思?

    那问题又来了,如何在拦截方法中来执行我们自己的事件方法呢?当然是通过参数传递到动态代理的Handler对象啦,所以整体现实的思想明确了,下面来搞一下:

    先来定义一个InvocationHandler:

    然后接下来继续回到注入事件的代码中处理:

    /**
         * 事件注入
         */
        private static void injectEvent(Object context) {
            Class<?> clazz = context.getClass();
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                //得到方法上的所有注解
                Annotation[] annotations = method.getAnnotations();
                for (Annotation annotation : annotations) {
                    Class<?> annotionClass = annotation.annotationType();
                    EventBase eventBase = annotionClass.getAnnotation(EventBase.class);
                    //如果没有eventBase,则表示当前方法不是一个处理事件的方法
                    if (eventBase == null) {
                        continue;
                    }
                    //开始获取事件三要素信息
                    //订阅
                    String listenerSetter = eventBase.listenerSetter();
                    //事件(事件监听的类型)
                    Class<?> listenerType = eventBase.listenerType();
                    //事件处理   事件被触发之后,执行的回调方法的名称
                    String callBackMethod = eventBase.callbackMethod();
    
                    try {
                        //反射得到ID,再根据ID号得到对应的VIEW
                        Method valueMethod = annotionClass.getDeclaredMethod("value");
                        int[] viewId = (int[]) valueMethod.invoke(annotation);
                        for (int id : viewId) {
                            Method findViewById = clazz.getMethod("findViewById", int.class);
                            View view = (View) findViewById.invoke(context, id);
                            if (view == null) {
                                continue;
                            }
                            //得到ID对应的VIEW以后,开始在这个VIEW上执行监听
                            ListenerInvocationHandler listenerInvocationHandler = new ListenerInvocationHandler(context, method);
                            //生成事件监听代理proxy======View.OnClickListener()对象
                            Object proxy = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, listenerInvocationHandler);
    
                            //接下来则给View来设置监听,还是用反射来获取View中设置事件的方法
                            Method onClickMethod = view.getClass().getMethod(listenerSetter, listenerType);
                            //将代理对象设置成View的事件监听对象
                            onClickMethod.invoke(view, proxy);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
    
            }
        }

    下面针对这个代码再来理解下,其实整个思想用一个代码模拟的话就是这样:

    好的,还原Activity的代码:

    至此一个通用的事件注入代码就已经写完了,何为通用,比如我们要对按钮再增加其它事件比较长按,咱们只需要这样做:

    然后,再到我们Activity中定义长按的处理方法:

    package com.android.iocstudy;
    
    import android.os.Bundle;
    import android.view.View;
    import android.widget.Button;
    import android.widget.Toast;
    
    @ContentView(R.layout.activity_main)
    public class MainActivity extends BaseActivity {
    
        @ViewInject(R.id.btn_test1)
        private Button btn_test1;
        @ViewInject(R.id.btn_test2)
        private Button btn_test2;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
        }
    
        @OnClick({R.id.btn_test1, R.id.btn_test2})
        public void click(View v) {
            switch (v.getId()) {
                case R.id.btn_test1:
                    Toast.makeText(this, "test1 clicked!", Toast.LENGTH_SHORT).show();
                    break;
                case R.id.btn_test2:
                    Toast.makeText(this, "test2 clicked!", Toast.LENGTH_SHORT).show();
                    break;
            }
        }
    
        @OnLongClick({R.id.btn_test1, R.id.btn_test2})
        public boolean longClick(View v) {
            switch (v.getId()) {
                case R.id.btn_test1:
                    Toast.makeText(this, "test1 long clicked!", Toast.LENGTH_SHORT).show();
                    break;
                case R.id.btn_test2:
                    Toast.makeText(this, "test2 long clicked!", Toast.LENGTH_SHORT).show();
                    break;
            }
            return true;
        }
    }

    然后就可以了,啥都不需要写,此时运行一下看效果是否好用:

    这就是所谓的“通用性”,未来如果有更多的事件只需要建立对应的事件注解,然后我们再定义相应的事件方法既可,好,为了进一步的测一下此框架的通用性,咱们将这个框架用到一个对话框上面,看是否也能够用:

    新建一个Dialog:

    package com.android.iocstudy;
    
    import android.content.Context;
    import android.os.Bundle;
    import android.view.Gravity;
    import android.view.View;
    import android.view.WindowManager;
    import android.widget.Button;
    import android.widget.Toast;
    
    import androidx.annotation.NonNull;
    
    @ContentView(R.layout.my_dialog)
    public class MyDialog extends BaseDialog {
        @ViewInject(R.id.btn_dialog)
        Button dialogBtn;
    
        public MyDialog(@NonNull Context context) {
            super(context);
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            Toast.makeText(getContext(), "btn_dialog " + dialogBtn, Toast.LENGTH_SHORT).show();
        }
    
        @OnClick(R.id.btn_dialog)
        public void click(View view) {
            Toast.makeText(getContext(), "  dialog点击啦", Toast.LENGTH_SHORT).show();
        }
    
        @Override
        public void show() {
            super.show();
            /**
             * 设置宽度全屏,要设置在show的后面
             */
            WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
            layoutParams.gravity = Gravity.CENTER;
            layoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
            layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
            getWindow().setAttributes(layoutParams);
        }
    }

    同样注入也抽象到Base当中:

    package com.android.iocstudy;
    
    import android.app.Dialog;
    import android.content.Context;
    import android.os.Bundle;
    
    import androidx.annotation.NonNull;
    
    public class BaseDialog extends Dialog {
        public BaseDialog(@NonNull Context context) {
            super(context);
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            InjectUtils.inject(this);
        }
    
    }

    使用方式基本跟Activity是一样的,下面运行看一下:

    妥妥的,扩展性还是不错的,当然实际商用可能不会用自己手写的,但是手写一个对于IOC的思想的掌握是有很大的帮助作用的,其实对于这个框架还可以进一步强大,比如:

  • 相关阅读:
    maven常用命令
    项目管理需要做的事情
    jinkins 部署过程
    怎么操作会导致MySQL锁表
    高性能Java代码的规范
    java8新特性(2)--接口的默认方法
    java8新特性1--Lambda表达式
    eclipse web项目
    js 0 "" null undefined
    Android分页加载
  • 原文地址:https://www.cnblogs.com/webor2006/p/12374975.html
Copyright © 2020-2023  润新知