手写系列(一) | IOC注入框架--简易版ButterKnife

856 阅读6分钟

前言

IOC:Inversion of Control,控制反转。同依赖注入(DI),控制反转(IOC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。本文手写了一个简易版的IOC框架,实现几种依赖注入。

最终实现效果

实现了布局绑定@InjectLayout、视图绑定@InjectView、事件绑定@InjectOnClick@InjectOnLongClick

@SuppressLint("NonConstantResourceId")
@InjectLayout(R.layout.activity_main)
class MainActivity : AppCompatActivity() {

    @InjectView(R.id.button1)
    private lateinit var button1: Button

    @InjectView(R.id.button2)
    private lateinit var button2: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        InjectManager.injectLayout(this)
//        setContentView(R.layout.activity_main)

        InjectManager.injectView(this)
//        button1 = findViewById(R.id.button1)
//        button2 = findViewById(R.id.button2)

        InjectManager.injectEvent(this)
        /*button1.setOnClickListener(object : View.OnClickListener {
            override fun onClick(p0: View?) {
                Toast.makeText(this@MainActivity, "onClick ${p0?.id}", Toast.LENGTH_LONG).show()
            }
        })

        button2.setOnLongClickListener(object : View.OnLongClickListener {
            override fun onLongClick(p0: View?): Boolean {

                Toast.makeText(this@MainActivity, "onLongClick ${p0?.id}", Toast.LENGTH_LONG).show()

                return false
            }
        })*/
    }

    @InjectOnClick(R.id.button1)
    private fun onJhClick(view: View) {
        Toast.makeText(this@MainActivity, "onClick ${view.id}", Toast.LENGTH_SHORT).show()
    }

    @InjectOnLongClick(R.id.button1, R.id.button2)
    private fun onJhLongClick(view: View): Boolean {
        Toast.makeText(this@MainActivity, "onLongClick ${view.id}", Toast.LENGTH_SHORT).show()
        return false
    }
}

涉及到的相关技术

1. 注解

注解的声明

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

元注解

@Target
Type:类/接口
FIELD:属性
METHOD:方法
PARAMETER:参数
CONSTRUCTOR:构造方法
LOCAL_VARIABLE:局部变量
ANNOTATION_TYPE:该注解使用在另一个注解上
PACKAGE:包
@Retention
注解会在class字节码文件中存在, 可以通过反射获取到该注解的内容
SOURCE:源码级操作(检查、检测),如@override
CLASS:编译时预操作,APT技术常用
RUNTIME:运行时编译,动态获取注解并执行相关操作,如本文中运用到的三种绑定均为编译时注解
生命周期:SOURCE < CLASS < RUNTIME

注解元素

int value();	// 可以理解为调用注解时的传参

2. 动态代理

定义一个各个被代理类都需要实现的公共接口

interface People {
    fun jiao(): String
}

定义被代理类

class CEO : People {
    override fun jiao(): String {
        return "woshiCEO"
    }
}

实现InvocationHandler,实际调用代理类的方法,代理类调用方法最终会走到invoke方法中,注意invoke的触发时机是代理类对象方法被调用的时候,而不是生成代理类的时候

class AnimalHandler(private var proxyObj: Any) : InvocationHandler {
    override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
        println("method name:${method?.name} args:${Arrays.toString(args)}")
        return method?.invoke(proxyObj)
    }
}

实现动态代理,动态代理实现类中传入被代理对象,并调用被代理对象的方法

@Test
    fun test() {
        val animal = COO()

        val handler = AnimalHandler(animal)

        val proxy: People = Proxy.newProxyInstance(
            animal.javaClass.classLoader,
            animal.javaClass.interfaces,
            handler
        ) as People
        val jag = proxy.jiao()
        println(jag)
    }

注入框架实现

1. 布局文件绑定

声明注解

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

@Target类型为TYPE,表明其作用范围是类或者接口
生命周期为运行时
定义int类型注解元素,绑定时传入布局文件id

