xUtils3源码分析(二):事件的绑定

image.png

本篇是xUtils3源码解析的第二篇,主要分析xUtils3的事件绑定机制,上一篇主要分析了view的绑定机制,感兴趣的同学可以阅读:
xUtils3源码解析(一):View的绑定
另外阅读本文需要动态代理的基本知识,请参阅

亦山: Java动态代理机制详解

个人认为讲的较为简单清晰,但鉴于本人对动态代理并不精通,因此不做说明,请读者见谅。
下面还是通过xUtils3的项目例子作为解析,事件绑定的注解是通过Event这个注解类:

Event事件注解

该注解作者已经做出了比较详细的说明,我们简单补充下:

  • @Target(ElementType.METHOD)说明该注解只能用于方法上;
  • @Retention(RetentionPolicy.RUNTIME)说明注解保留到运行时;
  • int [] value()说明控件可以声明多个;
  • int [] parentId()说明需要绑定事件的控件可以在一个父布局中查找;
  • Class<?> type()就是需要的事件类,默认是click事件;
  • String setter()就是设置事件的方法,比如setOnClickListener;
  • String method()如果接口有多个方法,那么需要指定调用哪个方法;
    之后我们使用Event注解写个简单的例子,比如绑定一个按钮,弹出Toast提示:
    为按钮绑定事件例子

    首先还是调用x.view().inject(this);,之后就是将Event注解绑定在方法上,我们还是看看x.view().inject(this);做了什么:
    ViewInjectorImpl.java

    ViewInjectorImpl实现了ViewInjector接口,该接口上篇做过介绍,这里不再说明。
    其实该方法首先是判断了有没有ContentView注解,如果有的话会通过
    反射调用setContentView方法

    来给Activity设置布局,最后调用
    <pre><code>injectObject(activity, handlerType, new ViewFinder(activity));</code></pre>
    该方法的前半部分主要是用于View的绑定,上篇已经解释过,下半部分主要是用于事件的绑定,也是我们今天分析的重点:
    injectObject方法

    简单起见,只保留了下半部分的代码。
    首先我们看到第一句:
    <pre><code>Method[] methods = handlerType.getDeclaredMethods();</code></pre>
    hanlderType其实就是Activityclass,主要是获取了它所声明的所有方法,目的主要是获取添加了Event注解的方法,之后进入循环,再接着看:
    判断方法是否合法

    如果方法是静态的或不是private类型的,那么不对方法进行检查,然后对方法的注解进行检查:
    检查注解是否是Event类型

    如果Event注解存在,则获取到控件的id和父控件id两个数组:
    获取控件id

    得到数组长度:
    数组长度

    可以看到父id不写的话长度自然是0,再看:
    添加代理反射

    这里可以看到只有填写了控件的id才是能够绑定的,并且对控件id和父控件id进行配对,匹配规则是这样的:
    比如控件id1,2,
    父控件id4,5,6
    那么只匹配前2对,
    如果控件id1,2,
    父控件id4
    那么其实只匹配第1对,也就是匹配两者少的那组,但是没有父id的时候,就是0来代替了。
    子控件id和父控件id组成了ViewInfo对象,该对象主要是用来封装这一对id的。
    最后一句代码是核心:
    addEventMethod核心方法

    该方法在内部完成了事件的绑定,让我们看看这个过程:
 public static void addEventMethod(
            //根据页面或view holder生成的ViewFinder
            ViewFinder finder,
            //根据当前注解ID生成的ViewInfo
            ViewInfo info,
            //注解对象
            Event event,
            //页面或view holder对象 MainActivity
            Object handler,
            //当前注解方法 当前是我们定义的test方法
            Method method) {
        try {
            View view = finder.findViewByInfo(info);//获取需要绑定事件的view控件 当前是 btn_test1按钮

            if (view != null) {
                // 注解中定义的接口,比如Event注解默认的接口为View.OnClickListener
                Class<?> listenerType = event.type();
                // 默认为空,注解接口对应的Set方法,比如setOnClickListener方法
                String listenerSetter = event.setter();
                if (TextUtils.isEmpty(listenerSetter)) {
                    listenerSetter = "set" + listenerType.getSimpleName();
                }

                String methodName = event.method();//不写默认就是""

                boolean addNewMethod = false;
                /*
                    根据View的ID和当前的接口类型获取已经缓存的接口实例对象,
                    比如根据View.id和View.OnClickListener.class两个键获取这个View的OnClickListener对象
                 */
                Object listener = listenerCache.get(info, listenerType);
                DynamicHandler dynamicHandler = null;
                /*
                    如果接口实例对象不为空
                    获取接口对象对应的动态代理对象
                    如果动态代理对象的handler和当前handler相同
                    则为动态代理对象添加代理方法
                 */
                if (listener != null) {
                    dynamicHandler = (DynamicHandler) Proxy.getInvocationHandler(listener);
                    addNewMethod = handler.equals(dynamicHandler.getHandler());
                    if (addNewMethod) {
                        dynamicHandler.addMethod(methodName, method);//"" 和 "test"
                    }
                }

                // 如果还没有注册此代理
                if (!addNewMethod) {

                    dynamicHandler = new DynamicHandler(handler);

                    dynamicHandler.addMethod(methodName, method);

                    // 生成的代理对象实例,比如View.OnClickListener的实例对象
                    listener = Proxy.newProxyInstance(
                            listenerType.getClassLoader(),
                            new Class<?>[]{listenerType},
                            dynamicHandler);

                    listenerCache.put(info, listenerType, listener);//然后放入缓存
                }

                /**获取view控件需要绑定的方法,这里根据setOnClickListener方法名,和View.OnClickListener.class
                 获取setOnClickListener的Method对象,然后通过invoke方法,将View.OnClickListener的代理实例对象
                 设置给view控件,这样就完成了view事件的绑定,这里我们为啥需要View.OnClickListener的代理实例对象呢?
                 回想下我们手动调用的写法:
                 view.setOnClickListener(new View.OnClickListener() {
                                                    @Override
                                                    public void onClick(View v) {

                                                    }
                                                });
                 我们需要一个View.OnClickListener的实体类设置给view对象的,不能将接口直接设置给view,所以需要这个
                 代理实例对象listener,这也就是动态代理的意义所在,希望大家理解。
                 **/
                Method setEventListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
                setEventListenerMethod.invoke(view, listener);
            }
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }
    }

