Android iOS开发知识库

一个关于RadioGroup的坑

2017-08-31  本文已影响333人  chrnie

RadioGroup应该算是一个很常用的控件了,用于作为RadioButton的父控件,可以实现单选框。然而最近用了类似flux的单向数据流架构后,再使用RadioGroup立马遇到了一个大坑。

类flux架构.png

架构如上图,View的状态由ViewHolder中的变量决定,而ViewHolder中值的修改由用户输入触发控件的各种OnChangeListener改变。这里用到的是RadioGroup.OnChangeListener。

void onCheckedChanged (RadioGroup group, int checkedId)

onCheckChanged中int checkedId官方给的解释是:the unique identifier of the newly checked radio button。也就是说,在RadioGroup中一旦有RadioButton.checked改变了,就可以通过这个Listener获取到通知。

    public void check(@IdRes int id) {
        // don't even bother
        if (id != -1 && (id == mCheckedId)) {
            return;
        }

        if (mCheckedId != -1) {
            setCheckedStateForView(mCheckedId, false);
        }

        if (id != -1) {
            setCheckedStateForView(id, true);
        }

        setCheckedId(id);
    }

RadioGroup.check函数可以修改子控件的check值,并且在进入函数的第一行就做了去重处理,防止Listener触发check函数,check函数又触发Lisnter导致无限调用地狱。这一切看着是如此完美,然后这就是坑的开始。一旦按照这个架构搭建好之后,如果只是用户触发该RadioGroup中的RadioButton触发值的刷新,这个流程完全没有任何问题。但是使用该架构,为的就是使UI能实时响应ViewHolder中的值更改,随时刷新页面。例如在一个其它的页面也能设置一个这个选项值,导致了Model的更新,Model的更新触发了ViewHolder的更新,ViewHolder的更新触发了RadioGroup.check函数。这时就会出现ANR,按前面的分析,一切都是那么的完美,不科学啊!而且通过单步调试,发现onCheckedChanged中收到的checkedId居然是错误的,百思不得其解啊!
这时,就需要看看RadioGroup的工作原理了。在RadioGroup.check函数中,setCheckedStateForView(mCheckedId, false);这行调用触发了上一个被checked的RadioButton.check函数。下面是RadioButton.check函数的源码:

    public void setChecked(boolean checked) {
        if (mChecked != checked) {
            mChecked = checked;
            refreshDrawableState();
            notifyViewAccessibilityStateChangedIfNeeded(
                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);

            // Avoid infinite recursions if setChecked() is called from a listener
            if (mBroadcasting) {
                return;
            }

            mBroadcasting = true;
            if (mOnCheckedChangeListener != null) {
                mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
            }
            if (mOnCheckedChangeWidgetListener != null) {
                mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
            }

            mBroadcasting = false;            
        }
    }

其中可以看到 mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);这行调用通知了RadioGroup,说我的checked值改变了。我们来看看通知RadioGroup之后,它做了什么。

    private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
            // prevents from infinite recursion
            if (mProtectFromCheckedChange) {
                return;
            }

            mProtectFromCheckedChange = true;
            if (mCheckedId != -1) {
                setCheckedStateForView(mCheckedId, false);
            }
            mProtectFromCheckedChange = false;

            int id = buttonView.getId();
            setCheckedId(id);
        }
    }

可以看到,里面居然没有使用传递进来的isChecked值!也就是说RadioGroup根本没有检查回调的RadioButton是不是被checked的。紧接着就调用setCheckedId函数,把触发的控件当作了是被checked的对待:

    private void setCheckedId(@IdRes int id) {
        mCheckedId = id;
        if (mOnCheckedChangeListener != null) {
            mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
        }
    }

在里面,再次触发了RadioGroup.OnCheckedChangeListener,并且把不是被checked的id当作参数传递了进去。于是乎,这就改变了ViewHolder中的值,接着触发了RadioGroup.check函数,接下来就是一系列的无穷调用...为什么在不使用响应式架构的时候不会出现bug呢,因为以前Listener中的值不会触发RadioGroup.check函数,而在check函数会触发两次Listener,在最后一次会将正确的id值传入Listener中。
知道了坑在哪之后,解决的方法就是不要直接使用Listener传递的checkedId,还要检查该RadioButton.checked属性是不是true,只有为true时才将值设置给ViewHolder,这样地狱调用也就不会出现了。

上一篇下一篇

猜你喜欢

热点阅读