Android进阶Android开发经验谈首页投稿(暂停使用,暂停投稿)

都说依赖注入,我就从实现的角度来一发,以android作为引子.

2016-05-19  本文已影响378人  a帆仔

用过诸多的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怎么办呢?


留给读者。

上一篇下一篇

猜你喜欢

热点阅读