以上就是绑定事件的过程,还涉及到两个对象:
一个是缓存代理对象的DoubleKeyValueMap的实例类listenerCache,主要用于存放缓存的代理对象,用ConcurrentHashMap作为容器,保证并发安全,有兴趣的同学可以分析下该类,代码较清晰,这里不再分析。
一个是DynamicHandler的实现类dynamicHandler,该类的invoke方法主要用于触发真正的绑定方法,这里我们是test方法,这里我们看下是如何调用的:

public static class DynamicHandler implements InvocationHandler {
        // 存放代理对象,比如Fragment或view holder,采用弱引用,避免内存泄漏
        private WeakReference<Object> handlerRef;
        // 存放代理方法
        private final HashMap<String, Method> methodMap = new HashMap<String, Method>(1);

        private static long lastClickTime = 0;

        public DynamicHandler(Object handler) {
            this.handlerRef = new WeakReference<Object>(handler);
        }

        public void addMethod(String name, Method method) {
            methodMap.put(name, method);
        }

        public Object getHandler() {
            return handlerRef.get();
        }

        /**
         *在调用onClick方法时,会调用此方法,完成真正的test方法调用
         * @param proxy 代理对象
         * @param method 这里是onClick方法
         * @param args onClick方法的参数 View
         * @return
         * @throws Throwable
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object handler = handlerRef.get();
            if (handler != null) {

                String eventMethod = method.getName();//得到onClick方法的名字
                if ("toString".equals(eventMethod)) {//如果是toString方法直接返回本类名
                    return DynamicHandler.class.getSimpleName();
                }

                //以"onClick"作为key,获取绑定的方法,这里自然获取不到,因为Key是""
                method = methodMap.get(eventMethod);
                if (method == null && methodMap.size() == 1) {//这里做下判断
                    for (Map.Entry<String, Method> entry : methodMap.entrySet()) {
                        if (TextUtils.isEmpty(entry.getKey())) {//因为key是"",所以满足条件
                            method = entry.getValue();//这里就是获取""key对应的方法,也就是test方法了
                        }
                        break;
                    }
                }

                if (method != null) {
                    /**这里AVOID_QUICK_EVENT_SET 是个HashSet
                     * AVOID_QUICK_EVENT_SET.add("onClick");
                     * AVOID_QUICK_EVENT_SET.add("onItemClick");
                     * eventMethod 的名称是onClick,因此是VOID_QUICK_EVENT_SET
                     * 所包含的
                     */
                    if (AVOID_QUICK_EVENT_SET.contains(eventMethod)) {
                        long timeSpan = System.currentTimeMillis() - lastClickTime;
                        if (timeSpan < QUICK_EVENT_TIME_SPAN) {//防止过快点击
                            LogUtil.d("onClick cancelled: " + timeSpan);
                            return null;
                        }
                        lastClickTime = System.currentTimeMillis();
                    }

                    try {
                        //最后这里利用test方法反射直接调用,这样就完成了test的方法的调用
                        return method.invoke(handler, args);
                    } catch (Throwable ex) {
                        throw new RuntimeException("invoke method error:" +
                                handler.getClass().getName() + "#" + method.getName(), ex);
                    }
                } else {
                    LogUtil.w("method not impl: " + eventMethod + "(" + handler.getClass().getSimpleName() + ")");
                }
            }
            return null;
        }
    }

以上就是事件绑定的相关分析,通过阅读本模块源码可以学习的知识点为:
注解,动态代理和反射。
接下来将展开其他模块的分析,敬请期待...

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

推荐阅读更多精彩内容