IOC依赖注入(一)— 手写ButterKnife框架

IOC

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,其中最常见的就是依赖注入(Dependency Injection, 简称DI)

依赖注入

现在市面上越来越多的开源框架使用了依赖注入技术。什么是依赖注入呢?其实就是使用注解的方式去实现某些功能。

比如:

1.运行时注入 xUtils,eventBus,springMVC
2.源码时注入 android studio插件
3.编译时注入 butterknife,dagger2

核心思想

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

在这里插入图片描述

怎么理解呢?如上图,当我们还是一个单身狗时,出门的时候要自己去拿衣服,让后自己穿上才出门。自从有了女朋友,就再也不用这么麻烦了,让女朋友帮你拿衣服和穿衣服,完事直接出门就可以了。我们就相当于被动接收,女朋友做的就是第三方获取并使用原来的代码的一些行为。

实现

注入方式

在手撸代码之前,我们要先了注解注入的三种形式

  • 运行时注入

    运行时注入就是我们先在代码中定义好一个行为轨迹,程序运行时按照我们的轨迹来执行。今天我们主要使用运行时注入来实现一个ButterKnife框架(运行时注入性能欠佳,现在ButterKnife使用的是编译时注入的方式,不过为了更好的理解依赖注入,这里使用运行时注入来实现)。

  • 源码注入

    源码注入可以理解为as插件,比如gson format,让插件为我们生成一系列的需要的代码。

  • 编译时注入

    编译时注入就是在程序编译时生成一些列的代码,比如ARouter,GreenDao等框架,其实就是利用gradle插
    件来hook住编译时的生命周期,处理我们需要的一些事情。

注入类型

我么先拿最常见的Override注解来看

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

可以看到注解类上面有两个标记TargetRetention

  • @Target

    在定义注解类时候,需要标记个Target注解,里面传入一个ElementType枚举类,这个注解是为了标明我们定义的注解在什么地方使用。

    public enum ElementType {
    	// 在class上面打标记
        TYPE,
    	// 在成员变量打标记
        FIELD,
    	// 在方法打标记
        METHOD,
    	// 在参数打标记
        PARAMETER,
    	// 在构造方法打标记
        CONSTRUCTOR,
    	// 在本地变量打标记
        LOCAL_VARIABLE,
    	// 在注解类打标记
        ANNOTATION_TYPE,
    
    }
    

    比如我们标记了一个ElementType.Type,那么这个注解就只能用在在class上面,用在成员变量上面就会报错。

  • @Retention

    这个标记有三个参数:

    public enum RetentionPolicy {
      	// 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
        SOURCE,
    	// 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
        CLASS,
    	// 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
        RUNTIME
    }
    

    可以看到除了RUNTIME,其他两个都最终会被抛弃,所以我们要实现功能性代码的时候,就需要使用RUNTIME这个类。

撸码

一、布局文件注入

首先来写一下布局文件的注入,比如我们不想写烦人的setContentView方法,直接用个注解来搞定

@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {
	@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
    }
}

布局文件只有一个textview

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.kevin.java.JavaActivity">

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="这是依赖注入的布局文件"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


</androidx.constraintlayout.widget.ConstraintLayout>

下面开始定义这个注解类:

/**
 * 我们在class上面定义该注解,就用ElementType.TYPE
 * 实现功能性代码,不能将此注解抛弃,使用RetentionPolicy.RUNTIME
 */

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectLayout {

    int value();
}

现在知道我要出门了,衣服也有了,接下来就要创建一个女朋友,来帮我们拿衣服和穿衣服:

class InjectUtils {

    /**
     * 注入布局文件
     *
     * @param context 传入一个context,就是把activity传进来
     */
    static void injectLayout(Context context) {

        // 1. 获取当前class
        Class<?> clazz = context.getClass();
        // 2. 根据class获取class上面的InjectLayout注解
        InjectLayout annotation = clazz.getAnnotation(InjectLayout.class);
        // 判空
        if (annotation == null) return;
        
        // 3. 获取注解中的值,这里就是布局文件的id
        int layoutId = annotation.value();
        Log.i("kangf", "id === " + layoutId);
        try {
            // 4. 获取activity中的setContentView方法
            Method method = clazz.getMethod("setContentView", int.class);
            // 5. 执行setContentView方法,传入layoutId参数
            method.invoke(context, layoutId);
        } catch (Exception e) {
            Toast.makeText(context, "找不到setContentView方法", Toast.LENGTH_LONG).show();
            e.printStackTrace();
        }


    }
}

