前言
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,表明其作用范围是注解的注解
修改InjectOnClick、InjectOnLongClick两个注解
@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拿到三个重要元素:listenerTypelistenerSettermethodName - 通过动态代理Hook
setOnXXXListener方法,传入刚刚拿到的需要注入的方法method,在invoke中实现代理return realMethod.invoke(activity, objects); - 通过反射
findViewById方法拿到view,反射listenerSetter拿到setOnXXXListener方法,调用setMethod.invoke(view, listener);方法时传入代理对象listener
总结
本文是通过反射处理运行时注解,实现几种绑定功能,实际ButterKnife项目并非是基于运行时注解,而是基于编译时注解,虽然ButterKnife在9.0版本后加入了基于运行时注解的库github.com/JakeWharton… 但由于反射的性能消耗问题,官方并不推荐将其运用到生产环境。编译时注解涉及到APT技术,在编译时即生成模板代码,避免了反射的性能损耗。