Android-IOC代码思路实现(布局注入、控件注入、事件注入)


IOC思想,现在很多框架中都有使用,,如Butterknife、Dagge、Xutils、Retrofit等等

简介

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

图解

IOC之前,就好像自己的事情自己主动做;
IOC之后,就好像自己的事情由别人去做,相当于被动接受;
代码中IOC,相信用过上面讲的框架的人,大概都知道了,注解标识…
在这里插入图片描述
移动端IOC框架中常用到的用途:布局注入、控件注入、事件注入
下面代码使用到的是运行时反射的方式

下面我们来看代码吧

布局注入

布局注入个人认为是这三个注入中最简单的;
像我们Activity中设置布局,是setContentView(id)
相信大家都不陌生吧?…
我们来通过IOC来做

自定义注解

1、自定义注解

我们自定义一个注解,这个注解我们作用在类上面,运行时可操作访问;
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到;
@Target(ElementType.TYPE) //注解所修饰的对象范围 ElementType.TYPE作用在类上面;
定义了一个int类型的value,用于接收布局

package com.lk.myioc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解 用来设置布局
 */
@Retention(RetentionPolicy.RUNTIME) //生命周期   RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.TYPE)   //注解所修饰的对象范围   ElementType.TYPE作用在类上面
public @interface ContentView {
    int value();    //注解里面的值  int类型

}

使用注解

2、在Activity上应用出这个注解

@ContentView(R.layout.activity_main)
自定义注解类@Target设置的是作用在类上面
括号里面传入的是布局

package com.lk.myioc;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

/**
 * 我们自己编写的自定义注解类ContentView
 */
@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i("MainActivity",btn1.toString());
        Log.i("MainActivity",btn2.toString());
    }
}

编写注入代码

3、接下来我们来编写注入的代码

1、通过反射获取到类对象;
2、得到这个类对象里面的ContentView注解;
3、判断了当前这个类是否有这个注解;
4、如果有这个注解,就获取这个注解里面定义的value的值(里面是我们传入进来的布局id);
5、反射获取到setContentView这个方法;
6、执行setContentView这个方法;
里面一些判断这里就省略了,自己在项目中用的时候,记得要加上各种判空…

package com.lk.myioc;

import android.view.View;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 注入的工具类
 */
public class InUtils {
    public static void inContextView(Object context){
        //反射获取类
        Class<?> clazz = context.getClass();
        //注入功能就在这里完成
        //布局注入
        initLayout(clazz,context);
    }