给待绑定视图添加注解

@InjectLayout(R.layout.activity_main)
class MainActivity : AppCompatActivity() {}

动态注入布局文件

核心思想:利用注解找到待注入的Activity,自己实现setContentView()方法

/**
     * 注入布局文件
     *
     * @param obj
     */
    public static void injectLayout(Object obj) {

        try {
            // 获取Activity类的注解
            InjectLayout annotation = obj.getClass().getAnnotation(InjectLayout.class);

            // 注解不为空
            if (annotation != null) {
                // 通过注解的方法获取注解的参数
                int arg = annotation.value();
                // 反射拿到setContentView方法,第二个参数为参数类型
                Method method = obj.getClass().getMethod("setContentView", int.class);
                // invoke第一个传obj,如果传null为静态方法,第二个参数:注解中获取到的R.layout.xxx
                method.invoke(obj, arg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

视图文件中初始化inject方法,取代setContentView()方法,完成注入

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        InjectManager.injectLayout(this)
//        setContentView(R.layout.activity_main)
...
}

2. View视图绑定

声明注解

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

@Target类型为FIELD,表明其作用范围是属性

给属性添加注解

@InjectView(R.id.button1)
    private lateinit var button1: Button

实现注入

核心思想:通过属性注解找到待注入属性,自己实现findViewById拿到View,并set给找到的类属性

/**
     * 注入控件
     *
     * @param obj
     */
    public static void injectView(Object obj) {
        try {

            // 拿到所有不管private/public的字段
            Field[] declaredFields = obj.getClass().getDeclaredFields();

            for (Field field : declaredFields) {
                // 拿到成员变量的注解
                InjectView fieldAnnotation = field.getAnnotation(InjectView.class);
                // 反射拿到setContentView方法,第二个参数为参数类型
                Method method = obj.getClass().getMethod("findViewById", int.class);
                if (fieldAnnotation != null) {
                    int value = fieldAnnotation.value();
                    // invoke第一个传obj,如果传null为静态方法,第二个参数:注解中获取到的R.id.xxx
                    Object findViewById = method.invoke(obj, value);
                    field.setAccessible(true);
                    // 第一个参数,field所属于的对象,findById获得的值
                    field.set(obj, findViewById);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

初始化inject方法,取代findViewById()

InjectManager.injectView(this)
//        button1 = findViewById(R.id.button1)

3. 事件绑定

与布局和视图绑定稍有不同,首先事件绑定的类型不止一种,例如OnClickListener OnLongClickListener等。另外,要让执行View.setOnXXXListener回调方法时,实际调用Activity的私有方法:private fun onJhClick(view: View) {},private fun onJhLongClick(view: View): Boolean {}

注解声明

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectOnClick {
    int[] value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectOnLongClick {
    int[] value();
}

@Target类型为METHOD,表明其作用范围是方法
特殊:需要定义基于注解的注解,用于区分不同事件中的set方法和匿名内部类及其回调方法

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectEvent {
    Class<?> listenerType();	// View.OnXXXListener
    String listenerSetter();	// setOnXXXListener
    String methodName();	// onXXX
}

@Target类型为ANNOTATION_TYPE,表明其作用范围是注解的注解
修改InjectOnClickInjectOnLongClick两个注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@InjectEvent(listenerType = View.OnClickListener.class, listenerSetter = "setOnClickListener", methodName
        = "onClick")
public @interface InjectOnClick {
    int[] value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@InjectEvent(listenerType = View.OnLongClickListener.class, listenerSetter = "setOnLongClickListener", methodName
        = "onLongClick")
public @interface InjectOnLongClick {
    int[] value();
}

给方法添加注解

@InjectOnClick(R.id.button1)
    private fun onJhClick(view: View) {
        Toast.makeText(this@MainActivity, "onClick ${view.id}", Toast.LENGTH_SHORT).show()
    }

    @InjectOnLongClick(R.id.button1, R.id.button2)
    private fun onJhLongClick(view: View): Boolean {
        Toast.makeText(this@MainActivity, "onLongClick ${view.id}", Toast.LENGTH_SHORT).show()
        return false
    }

注入

public static void injectEvent(Object obj) {

        // 取当前类的所有方法,包括public和private
        Method[] declaredMethods = obj.getClass().getDeclaredMethods();

        for (Method method : declaredMethods) {
            // 获取注解列表
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                // 获取注解的注解
                Class<? extends Annotation> annotationType = annotation.annotationType();
                InjectEvent injectEvent = annotationType.getAnnotation(InjectEvent.class);
                if (injectEvent != null) {
                    try {
                        Class<?> listenerType = injectEvent.listenerType();     // View.OnClickListener.class
                        String listenerSetter = injectEvent.listenerSetter();   // setOnClickListener
                        String methodName = injectEvent.methodName();           // onClick()

                        //创建InvocationHandler和动态代理(代理要实现listenerType,这个例子就是处理onClick点击事件)
                        ProxyListenerHandler proxyHandler = new ProxyListenerHandler(obj, method);      // method:onJhClick/onJhLongClick
                        Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, proxyHandler);

                        // 拿注解中的方法int[] value();
                        Method valueMethod = annotationType.getMethod("value");
                        // 通过value()拿id列表,参数为注解类
                        int[] idArray = (int[]) valueMethod.invoke(annotation);
                        if (idArray != null) {
                            Method findViewByIdMethod = obj.getClass().getMethod("findViewById", int.class);
                            for (int id : idArray) {
                                // 通过findViewById拿view
                                Object view = findViewByIdMethod.invoke(obj, id);
                                if (view != null) {
                                    // 调用view.setOnxxxClick(listenerType)
                                    Method setMethod = view.getClass().getMethod(listenerSetter, listenerType);
                                    setMethod.setAccessible(true);
                                    // 这里的参数被动态代理,事件触发时会回调handler的invoke()方法
                                    setMethod.invoke(view, listener);     // 不能用listenerType.newInstance()
                                }
                            }
                        }

                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            }
        }

    }
class ProxyListenerHandler implements InvocationHandler {

    private final Object activity;
    private final Method realMethod;

    public ProxyListenerHandler(Object activity, Method realMethod) {
        this.activity = activity;
        this.realMethod = realMethod;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {

        Log.i("loc", "method name = " + method.getName() + " and args = " + Arrays.toString(objects));

        // method对应:onClick(p0: View?)/onLongClick(p0: View?)
        // Object[] 对应 p0
        //将onClick/onLongClick方法的调用映射到activity中的onJhClick/onJhLongClick方法
        if (null != realMethod) {
            realMethod.setAccessible(true);
            // 这里的objects只有一项,是btn:View
            return realMethod.invoke(activity, objects);
        }

        return method.invoke(o, objects);
    }
}

核心思想:

  • 拿到当前Activity所有的方法
  • 遍历包含注解的方法并找到注解中包含InjectEvent的注解,确定需要注入的实际方法method
  • 通过注解的注解InjectEvent拿到三个重要元素:listenerType listenerSetter methodName
  • 通过动态代理HooksetOnXXXListener方法,传入刚刚拿到的需要注入的方法method,在invoke中实现代理return realMethod.invoke(activity, objects);
  • 通过反射findViewById方法拿到view,反射listenerSetter拿到setOnXXXListener方法,调用setMethod.invoke(view, listener);方法时传入代理对象listener

总结

本文是通过反射处理运行时注解,实现几种绑定功能,实际ButterKnife项目并非是基于运行时注解,而是基于编译时注解,虽然ButterKnife在9.0版本后加入了基于运行时注解的库github.com/JakeWharton… 但由于反射的性能消耗问题,官方并不推荐将其运用到生产环境。编译时注解涉及到APT技术,在编译时即生成模板代码,避免了反射的性能损耗。