然后再activity中使用这个女朋友:

@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 让女朋友帮我穿衣服
        InjectUtils.injectLayout(this);
    }
}

其实布局文件注入大致分为5步,上面注释已经写清楚了,大致就是获取到注解类中的值,然后通过反射执行activity中的setContentView方法,很简单,这里就不再多讲了,来看一下运行效果:

在这里插入图片描述
二、View注入

明白了layout注入的原理,View注入也是相同的道理,先来定义View的注解:

@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {

    @InjectView(R.id.tv_test)
    private TextView mTextView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtils.injectLayout(this);
        
        // 别忘了创建自己的女朋友
        InjectUtils.injectView(this);

        if(mTextView == null) {
            Toast.makeText(this, "没有找到textview",Toast.LENGTH_SHORT).show();
            return;
        }

        mTextView.setText("这是通过注解获取了TextView");
    }
}

布局文件没有变化,我们通过InjectView注解获取到TextView,并修改TextView的文字。

接下来创建一个InjectView注解类:

/**
 * 因为是在成员变量上打的标记,所以使用ElementType.FIELD
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectView {

    int value();
}

女盆友的创建:

static void injectView(Context context) {
        // 1. 获取当前class
        Class<?> clazz = context.getClass();

        // 2. 获取activity中所有的成员变量
        Field[] declaredFields = clazz.getDeclaredFields();

        // 3. 开始遍历
        for (Field field : declaredFields) {
            field.setAccessible(true);
            // 4. 获取字段上面的InjectView注解
            InjectView annotation = field.getAnnotation(InjectView.class);
            // 5. 如果字段上面没有注解,就不用处理了
            if (annotation == null) {
                return;
            }
            int viewId = annotation.value();

            try {
                // 6. 获取 findViewById 方法
                Method findViewMethod = clazz.getMethod("findViewById", int.class);
                // 7. 执行方法,获取View
                View view = (View) findViewMethod.invoke(context, viewId);
                // 8. 把view赋值给该字段
                field.set(context, view);
            } catch (Exception e) {
                Toast.makeText(context, "没有找到findViewById方法", Toast.LENGTH_SHORT).show();
                e.printStackTrace();
            }

        }

    }

别忘了在activity中使用: InjectUtils.injectView(this);

思路跟注入layout差不多,获取到所有的字段,遍历并获取到字段上面的注解标记,通过注解的值获取到view, 最后把view赋值给这个字段。下面来看一下运行结果吧!

在这里插入图片描述
三、事件注入

事件注入相对比较麻烦一些了 ,安卓中有26大事件,我们可以写很多注解类,但是女盆友是用户使用的,不能创建那么多女盆友啊,这怎么办呢?这时候我们就需要通过动态代理 + 注解的方式来解决了。

首先需要创建一个总线注解,给每一个事件注解使`用,这个总线包含了事件三要素:

  • 事件源: TextViewButton
  • 事件(View.OnClickListener、View.OnLongClickListener
  • 订阅事件(setOnClickListener()setOnLongClickListener()
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EventBus {

    // 事件类型
    Class<?> eventType();
    // 方法名
    String eventMethod();
}

这个总线式应用到所有的事件注解类中的,所以这里使用了ElementType.ANNOTATION_TYPE

事件注解类就是这样的:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBus(eventType = View.OnClickListener.class, eventMethod = "setOnClickListener")
public @interface OnClick {

    int[] value();
}

比如:

点击事件就用eventType = View.OnClickListener.class, eventMethod = “setOnClickListener”

长按事件用eventType = View.OnLongClickListener.class, eventMethod = “setOnLongClickListener”

接下来必不可少的当然是创建个贴心的女朋友啦~

static void injectEvent(final Context context) {
        // 1. 开头是一样的,获取到class对象
        Class<?> clazz = context.getClass();
        // 2. 获取到所有的方法
        Method[] declaredMethods = clazz.getDeclaredMethods();
        // 3. 遍历所有的方法
        for (final Method method : declaredMethods) {
            method.setAccessible(true);
            // 4. 获取方法上的注解, 因为一个方法上面可能会有多个注解,所以要获取所有的注解
            Annotation[] annotations = method.getAnnotations();
            // 5. 遍历方法上面的注解
            for (Annotation annotation : annotations) {
                // 6. 获取这个注解上面的注解类(也就是OnClick注解的class)
                Class<? extends Annotation> aClass = annotation.annotationType();
                // 7. 根据OnClick注解的class,获取EventBus注解
                EventBus eventBus = aClass.getAnnotation(EventBus.class);
                // 8. 判断如果有EventBus注解,才代表的是事件注解,进行处理
                if (eventBus != null) {
                    // 9. 获取EventBus注解的值
                    Class<?> eventType = eventBus.eventType();
                    String eventMethodName = eventBus.eventMethod();
                    try {
                        // 10. 通过反射拿到方法注解中的值(这里就是所有view的id数组)
                        Method ids = aClass.getDeclaredMethod("value");
                        int[] viewIds = (int[]) ids.invoke(annotation);
                        if (viewIds == null) {
                            return;
                        }
                        // 11. 遍历id数组
                        for (int viewId : viewIds) {
                            // 12. 获取view
                            Method findViewMethod = clazz.getMethod("findViewById", int.class);
                            View view = (View) findViewMethod.invoke(context, viewId);

                            // 13. 如果有这个view,才进行处理
                            if (view != null) {
                                // 14. 动态代理,代理事件类型,交给我们的方法来处理
                                Object proxy = Proxy.newProxyInstance(context.getClassLoader(), new Class[]{eventType},
                                        new InvocationHandler() {
                                            @Override
                                            public Object invoke(Object proxy, Method oldMethod, Object[] args) throws Throwable {
                                                // 执行当前activity中的方法,参数不能少,需要跟原事件方法参数一样
                                                return method.invoke(context, args);
                                            }
                                        });
                                // 获取activity中的事件方法
                                Method activityEventMethod = view.getClass().getMethod(eventMethodName, eventType);
                                // 15. 当这个方法执行的时候,自动执行代理方法
                                activityEventMethod.invoke(view, proxy);
                            }
                        }


                    } catch (Exception e) {
                        Toast.makeText(context, "找不到该value方法", Toast.LENGTH_SHORT).show();
                        e.printStackTrace();
                    }


                }
            }
        }

    }

思路就是通过事件类型获取事件的类型和方法名,然后通过代理取到事件的方法,当执行事件的时候自动执行我们在activity中定义的事件方法。动态代理大家还不了解的话可以去学习一下,很简单。具体思路大家可以照着上面的注释缕一缕。

如果增加一个事件也很简单了,只需增加一个事件注解类:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@EventBus(eventType = View.OnClickListener.class, eventMethod = "setOnClickListener")
public @interface OnClick {

    int[] value();
}

在activity中使用:

@InjectLayout(R.layout.activity_java)
public class JavaActivity extends AppCompatActivity {

    @InjectView(R.id.tv_test)
    private TextView mTextView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        InjectUtils.injectLayout(this);
        InjectUtils.injectEvent(this);
    }


    @OnClick({R.id.tv_test})
    private void clickText(View view) {
        Toast.makeText(this, "点击了view", Toast.LENGTH_SHORT).show();
    }

    @OnLongClick({R.id.tv_test})
    private boolean longClickText(View view) {
       Toast.makeText(this, "长按点击了view", Toast.LENGTH_SHORT).show();
       return true;
    }
}

一个点击事件,一个长按事件,来看效果图:

在这里插入图片描述

代码都已经上传到了github,有兴趣的可以下载下来看看:

github

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿烦大大@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值