Android:xUtils3拆解笔记——视图模块详解及拓展

【转载请注明出处】
作者:DrkCore (http://blog.csdn.net/DrkCore)
原文链接:(http://blog.csdn.net/DrkCore/article/details/50922448)

如果你能点进这篇博文,说明你和笔者一样也是使用xUtils的Android开发者。作为国内老牌的框架xUtils的功能禁得起考验。在版本升级到了xUtils3后笔者果断fork了一波。

在接下来的篇幅中笔者将为你讲解xUtils的视图注入模块的实现方式,以及如何拓展出新的功能。

视图注入模块基本介绍

该模块是xUtils四大模块中最简单的一个,其所有的逻辑都在主线程中完成且基本只在界面启动时调用一次,因而将之作为拆解xUtils3框架的第一步而言再合适不过了。

在旧版本中该模块除了查找视图外还能使用注解将资源(比如String或者Drawable等)绑定到成员域上,但是xUtils3中该模块就只专心做视图注入和事件绑定了。这倒算是一件好事,因为说实话资源注入用的很少而且到要用资源时才加载会更轻快一些。

废话不多说了,让我们进入正题。

要讲视图注入模块首先要讲的肯定是注解,如果你对注解还不了解的话请点此度娘传送门自行学习,在之后的章节中默认你们已经了解了注解的基本使用方法。

在xUtils3的org.xutils.view.annotation包中可以看到我们平常使用的三个注解:ViewInject、ContentView、Event。

从ViewInject注解开始

ViewInject注解本身没什么内容:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {

    int value();

    /* parent view id */
    int parentId() default 0;
}

大体就是用两个属性标出视图的位置信息,int value()自然就是用来标志视图的id的而int parentId()是用来标志目标所在的父视图的id,这样就可以通过查找父视图来区分两个同id的视图(如果你愿意在一个xml里使用同一个id两次的话)。

注解本身只起到了标注的作用,真正的逻辑实现则写在了org.xutils.view.ViewInjectorImpl类的injectObject(Object handler, Class handlerType, ViewFinder finder)方法中。

形参中的handler是需要绑定视图的实例,其类可以是Activity、Fragment甚至是自定义ViewHolder,只要有成员域被ViewInject标注即可;

handlerType自然是handler.getClass(),不解释;

需要注意的是ViewFinder。我们知道在Androd中拥有findViewById(int)这个方法的只有View和Activity(当然,本质上Activity.findViewById其实也是来自View的)而ViewFinder是二者的装饰者,挺简单的,具体实现瞟一眼源码你就懂了。

接着让我来看看injectObject中使用ViewInject的关键代码:

        // inject view
        // 这里我们可以看到使用反射获取定义的成员域
        Field[] fields = handlerType.getDeclaredFields();
        if (fields != null && fields.length > 0) {
            for (Field field : fields) {
                //跳过无法注入或者不需要注入的字段
                Class<?> fieldType = field.getType();
                if (
                /* 不注入静态字段 */     Modifier.isStatic(field.getModifiers()) ||
                /* 不注入final字段 */    Modifier.isFinal(field.getModifiers()) ||
                /* 不注入基本类型字段 */  fieldType.isPrimitive() ||
                /* 不注入数组类型字段 */  fieldType.isArray()) {
                    continue;
                }

                // 检查该成员变量的域是否被ViewInject注解所标注
                ViewInject viewInject = field.getAnnotation(ViewInject.class);
                if (viewInject != null) {// 你看,有标注了吧,哈哈哈
                    try {
                        // 从viewInject中找出目标View的id并且使用ViewFinder来查找对应的视图
                        // 上文说的parentId()在这个地方用上了
                        View view = finder.findViewById(viewInject.value(), viewInject.parentId());
                        if (view != null) {
                            // 剩下的就是打开权限然后用反射赋值,轻车熟路
                            field.setAccessible(true);
                            field.set(handler, view);
                        } else {
                            //如果用ViewInject注解了但是找不到视图的话几乎可以肯定是编码错误,这里作者直接抛出了运行时异常
                            throw new RuntimeException("Invalid @ViewInject for "
                                    + handlerType.getSimpleName() + "." + field.getName());
                        }
                    } catch (Throwable ex) {
                        // 上面如果找不到View抛出RuntimeException的话也会到这里来然后被这个能消化Trowable的catch给吃掉
                        // 结果就是一旦找不到一个View视图注入的整个流程都将被终止掉

                        // 所以如果你用xUtils3多的话就会遇到明明是实例化XML炸了导致注入视图失败
                        // 你得到的却是因为使用了未被注入的成员导致NullPointer的坑

                        // 这里作者倒是还写了一个LogUtil用来避免输出的日志泄露
                        // 写代码久的人多少都有一个自己的LogUtil
                        LogUtil.e(ex.getMessage(), ex);
                    }
                }
            }
        } // end inject view

以上就是ViewInject注解的核心逻辑,你看,并不难。

ContentView注解是小菜

ContentView注解只有一个int value()属性,显然是用来标志xml的id的,代码太少就不贴上来了。

主要逻辑同样是在ViewInjectorImpl类中,该类中有很多inject()的重载方法,其中针对Activity的方法如下:

    // 该方法是用来注入Activity实例的
    @Override
    public void inject(Activity activity) {
        //获取Activity的ContentView的注解
        Class<?> handlerType = activity.getClass();
        try {
            // findContentView方法是定义在ViewInjectorImpl下文中的方法
            // 如你所见是几行用于获取注解的标准姿势,因篇幅有限故不展开
            ContentView contentView = findContentView(handlerType);
            if (contentView != null) {
                int viewId = contentView.value();
                if (viewId > 0) {
                    // 用反射调用Activity.setContentView(int)方法
                    // 尽管笔者一直觉得这里并没有用反射的必要
                    Method setContentViewMethod = handlerType.getMethod("setContentView", int.class);
                    setContentViewMethod.invoke(activity, viewId);
                }
            }
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }

        // setContentView之后再直接注入其他的东西
        injectObject(activity, handlerType, new ViewFinder(activity));
    }

除了Activity之外,ContentView注解还能用在Fragment上的。

旧版本的xUtils的视图注入模块让人比较诟病的一点就是没办法对Fragment进行视图注入,你只能在onCreateView()方法中自己用inflater实例化一个View返回,然后在onViewCreated里面对已经实例化的view进行注入。不少开发者由于无法忍受冗长的代码(虽然只有几行但就是不爽)从而走上了fork和魔改的不归路(包括笔者)。

好在在xUtils3里面作者明显考虑到了这一点,以下是针对Fragment的核心代码:

    // 该方法是用来注入Fragment实例的
    // 你会注意到这里的形参中除了开始的Object fragment之外还有着Layoutinflater和ViewGroup
    // 明显对应着Fragment.onCreateView()回调方法
    @Override
    public View inject(Object fragment, LayoutInflater inflater, ViewGroup container) {
        // inject ContentView
        View view = null;
        Class<?> handlerType = fragment.getClass();
        try {
            ContentView contentView = findContentView(handlerType);
            if (contentView != null) {
                int viewId = contentView.value();
                if (viewId > 0) {
                    // 在这里xUtils把我们写了无数遍的那行代码写掉了,简直就是拯救强迫症的福音
                    view = inflater.inflate(viewId, container, false);
                }
            }
        } catch (Throwable ex) {
            LogUtil.e(ex.getMessage(), ex);
        }

        // inject res & event
        // 和上面一样,准备好了内容布局之后直接上injectObject()方法注入其他的东西
        injectObject(fragment, handlerType, new ViewFinder(view));

        // 返回View的实例
        // 这样Fragment.onCreateView()方法只需要一行代码就行了
        return view;
    }

Event注解,这才是硬菜

Event的注解是视图模块的核心,可以说当你理解了这个注解的实现后自己写一个类似的功能就不再是一个难事(当然,知道轮子怎么造就行了,没必要自己造)。

想要读懂这部分的代码的话你需要Java的动态代理机制的知识实现的,如果你对此不甚了解的话可以稍后点击这个度娘传送门自行学习。

Event注解的实现主要逻辑在org.xutils.view.EventListenerManager中。

其中有一个名为DynamicHanlder的内部类,用于处理事件注入的代理逻辑,如下:

    // 事件接口的反射代理
    public static class DynamicHandler implements InvocationHandler {
        // 存放代理对象,比如Fragment或view holder
        // 这里你可以看到原作者使用了弱引用避免内存泄露
        private WeakReference<Object> handlerRef;
        // 存放代理方法
        // 比如"onClick"字符对应着被Event注解的方法method
        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();
        }

        // 对动态代理调用的任何方法都会通过这个invoke方法来执行
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object handler = handlerRef.get();
            if (handler != null) {//当hanlder还未被回收
                String eventMethod = method.getName();
                if ("toString".equals(eventMethod)) {
                    // toString特殊处理
                    // 这里如果你调用了proxy.toString()的话会产生无限递归导致栈溢出
                    return DynamicHandler.class.getSimpleName();
                }

                // 按照名字从映射关系中取出真正被映射着的那个方法
                // 比如按照"onClick"取出你的doSomething()方法
                method = methodMap.get(eventMethod);
                if (method == null && methodMap.size() == 1) {
                    // 如果映射关系中只有一个那必定是onClick的映射
                    // 这里解释了为什么Event不指定type时仍能触发onClick方法
                    for (Map.Entry<String, Method> entry : methodMap.entrySet()) {
                        if (TextUtils.isEmpty(entry.getKey())) {
                            method = entry.getValue();
                        }
                        break;
                    }
                }

                if (method != null) {
                    // 避免用户点击的频率太快
                    // 尽管笔者觉得并没有这种必要
                    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 {//反射触发被映射的方法
                        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;
        }
    }

魔改:移除被注解方法必须私有的限制

xUtils3的事件注解的实现方式虽然和原版一样但设计思想却是不同的,在xUtils3中只需@Event一个注解即可将控件的回调方法绑定到方法上。

在@Event注解类上原作者给出这样的注释:

  • 方法必须私有限定,
  • 方法参数形式必须和type对应的Listener接口一致.
  • 注解参数value支持数组: value={id1, id2, id3}

想必大家已经看出了问题所在:在JAVA中接口定义的方法都是public修饰的而控件的监听方法都是定义在接口中的,也就是说在xUtils3默认不支持比如Activity实现OnClickLIstener接口并将事件绑定到onClick(View)方法上。将工程从原版迁移到xUtils3的过程中笔者一直不明白原作者的为什么要这么设计,好在框架本身是开源的,这个限制改起来并不困难。

事件注解的逻辑在org.xutils.view.ViewInjectorImpl类的injectObject (Object, Class, ViewFinder)方法中,大概在180行的位置,关键代码如下:

                if (Modifier.isStatic(method.getModifiers())
                        || !Modifier.isPrivate(method.getModifiers())) {
                    continue;
                }

其中第二个判断条件!Modifier.isPrivate(method.getModifiers())过滤掉了所有非私有的方法,去掉即可。

魔改:添加绑定事件到无参方法的支持

使用事件注解时大家肯定写过这样的代码:

@Event(R.id.button)
private void doSomething(View v){
   // 业务逻辑
}

按照事件注解功能的第二条限制“方法形参必须和对应的Listener接口一致”而言这是推荐写法。

逻辑上而言自然没有什么问题,但是如果这个方法中的业务逻辑并不需要用到控件或者只使用成员变量中的控件时,多一个形参就会变得很多余。相信我,这是使用xUtils3时经常遇到的情况。而且如果你没有按照上一节描述的方法解除被注解方法的私有限制的话,你无法让IDE(Eclipse或者AS)帮你生成监听接口对应的方法语句,必须手动写上这个多余的View v形参。同时因为这个多余的形参在代码的其他的地方调用时必须使用doSomething(null)的形式。作为有代码洁癖的人这种情况简直不能忍,好在魔改起来并不困难。

别担心,就算不明白这些一样能魔改,因为要解除这个限制只需要两行代码

事件注解的逻辑位于org.xutils.view.EventListenerManager类中,该类行数不多却是事件注解的核心。

我们可以看EventListenerManager在120行左右有一个名为DynamicHandler的内部类,对动态代理创建的临时对象的任何方法(没错,包括toString)的调用都将跳转到DynamicHandler的invoke(Object, Method, Object[])方法中,最后使用反射调用被@Event注解的方法。如下:

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //我们可以看到这里使用了弱引用以避免内存泄漏的问题
            Object handler = handlerRef.get();
            if (handler != null) {
                //获取被代理的方法名
                String eventMethod = method.getName();
                if ("toString".equals(eventMethod)) {//toString特殊处理
                    return DynamicHandler.class.getSimpleName();
                }

                //获取被代理的方法名映射到的实际方法
                //比如从"onClick"映射到你的"doSomething"方法
                method = methodMap.get(eventMethod);

                //这里我们可以看到当被映射的方法不存在时取映射集合中唯一的那个方法
                //这个解释了为什么当注入OnClickListener时无需指明onClick方法名的问题
                //因为如果一个事件接口只有一个方法的话,在这里就可以找到它
                if (method == null && methodMap.size() == 1) {
                    for (Map.Entry<String, Method> entry : methodMap.entrySet()) {
                        if (TextUtils.isEmpty(entry.getKey())) {
                            method = entry.getValue();
                        }
                        break;
                    }
                }

                if (method != null) {
                    //一定时间内只允许在AVOID_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 {
                        //前方高能,这句代码就是反射回调你的"doSomething"方法的地方
                        //也就是我们要魔改的地方
                        //如何修改请看下文
                        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;
        }
    }

关键点在return语句中:return method.invoke(handler, args);最后的args是原监听事件对应的参数,也是这个地方限定了方法参数形式必须和type对应的Listener接口一致。

那么如何魔改就很明确了,在return语句之前加上:

//当要调用的方法无参时,将args置空
boolean noParams = method.getParameterTypes().length == 0;
args = noParams? null :args;
//这是原来的return语句
return method.invoke(handler, args);

判定被绑定的方法无参时传入将args置为null即可。

魔改:添加多类型的事件绑定

xUtils3原版当然是不支持多类型的事件绑定的,一是因为有形参必须一致的限制,二是因为一个方法上不允许有两个相同的注解。但是经过我们上面的修改之后我们可以做到这一点,唯一的限制是被注解的方法必须是无参的

注解类支持的成员类型如下:

1. 基本数据类型
2. String
3. Class
4. enum
5. Annotation
6. 以上类型的数组

通过以上第5点我们发现注解类支持将成员变量定义为注解,解决方案就在这里。首先我们在org.xutils.view.annotation包下创建一个名为MultiEvent的类,如下:

/** * 多级事件注解 * * @author DrkCore * @since 2016年1月28日13:16:41 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiEvent {

    Event[] value ();

}

之后我们来到org.xutils.view.ViewInjectorImpl类,也就是第一节里我们魔改的地方。

依旧是injectObject (Object, Class, ViewFinder)方法,该方法的主要逻辑分为两部分,上半部分是注入View视图的,下半部分就是事件注入了。我们将事件注入的部分解耦成一个新的方法,如下:

/** * 注入事件 * * @param handler * @param finder * @param method * @param event */
private static void injectEvent (Object handler, ViewFinder finder, Method method, Event event) {
    try {
        // id参数
        int[] values = event.value();
        int[] parentIds = event.parentId();
        int parentIdsLen = parentIds == null ? 0 : parentIds.length;
        //循环所有id,生成ViewInfo并添加代理反射
        for (int i = 0; i < values.length; i++) {
            int value = values[i];
            if (value > 0) {
                ViewInfo info = new ViewInfo();
                info.value = value;
                if (parentIds != null) {
                    info.parentId = parentIdsLen > i ? parentIds[i] : 0;
                }
                method.setAccessible(true);
                    EventListenerManager.addEventMethod(finder, info, event, handler, method);
            }
        }
    } catch (Throwable ex) {
        LogUtil.e(ex.getMessage(), ex);
    }
}

然后在将原本判断事件是否被@Event注解的地方修改一下,如下:

        // inject event
        Method[] methods = handlerType.getDeclaredMethods();
        if (methods != null && methods.length > 0) {
            for (Method method : methods) {
                //这个地方就是我们之前移除方法必须私有限定的限制的地方
                if (Modifier.isStatic(method.getModifiers())) {
                    continue;
                }

                //检查当前方法是否是@MultiEvent注解的方法
                MultiEvent multiEvent = method.getAnnotation(MultiEvent.class);
                if (multiEvent != null) {
                    Event[] events = multiEvent.value();
                    if (events != null && events.length > 0) {
                        //循环注入@MultiEvent标注的@Event事件
                        for (Event event : events) {
                            injectEvent(handler, finder, method, event);
                        }
                    }
                }

                //检查当前方法是否是event注解的方法
                Event event = method.getAnnotation(Event.class);
                if (event != null) {
                    //注入原来的的@Event
                    injectEvent(handler, finder, method, event);
                }
            }
        }// end inject event

这么修改之后即可实现将多事件绑定到一个方法上的写法了。比方说你的Activity中有一个refresh()的刷新的方法,每次进入界面都需要调用一次,界面上有按钮可以发起刷新,同时还有下拉刷新的功能,那么这时候你就可以这么写:

@MultiEvent({
    //添加SwipeRefreshLayout的事件注入
    @Event(value = R.id.swipeRefreshLayout,type = SwipeRefreshLayout.OnRefreshListener.class),
    //添加刷新按钮的事件注入
    @Event(R.id.refreshButton)
})
public void refresh () {}

总结

魔改之后你会发现你能够随性所欲地使用xUtils3的View模块,只要你愿意你甚至能够将事件直接绑定到Activity.finish()方法上实现一键退出的功能,但造成的后果往往是易读性和可维护性的下降。

自由的代价往往是混乱,这里笔者只是为大家提供了一种可能性,至于如何维护项目的整洁那便仁者见仁智者见智。如若日后有机会笔者会另起一篇博文,与大家分享一下笔者项目的分包经验等。

以上即是本片博文的全部内容,如有纰漏,还望赐教。

你可能感兴趣的:(github,android,xUtils3)