都说依赖注入,我就从实现的角度来一发,以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怎么办呢?


留给读者。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270

推荐阅读更多精彩内容