项目疑难杂症记录(一):fragment单例导致的界面异常

前言:之前项目中也会遇到一些头疼的问题或者难解的bug,有些可能花费不少时间精力解决了,但是没有记录,打算从本篇博客开始,记录下项目中遇到的我认为的疑难杂症,算是对自己学习的总结,如果凑巧你也看到了,并且对你有一定的帮助,那将会是一件很有意义的事情。

最近同事做Tv项目,遇到了界面上一个奇怪的并且百思不得其解的现象,请我帮忙分析解决,先看下面的一个出现问题的效果图

进去注册页面,每个控件的值不完全一样,然后退出,再次进入注册页面,每个控件的值竟然完全一样。
一、问题初步排查定位思路:
1、代码逻辑错误导致数据保存一样?
2、fragment中的页面没有销毁?

首先很快速的排除了数据源是一样的问题,因为每个控件来源的数据值都是不一样的。
再看看fragment的生命周期,是否是页面没有销毁,下面是打印日志。

 D/SIPFragment:  onCreate
 D/SIPFragment:  onCreateView 
 D/SIPFragment:  onDestroyView
 D/SIPFragment:  onDestroy

从日志中可以明显看到页面是销毁了,那下次进来页面应该是重新创建的啊,怎么会值是一样的呢?

二、问题再次查找
因为同事的fragment创建的时候使用是单例?我隐隐觉得和这个有点关系,遂把fragment每次创建的时候都new出新的对象,果然,再次进入页面,就是正常的现象了。
但是新的问题又出现了,同事反馈说,之前他也用过fragment单例,也没有出现过这个问题啊,这个就有点奇怪了,我的注意点又来到了自定义的控件上,因为里面的每个item是frameLatout控件封装,里面包含了EditText。
我顺着这个思路,去掉封装的控件,改用在xml中定义每个需要保存数据的EditText,重新运行程序,结果果然如预期的一样,是正常的,不是出问题的现象。
好了,到这里先简单理一下思路:
Q1、fragment单例会引起这个现象,但不能说单例一定会造成这个问题,单例是如何造成这个bug的呢?其中的机制是什么?
Q2、自己封装的控件,也会引起这个问题,这个又是怎么回事呢,虽然是自定义的控件,但是每个控件的对象和EditText的对象都是不一样的。

但与此同时,又提出了两个问题:
Q3、为什么是修改页面上最后一个控件的值,被保存下来了,换句话说,为什么每个控件相同的值是上一次页面最后一个控件的值?
Q4、为什么每个控件左边的TextView可以做到不相同,而EditText确是一样的呢?

我决定还是一如既往的从源码入手,毕竟我知道一切答案都可以从源码中找到。
既然单例会导致bug,那单例肯定出了问题,单例,顾名思义,是反反复复进入,对象还是同一个,我以经验判断,必定是对象里面的一些状态,标志位等数据没有恢复初始化状态,遂去源码中这个具体的地方查找。

    /**
     * Called by the fragment manager once this fragment has been removed,
     * so that we don't have any left-over state if the application decides
     * to re-use the instance.  This only clears state that the framework
     * internally manages, not things the application sets.
     */
    void initState() {
        mIndex = -1;
        mWho = null;
        mAdded = false;
        mRemoving = false;
        mFromLayout = false;
        mInLayout = false;
        mRestored = false;
        mBackStackNesting = 0;
        mFragmentManager = null;
        mChildFragmentManager = null;
        mHost = null;
        mFragmentId = 0;
        mContainerId = 0;
        mTag = null;
        mHidden = false;
        mDetached = false;
        mRetaining = false;
        mLoaderManager = null;
        mLoadersStarted = false;
        mCheckedForLoaderManager = false;
    }

找到了这个方法,是恢复初始状态的,感觉考虑的挺周到的嘛~,重置了这么多的变量,但我还是不放心,于是和变量声明的地方一一去比对,果然,被我发现了漏网之鱼,哈哈,真是看到了希望,这个变量就是mSavedViewState

    SparseArray mSavedViewState;

这个变量看起来应该是缓存view数据状态的,我们看下其怎么使用的

  case Fragment.ACTIVITY_CREATED:
       if (newState < Fragment.ACTIVITY_CREATED) {
           if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f);
            if (f.mView != null) {
            // Need to save the current view state if not
             // done already.
   if (mHost.onShouldSaveFragmentState(f) &&   f.mSavedViewState == null){
                          //保存fragment的的view状态
                                saveFragmentViewState(f);
                            }
            }
        f.performDestroyView();

看到fragment在销毁前保存了状态,看下saveFragmentViewState方法。

    void saveFragmentViewState(Fragment f) {
        if (f.mView == null) {
            return;
        }
        //如果数据为空,new一下
        if (mStateArray == null) {
            mStateArray = new SparseArray();
        } else {
          //有数据,清楚掉
            mStateArray.clear();
        }
        //调用view的保存状态方法,将数据集合作为参数
        f.mView.saveHierarchyState(mStateArray);
        if (mStateArray.size() > 0) {
           //如果有数据,将数据赋值给fragment中的数据缓存集合
            f.mSavedViewState = mStateArray;
            mStateArray = null;
        }
    }