    /**
     * 布局注入
     * @param clazz
     * @param context
     */
    private static void initLayout(Class<?> clazz,Object context) {
        int layoutId = 0;
        //得到这个类的ContentView注解
        ContentView contentView = clazz.getAnnotation(ContentView.class);
        //如果当前这个类里面有这个注解
        if(contentView!=null){
            //得到注解中的value值
            layoutId = contentView.value();
            //反射去执行setContentView(id)
            try {
                //获取到这个方法
                //第一个参数是方法名;第二个参数是方法的参数类型
                Method setContentView = clazz.getMethod("setContentView", int.class);
                //执行这个方法
                //第一个参数是方法的对象
                //后面的参数是这个方法需要传递进去的参数
                setContentView.invoke(context,layoutId);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

控件注入

findViewById(id) 这个方法获取控件
相信大家都不陌生吧?…
我们来通过IOC来做

自定义注解

1、自定义注解

我们自定义一个注解,这个注解我们作用在字段变量上面,运行时可操作访问;
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到;
@Target(ElementType.FIELD) //注解所修饰的对象范围 ElementType.FIELD 作用在变量上面;
定义了一个int类型的value,用于接收控件id

package com.lk.myioc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解 用来findViewById
 */
@Retention(RetentionPolicy.RUNTIME) //生命周期   RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.FIELD)   //注解所修饰的对象范围   ElementType.FIELD  作用在变量上面
public @interface FindView {
    int value();    //注解里面的值  int类型

}

使用注解

2、在Activity上应用出这个注解

@FindView(R.id.app_btn)
自定义注解类@Target设置的是作用在字段变量上面
括号里面传入的是控件的id

@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {

    @FindView(R.id.app_btn)
    private Button btn1;
    @FindView(R.id.app_btn1)
    private Button btn2;

	@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i("MainActivity",btn1.toString());
        Log.i("MainActivity",btn2.toString());
    }
}

编写注入代码

3、接下来我们来编写注入的代码

1、通过反射获取到类对象;
2、这里我们是要获取这个类里面的所有字段变量,这样我们才能一个个去找出标有FindView注解的字段变量
3、通过getDeclaredFields获取到所有的字段变量,所以得到的是个数组,所以我们需要遍历这个字段变量的数组;
4、老规矩,我们肯定是要挑选出标有FindView这个注解的字段变量,所以通过getAnnotation获取到注解,进行注解的判断;
5、如果有这个注解,就获取这个注解里面定义的value的值(里面是我们传入进来的控件id);
6、反射获取到findViewById这个方法;
7、执行findViewById这个方法,并返回了一个View;
8、下面我们就需要将这个获取到的view值设置到该字段变量上面去了;
9、因为有的可能是私有的字段变量,所以我们先忽略修饰符权限,setAccessible(true)
10、然后给字段变量赋值;
里面一些判断这里就省略了,自己在项目中用的时候,记得要加上各种判空…

package com.lk.myioc;

import android.view.View;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 注入的工具类
 */
public class InUtils {
    public static void inContextView(Object context){
        //反射获取类  因为这里会传入Activity的上下文  所以这里拿到的是Activity类
        Class<?> clazz = context.getClass();
        //注入功能就在这里完成
        //控件注入
        initView(clazz,context);
    }
     /**
     * 控件注入
     * @param clazz
     * @param context
     */
    private static void initView(Class<?> clazz,Object context) {
        //拿到这个类所有的字段
        Field[] fields = clazz.getDeclaredFields();
        //遍历所有的变量字段
        for (Field field : fields) {
            //拿到变量字段上标识的FindView自定义注解
            FindView findView = field.getAnnotation(FindView.class);
            //如果字段上标有这个注解  我们就注入
            if(findView!=null){
                //如果有  我们就获取里面的value字段的值
                int viewId = findView.value();
                try {
                    //反射获取findViewById这个方法
                    Method findViewById = clazz.getMethod("findViewById", int.class);
                    //反射去执行findViewById这个方法
                    View view = (View) findViewById.invoke(context,viewId);
                    //在访问时会忽略访问修饰符,可以通过反射获取私有变量的值
                    field.setAccessible(true);
                    //给这个变量字段设置值
                    field.set(context,view);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
 }

事件注入

事件注入就比较的复杂了,因为有很多中事件,这里我们就拿两种事件来做个例子;
但是我们要考虑到以后的扩展性,我们不可能每新加一种事件,就去写一个注入吧?
接下来我们来看看代码

自定义注解

1、自定义注解

我们自定义一个注解,这个注解我们作用在字段变量上面,运行时可操作访问;
@Retention(RetentionPolicy.RUNTIME) //生命周期 RetentionPolicy.RUNTIME程序运行时去使用可找到;
@Target(ElementType.METHOD) //注解所修饰的对象范围 ElementType.METHOD 作用在方法上面;
定义了一个int数组类型的value,用于接收控件id,记住这里可能是多个id
有没有发现与上面两个注解的不同?是不是多了一个**EventBase**注解?

点击事件的注解

package com.lk.myioc;

import android.view.View;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解 用来处理OnClick事件
 */
@Retention(RetentionPolicy.RUNTIME) //生命周期   RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.METHOD)   //注解所修饰的对象范围   ElementType.METHOD  作用在方法上面
//自定义的父注解   应用在注解类上面的
//listenerSetter   订阅关系
//listenerType     事件类
//callBackMethod   事件处理的方法名
@EventBase(listenerSetter = "setOnClickListener",
            listenerType = View.OnClickListener.class,
            callBackMethod = "onClick")
public @interface OnClick {
    int[] value() default -1;    //注解里面的值  int类型数组,默认值为-1
}

长按事件的注解

package com.lk.myioc;

import android.view.View;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解 用来处理OnLongClick事件
 */
@Retention(RetentionPolicy.RUNTIME) //生命周期   RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.METHOD)   //注解所修饰的对象范围   ElementType.METHOD  作用在方法上面
//自定义的父注解   应用在注解类上面的
//listenerSetter   订阅关系
//listenerType     事件类
//callBackMethod   事件处理的方法名
@EventBase(listenerSetter = "setOnLongClickListener",
        listenerType = View.OnLongClickListener.class,
        callBackMethod = "onLongClick")
public @interface OnLongClick {
    int[] value() default -1;    //注解里面的值  int类型数组,默认值为-1
}

我们再来看看这个EventBase这个注解

@Target(ElementType.ANNOTATION_TYPE) //注解所修饰的对象范围 ElementType.ANNOTATION_TYPE 作用在注解类上面
我们定义了三个属性:
1、****String listenerSetter(); 订阅关系,如**setOnClickListener**,但是我们为什么需要的是String字符串类型?
2、****Class<?> listenerType();事件, 如new View.OnClickListener(),我们这里Class类型;
3、****String callBackMethod();事件处理程序,比如onClick(View v),我们这里是String字符串类型;

package com.lk.myioc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 该注解在另一个注解上使用
 * 相当于父注解
 */
@Retention(RetentionPolicy.RUNTIME) //生命周期   RetentionPolicy.RUNTIME程序运行时去使用可找到
@Target(ElementType.ANNOTATION_TYPE)   //注解所修饰的对象范围   ElementType.ANNOTATION_TYPE  作用在注解类上面
public @interface EventBase {
//        btn1.setOnClickListener(new View.OnClickListener() {
//            @Override
//            public void onClick(View v) {
//
//            }
//        });

    //1、setOnClickListener   订阅关系
    String listenerSetter();
    //2、new View.OnClickListener()  事件
    Class<?> listenerType();
    //3、onClick(View v)     事件处理程序
    String callBackMethod();
}

使用注解

2、在Activity上应用出这个注解

@OnClick({R.id.app_btn,R.id.app_btn1})是我们需要注入的点击事件,我们在注解中考虑了多个控件的点击事件,记得吗?当时定义的属性是个int类型的数组,所以这里可以传入多个控件的id;
@OnLongClick(R.id.app_btn)使我们需要注入的长按事件,注解里面定义的属性也是数组,但是数组可以有多个或者1个,所以我们这里可以只传1个控件的id;

package com.lk.myioc;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

/**
 * 我们自己编写的自定义注解类ContentView
 */
@ContentView(R.layout.activity_main)
public class MainActivity extends BaseActivity {

    @FindView(R.id.app_btn)
    private Button btn1;
    @FindView(R.id.app_btn1)
    private Button btn2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i("MainActivity",btn1.toString());
        Log.i("MainActivity",btn2.toString());
    }

    @OnClick({R.id.app_btn,R.id.app_btn1})
    public void click(View view){
        switch (view.getId()){
            case R.id.app_btn:
                Toast.makeText(this,"第一个按钮点击了",Toast.LENGTH_LONG).show();
                break;
            case R.id.app_btn1:
//                Toast.makeText(this,"第二个按钮点击了",Toast.LENGTH_LONG).show();
                NewsDialog newsDialog = new NewsDialog(MainActivity.this);
                newsDialog.show();
                break;
        }
    }

    @OnLongClick(R.id.app_btn)
    public boolean longClick(View view){
        Toast.makeText(this,"第一个按钮长按了",Toast.LENGTH_LONG).show();
        return true;
    }
}

编写注入代码

3、接下来我们来编写注入的代码

1、通过反射获取到类对象;
2、获取这个类对象的所有方法函数,得到的是个方法的数组,我们来遍历它;
3、因为一个方法上面可能会有多个注解,所以我们这里获取了方法上面的所有注解
Annotation[] annotations = method.getAnnotations();
4、遍历这个方法上面的所有注解
5、查看这个注解类里面是否标有我们的注解EventBase这个注解,因为标有这个EventBase注解的,才是我们自己定义的事件注解,如果没有标有这个注解的,我们不做处理直接continue进行下一个注解的查找;
6、如果是我们的事件注解,我们就获取里面的订阅关系的字符串、事件类对象、以及事件处理程序的字符串;
7、拿到这个事件注解里面的id数组进行遍历;
这里我们就得考虑个问题,怎么样才能够在执行这个事件的时候,能回调出发到我们自己的这个标有自定义事件注解的方法上面了?
答案是动态代理

我们先看看代理类

package com.lk.myioc;

import android.util.Log;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 动态代理类
 * 我们要代理事件
 * 为什么要代理?
 * 因为我们不需要将事件转嫁到Activity的方法上面去
 *     @OnClick({R.id.app_btn,R.id.app_btn1})
 *     public void click(View view){
 *
 *     }

 */
public class ListenerInvocationHandler implements InvocationHandler {
    //代理类
    private Object activity;
    //代理类里面的代理方法
    private Method activityMethod;

    public ListenerInvocationHandler(Object activity, Method activityMethod) {
        this.activity = activity;
        this.activityMethod = activityMethod;
    }

    /**
     * 点了按钮后,执行这个方法   onClick
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //在这里去调用被注解的事件方法  click
//        @OnClick({R.id.app_btn,R.id.app_btn1})
//        public void click(View view){
//
//        }
        return activityMethod.invoke(activity,args);
    }
}

我们再来看看这个注入的方法

package com.lk.myioc;

import android.view.View;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 注入的工具类
 */
public class InUtils {
    public static void inContextView(Object context){
        //反射获取类  因为这里会传入Activity的上下文  所以这里拿到的是Activity类
        Class<?> clazz = context.getClass();
        //注入功能就在这里完成
        //事件注入
        initClick(clazz,context);
    }
/**
     * 事件注入
     * @param clazz
     * @param context
     */
    private static void initClick(Class<?> clazz,Object context) {
        //获取这个类里面的所有方法
        Method[] methods = clazz.getDeclaredMethods();
        //遍历所有方法
        for (Method method : methods) {
            //拿到方法上面所有的注解,因为一个方法上面可能会存在多个注解
            Annotation[] annotations = method.getAnnotations();
            //遍历方法上面的所有注解
            for (Annotation annotation : annotations) {
                //怎样找到这个注解标识是事件了?
                //思路:查看这个注解类里面是否标有我们的父注解EventBase这个注解
                //拿到这个注解类型
                Class<?> annotationClass = annotation.annotationType();
                EventBase eventBase = annotationClass.getAnnotation(EventBase.class);
                //如果没有EventBase这个注解标识  我们不做处理 继续下一个注解
                if(null==eventBase){
                    continue;
                }
                //否则就是事件注解
                 //获取  注解上  订阅关系的字符串
                String listenerSetter = eventBase.listenerSetter();
                //获取  注解上  事件的类
                Class<?> listenerType = eventBase.listenerType();
                //获取  注解上  事件处理程序的字符串
                String callBackMethod = eventBase.callBackMethod();

                Method valueMethoid = null;

                try {
                    //反射得到id,再根据id得到对应的view
                    //    @OnClick({R.id.app_btn,R.id.app_btn1})
                    valueMethoid =annotationClass.getDeclaredMethod("value");
                    int[] viewId = (int[]) valueMethoid.invoke(annotation);
                    //对每一个Id都需要事件绑定
                    for (int id : viewId) {
                        //找到id对应的对象
                        Method findViewById = clazz.getMethod("findViewById", int.class);
                        View view = (View) findViewById.invoke(context, id);
                        //如果view不存在  就不做操作
                        if(view==null){
                            continue;
                        }
                        //得到id对应的view以后
                        //开始在这个view上执行监听(使用代理,去代理事件)
                        //需要去执行activity上的时间处理方法
                        //activity==context     需要执行的方法==method
                        //拿到这个代理对象
                        ListenerInvocationHandler listenerInvocationHandler =
                                new ListenerInvocationHandler(context, method);
                        //开始代理这个接口
                        //比如如果是点击事件View.OnClickListener()===listenerType
                        // 第一个参数是需要代理的类的类加载器
                        // 第二个参数是动态代理类需要实现的接口
                        // 第三个参数是哪个类来代理   动态代理方法在执行时,会调用h里面的invoke方法去执行
                        Object proxy= Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, listenerInvocationHandler);
                        //根据字符串方法名获取到方法listenerSetter
                        //第一个参数是方法名setOnClickListener,第二个参数是方法的参数类型View.OnClickListener.class
                        //例如是这样:setOnClickListener(new View.OnClickListener() {})
                        Method listenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                        //方法执行到代理类里面去
                        //这里代理的是new View.OnClickListener() {}  ====proxy
                        listenerMethod.invoke(view,proxy);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

我们再来看看BaseAtivity的使用

package com.lk.myioc;

import android.app.Activity;
import android.os.Bundle;

import androidx.annotation.Nullable;

public class BaseActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //调用注入的方法
        InUtils.inContextView(this);
    }
}

如果需要扩展添加其他事件的时候,自己再去编写其他的事件注解类;记得修改成对应的订阅关系字符串、事件类、事件处理程序

以上就是简单的IOC注入的代码实现思路,每行代码已经加好注释,如有问题欢迎指出,大神勿喷,谢谢

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值