都说依赖注入,我就从实现的角度来一发,以android作为引子.
用过诸多的view注入的框架,例如xutils,butterknife,KJLibraray,Guice等,你了解过如何实现吗?
从零来一发, 今天老司机为新来者带带路~其他老司机略过
从demo上,我只实现两个功能@InjectView,@OnClick。前者注入view,后者注入点击事件。其他的实现角度上是一样的,大家可以一起探讨一下!
还是老套路,先分析:
1.我需要两个注解类@InjectView和@OnClick
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectView {
public @IdRes int value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
public @IdRes int[] value();
}
简单提一下:
@Target的ElementType中
public enum ElementType {
TYPE, //加在类上的注解
FIELD, //加在类中属性上的注解
METHOD, //加在类中方法上的注解
PARAMETER, //加在参数上的注解,可以是方法内的参数
CONSTRUCTOR, //加在构造函数上的注解
LOCAL_VARIABLE, //加在方法内变量上的注解
ANNOTATION_TYPE,
//加在注解上的注解
PACKAGE
//加在包名上的注解
}
@Retention中:
public enum RetentionPolicy {
SOURCE, //只保留在源码中
CLASS, //保留在类字节码中
RUNTIME //保留在运行时(我们自定义注解一般都是在运行时检测的)
}
ok,继续,
@InjectView中只接收一个int类型的值,用于表示view的id,
@OnClick中接收一个int[],表示可以接收多个view的id,绑定到同一个click执行方法上
既然有了注解,就少不了注解的解释者,
先分析一个view赋值的过程:
1. 首先要有一个rootView,用于findView
2. 还需要有目标view的id,这个就是@InjectView中的值
3. 需要目标对象
4. 把从rootView找到的view赋值给目标对象的目标变量
ok,看代码
public class InjectViewProcessor implements ProcessorIntf<Field>{
@Override
public boolean accept(AnnotatedElement e) {
return e.isAnnotationPresent(InjectView.class);
}
@Override
public void process(Object object, View view, Field field) {
InjectView iv = field.getAnnotation(InjectView.class);
final int viewId = iv.value();
final View v = view.findViewById(viewId);
field.setAccessible(true);
try {
field.set(object, v);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
怎么还实现了一个接口呢?
public interface ProcessorIntf<T extends AnnotatedElement> {
public boolean accept(AnnotatedElement t);
public void process(Object object, View view, T t);
}
这个泛型接口接收一个AnnotatedElement的子类,它是什么呢?
在java中,Field,Method,Constructor...一切可注解的对象都实现了AnnotatedElement接口。ProcessorIntf用于给解析器提供一系列通用行为:
/*
* 每个不同的处理器都会通过这个方法来告诉最终的调度者,这个注解是否由我来
* 处理
*/
public boolean accept(AnnotatedElement t);
process方法换个角度一想,无论是@InjectView,@InjectString,@OnClick等等任何注入的操作,是不是应该都需要这几个条件呢?所以:
/*这样看来,可以把处理行为抽象成这几个参数?
*第一个object是目标对象,
*第二个view是根view
*第三个是加上注解的那个东西
*/
public void process(Object object, View view, T e);
所以在InjectViewProcessor中是这样实现的:
@Override
public boolean accept(AnnotatedElement e) {
//如果当前这个AnnotatedElement实例加有InjectView注解,则返回true
return e.isAnnotationPresent(InjectView.class);
}
如果是返回true,说明这个它可以处理,则走到
@Override
public void process(Object object, View view, Field field) {
InjectView iv = field.getAnnotation(InjectView.class);
final int viewId = iv.value();
final View v = view.findViewById(viewId);
field.setAccessible(true);
try {
field.set(object, v);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
代码很简单,就简单说明下:
1.先拿到具体的注解类,并拿到里面的值比如R.id.txt等
2.从跟view中findViewById拿到指定的view
3.为防止目标属性是private的,将可访问设置为true,并赋值
-.ok,这样一个view就被注入到目标属性中了
接着再分析@OnClick的实现:
1.我需要知道要绑定点击事件的view的id,这个在@OnClick中的value指定
2.和之前的一样,也需要一个根view来拿到具体的view
3.要注入的对象,这个是必须的,
4.要把某个方法绑定为点击事件的回调,我还要知道是哪个方法
5.ok上述的条件都满足后,就可以拿到find来的view并设置setOnClickListener,在收到回调的时候,去调用指定方法,来实现间接的绑定
好了,分析就到这里面,看代码:
public class OnClickProcessor implements ProcessorIntf<Method> {
@Override
public boolean accept(AnnotatedElement e) {
return e.isAnnotationPresent(OnClick.class);
}
@Override
public void process(Object object,View view, Method method) {
final OnClick oc = method.getAnnotation(OnClick.class);
final int[] value = oc.value();
for (int id : value) {
view.findViewById(id).setOnClickListener(new InvokeOnClickListener(method,object));
}
}
private static class InvokeOnClickListener implements View.OnClickListener {
public Method method;
public WeakReference<Object> obj;
private boolean hasParam;
InvokeOnClickListener(Method m, Object object) {
this.method = m;
this.obj = new WeakReference<Object>(object);
final Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes == null || parameterTypes.length == 0) {
hasParam = false;
} else if (parameterTypes.length > 1 || !View.class.isAssignableFrom(parameterTypes[0])) {
throw new IllegalArgumentException(String.format("%s方法只能拥有0个或一个参数,且只接收View", m.getName()));
} else {
hasParam = true;
}
}
@Override
public void onClick(View v) {
//点击事件触发了
Object o = obj.get();
if (o != null) {
try {
if (hasParam) {
method.invoke(o, v);
} else {
method.invoke(o);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
再来分析代码:
//这个很简单,就是告诉管理器我响应OnClick注解
@Override
public boolean accept(AnnotatedElement e) {
return e.isAnnotationPresent(OnClick.class);
}
/*
* 这个也还是一样,
* 1.先拿到具体的注解对象 ,并拿到里面的值
* 2.因为存在多个id绑定到一个方法上的情况,所以一个循环不可少
* 3.就是拿到view,设置监听事件
* 4.但是,这个InvokeOnClickListener是个什么东西呢?
*/
@Override
public void process(Object object,View view, Method method) {
final OnClick oc = method.getAnnotation(OnClick.class);
final int[] value = oc.value();
for (int id : value) {
view.findViewById(id).setOnClickListener(new InvokeOnClickListener(method,object));
}
}
//先说下,这里面的InvokeOnClickListener是一个中间件,注册给系统,系统在得到点击事件后,通知给InvokeOnClickListener,在这个里面再调用你所指定的方法。
private static class InvokeOnClickListener implements View.OnClickListener {
public Method method;
public WeakReference<Object> obj;
private boolean hasParam;
InvokeOnClickListener(Method m, Object object) {
this.method = m;
this.obj = new WeakReference<Object>(object);
final Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes == null || parameterTypes.length == 0) {
hasParam = false;
} else if (parameterTypes.length > 1 || !View.class.isAssignableFrom(parameterTypes[0])) {
throw new IllegalArgumentException(String.format("%s方法只能拥有0个或一个参数,且只接收View", m.getName()));
} else {
hasParam = true;
}
}
@Override
public void onClick(View v) {
//点击事件触发了
Object o = obj.get();
if (o != null) {
try {
if (hasParam) {
method.invoke(o, v);
} else {
method.invoke(o);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
ok,构造函数中,拿到了要回调的方法,和目标对象。这是必须的。
然后就是几个简单的判断 :
1.先拿到方法的参数,看看有没有参数 , 没有就纪录下hasParam为false,
2.有参数的话,判断是几个参数,超过两个了直接就报错吧,那多的参数我从哪里给呢。
3.ok很听话的只接收一个View,hasParam为true
@Override
public void onClick(View v) {
//点击事件触发了
Object o = obj.get(); //为什么要用一个WeakReference,其实没有必要,因为activity消亡了,view也就消亡了,这个循环引用似乎不存在,但是我还是写下,假如有假如呢。
if (o != null) {
try {
if (hasParam) { //有参数,就把view传过去
method.invoke(o, v);
} else { //没有参数就直接调
method.invoke(o);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
ok,InjectView和OnClick到这里就已经完工了, 但是直接拿来用,却是有点不方便的,于是加一个管理者:
public class Injector {
private static List<? extends ProcessorIntf<? extends AccessibleObject>> chain = Arrays.asList(new InjectViewProcessor(), new OnClickProcessor());
public static void inject(Activity act) {
inject(act,act.getWindow().getDecorView());
}
public static void inject(Object obj, View rootView) {
final Class<?> aClass = obj.getClass();
final Field[] declaredFields = aClass.getDeclaredFields();
for (Field f : declaredFields) {
doChain(obj,f,rootView);
}
final Method[] declaredMethods = aClass.getDeclaredMethods();
for (Method m : declaredMethods) {
doChain(obj, m, rootView);
}
}
private static void doChain(Object obj,AccessibleObject ao, View rootView) {
for (ProcessorIntf p : chain) {
if(p.accept(ao)) p.process(obj,rootView,ao);
}
}
}
管理者很简单,提供了两个静态方法,一个给activity用, 一个可以给fragment,viewholder等任何对象用。其实最终用的也是同一个方法。
这里我用了一个处理器链的方式,假如后面我还要实现注入@string/xx,@color/xxx , @Service private WindowManager wm;等等等,把实现好的处理器加入到chain链中即可。
//这个就是前面已经说过的,把每个遍历到的方法或者属性,甚至是构造方法,类等等通过处理器链来询问这个注解你accept吗?接受则交给它来处理,
private static void doChain(Object obj,AccessibleObject ao, View rootView) {
for (ProcessorIntf p : chain) {
if(p.accept(ao)) p.process(obj,rootView,ao);
}
}
}
好了,长篇大论结束,最后贴下最终的调用:
public class TestActivity extends AppCompatActivity {
@InjectView(R.id.txt)
private TextView txt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_activity);
Injector.inject(this);
}
@OnClick({R.id.btnTestOne,R.id.btnTestTwo})
public void btnTestOne(Button view) {
final int id = view.getId();
if (id == R.id.btnTestOne) {
txt.setText("按钮一被点击");
}else{
txt.setText("按钮二被点击");
}
}
}
ok,如果我要实现一个注入contentView怎么办呢?
留给读者。