我们继续往下看,有种逐渐明朗的感觉有没有哈。
View的saveHierarchyState方法

    public void saveHierarchyState(SparseArray container) {
        dispatchSaveInstanceState(container);
    }
      protected void dispatchSaveInstanceState(SparseArray container) {
        if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
            //获取数据,调用onSaveInstanceState方法,稍后再看
            Parcelable state = onSaveInstanceState();
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onSaveInstanceState()");
            }
            if (state != null) {
                // Log.i("View", "Freezing #" + Integer.toHexString(mID)
                // + ": " + state);
              // 如果获取到数据,保存到SparseArray,key是View的ID。
                container.put(mID, state);
            }
        }
    }

好了,到这边能解释我们Q4了,这里面用控件的ID去作为key,我们知道无论是SprseArray还是HashMap,相同的key只会保存一个值,所以这就能解释为什么页面记录的是最后一个控件了。

我们继续看onSaveInstanceState方法,来看看能不能解释Q2,Q3的问题,EditText是TextView的子类,这个方法被TextView重载了,去看下其实现
TextView ->onSaveInstanceState

 @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        final boolean freezesText = getFreezesText();
        //初始化是否有光标
        boolean hasSelection = false;
        int start = -1;
        int end = -1;
        if (mText != null) {
           //内容不空,记录start,end位置
            start = getSelectionStart();
            end = getSelectionEnd();
            if (start >= 0 || end >= 0) {
                // Or save state if there is a selection
                hasSelection = true;
            }
        }
        // EditText有内容的话,hasSelection为true,会走这个分支
        if (freezesText || hasSelection) {
           //创建SavedState 这个子类
            SavedState ss = new SavedState(superState);
            if (freezesText) {
                if (mText instanceof Spanned) {
                    final Spannable sp = new SpannableStringBuilder(mText);
                    if (mEditor != null) {
                        removeMisspelledSpans(sp);
                        sp.removeSpan(mEditor.mSuggestionRangeSpan);
                    }
                    ss.text = sp;
                } else {
                  // 将文本内容放进缓存对象
                    ss.text = mText.toString();
                }
            }
            ss.error = getError();
            if (mEditor != null) {
                ss.editorState = mEditor.saveInstanceState();
            }
            //EditText 返回
            return ss;
        }
        //TextView返回
        return superState;
    }

这个方法的作用是保存EditText中的内容到缓存对象中,如果是TextView不会保存,好了,到这里就能解释TextView与EditText之间在缓存数据方面的差别了。
我们再次回到上面的dispatchSaveInstanceState方法

protected void dispatchSaveInstanceState(SparseArray container) {
        if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
            Parcelable state = onSaveInstanceState();
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onSaveInstanceState()");
            }
            if (state != null) {
                // Log.i("View", "Freezing #" + Integer.toHexString(mID)
                // + ": " + state);
                container.put(mID, state);
            }
        }
    }

数据怎么缓存的,看完了,那在哪里恢复的呢?怎么恢复的?我们继续看,

          //省略部分代码
               f.performActivityCreated(f.mSavedFragmentState);
                      // 此时已经调用完onCreateView,mView不为空
                        if (f.mView != null) {
                            f.restoreViewState(f.mSavedFragmentState);
                }

在performActivityCreated生命周期后开始恢复数据,这里就有意思了,这个地方调用是在onCreateView之后,设置数据是在onCreateView开始调用的initData里面,所以即使我给了一个正确的值,还是会被这个地方给刷掉,有意思吧

继续看restoreViewState方法。

   final void restoreViewState(Bundle savedInstanceState) {
      //由于是单例,之前的对象没有销毁,所以mSavedViewState 不为空。
        if (mSavedViewState != null) {
           //调用view的恢复数据的方法,这里就不继续往下看了
            mInnerView.restoreHierarchyState(mSavedViewState);
            //恢复完数据,重置该变量
            mSavedViewState = null;
        }
        mCalled = false;
        onViewStateRestored(savedInstanceState);
        if (!mCalled) {
            throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onViewStateRestored()");
        }
    }

好了,到这里,这个问题,总算可以完结了,不过fragment为什么要保存view的状态呢,那是因为防止我们的进程被系统杀死,所以保存了fragment中所有需要保存的状态,只是我们这里错误的使用单例,导致了这个机制的触发,真是触目惊心啊。

如果同学们看的不是很清楚,我最后补充下时序图。
fragment销毁时候:
项目疑难杂症记录(一):fragment单例导致的界面异常_第1张图片

下次再进来,创建时候:
项目疑难杂症记录(一):fragment单例导致的界面异常_第2张图片

你可能感兴趣的:(项目疑难杂症,慎用fragment单例,view的数据缓存,android界面数据一样)