同ID输入框,数据在ViewPager里被覆盖的问题

本文基于Support-V4包下的Fragment、FragmentManager类,SDK版本为27

背景描述

测试提了bug,页面输入框在录入数据后,切到其他页面再返回,输入框值会被最后一项覆盖。
每行控件都是通过LayoutInflater生成,添加进容器。

输入框都被覆盖成了8

复现步骤

只要滑动页数只要超过ViewPager的OffscreenPageLimit()默认值(1),滑回时就会复现

先说结论

控件保存数据时,把当前状态序列化,保存到变量(SparseArray mStateArray),Key是控件ID。
恢复数据时,根据ID,从mStateArray里面取序列化对象。
由于每个输入框的ID一样,导致mStateArray里该ID对应的值,就是最后一个输入框的序列化对象;
因此在恢复数据时,相同ID的控件,数据都会被最后一项覆盖。

所以禁用数据保存、恢复功能即可,下述操作任选一个
把View的id设置成View.NO_ID或者 .setSaveEnabled(false);

问题排查

排除业务代码问题后,我们给输入框添加了TextChanged监听事件,断点。
执行复现步骤,发现此时在生命周期的恢复步骤,editText被重新赋值。

原因分析

这个bug由View的状态保存、恢复引起,所以要彻底解决它,我们需要弄明白

  1. View是如何保存、恢复状态的
  2. fragment在ViewPager里何时调用View的保存、恢复事件
View是如何保存、恢复状态的

系统提供的控件,都实现了自己的保存、恢复操作,定义在各自的onSaveInstanceStateonRestoreInstanceState

    /**
     * 保存状态,返回一个序列化对象
     *
     * @return
     */
    @Nullable
    @Override
    protected Parcelable onSaveInstanceState() {
        return super.onSaveInstanceState();
    }

    /**
     * 还原状态,根据序列化入参
     *
     * @param state
     */
    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
    }

这俩函数解决了View如何把当前的状态序列化成对象;以及根据序列化对象来恢复当前状态

那么这个序列化对象是如何被赋值、使用的呢?我们主要关注以下两个函数

  1. dispatchSaveInstanceState(...)
  2. dispatchRestoreInstanceState(...)
dispatchSaveInstanceState

通过调用onSaveInstanceState得到View序列化的状态,然后存到了入参中container.put(mID, state);,key为控件ID。

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);
            }
        }
    }

从上文的if语句我们也可得出,如果一个View需要保存状态,需要以下两个条件:

  1. 该View有着唯一的一个id。可通过setId或xml设置id,如果id不唯一,由于SparseArray是以id为key保存状态的,那么相同的id的View的数据会覆盖
  2. 该View的标志位不是SAVE_DISABLED_MASK。可通过setSaveEnabled(boolean)方法来改变,默认是true,即可保存状态。

dispatchSaveInstanceState方法在ViewGroup里被重写,分发保存事件到每个子元素。

dispatchRestoreInstanceState

通过ID得到序列化对象,然后调用onRestoreInstanceState恢复当前状态

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

从上文的if语句我们也可得出,如果一个View需要恢复状态,需要设置id。
onRestoreInstanceState方法在ViewGroup里被重写,分发恢复事件到每个子元素。

对外暴露函数

上述两个函数都是protect访问权限的,View提供了代理方法,暴露给外界调用

  1. dispatchSaveInstanceState
    public void saveHierarchyState(SparseArray container) {
        dispatchSaveInstanceState(container);
    }
  1. dispatchRestoreInstanceState
    public void restoreHierarchyState(SparseArray container) {
        dispatchRestoreInstanceState(container);
    }
序列化对象管理

本文场景(ViewPager使用Fragment)而言,FragmentManager管理了Fragment状态的保存、恢复,并提供了SparseArray mStateArray;变量保存序列化对象。
保存
下面函数,把mStateArray变量作为参数,通知View保存状态,
然后把mStateArray`保存到Fragment的mSavedViewState变量上

//保存fragment里View的状态
void saveFragmentViewState(Fragment f) {
        if (f.mInnerView == null) {
            return;
        }
        if (mStateArray == null) {
            mStateArray = new SparseArray();
        } else {
            mStateArray.clear();
        }
        f.mInnerView.saveHierarchyState(mStateArray);
        if (mStateArray.size() > 0) {
            f.mSavedViewState = mStateArray;
            mStateArray = null;
        }
    }

恢复
由于上一步已经把mStateArray赋值给Fragment的mSavedViewState。因此下面函数看不到mStateArray的使用

    void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive) {
             //...一堆代码
              switch (f.mState) {
            //....一堆代码
               case Fragment.CREATED:
                if (f.mView != null) {
                          f.restoreViewState(f.mSavedFragmentState);
                 }
               break;
       }
    }

在Fragment的restoreViewState里就调用了restoreHierarchyState恢复对象

    final void restoreViewState(Bundle savedInstanceState) {
        if (mSavedViewState != null) {
            mInnerView.restoreHierarchyState(mSavedViewState);
            mSavedViewState = null;
        }
        mCalled = false;
        onViewStateRestored(savedInstanceState);
        if (!mCalled) {
            throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onViewStateRestored()");
        }
    }
小结

通过上面的分析,我们知道了
在保存的时候,View通过onSaveInstanceState()把自己的状态序列化,保存到FragmentManagermStateArray变量上;
在恢复的时候,View通过onRestoreInstanceState()FragmentmSavedViewState变量里取值恢复。
需要注意的是,View必须设置ID、标志位不是SAVE_DISABLED_MASK

系统何时调用View保存、恢复事件

我们可以通过给FragmentManagersaveFragmentViewState()moveToState()断点,来获取函数被执行的上下文环境。

保存
保存断点

完整上下文

java.lang.Thread.State: RUNNABLE
      at android.support.v4.app.FragmentManagerImpl.saveFragmentViewState(FragmentManager.java:2860)
      at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1509)
      at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1759)
      at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:792)
      at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2596)
      at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2383)
      at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2338)
      at android.support.v4.app.FragmentManagerImpl.execSingleAction(FragmentManager.java:2215)
      at android.support.v4.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:649)
      at android.support.v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:145)
      at android.support.v4.view.ViewPager.populate(ViewPager.java:1238)
      at android.support.v4.view.ViewPager.populate(ViewPager.java:1086)
      at android.support.v4.view.ViewPager$3.run(ViewPager.java:267)
      at android.view.Choreographer$CallbackRecord.run(Choreographer.java:927)
      at android.view.Choreographer.doCallbacks(Choreographer.java:702)
      at android.view.Choreographer.doFrame(Choreographer.java:635)
      at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:913)
      at android.os.Handler.handleCallback(Handler.java:751)
      at android.os.Handler.dispatchMessage(Handler.java:95)
      at android.os.Looper.loop(Looper.java:154)
      at android.app.ActivityThread.main(ActivityThread.java:6688)
      at java.lang.reflect.Method.invoke(Method.java:-1)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1468)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1358)
恢复
恢复断点

完整上下文

 at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1454)
      at android.support.v4.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1759)
      at android.support.v4.app.BackStackRecord.executeOps(BackStackRecord.java:792)
      at android.support.v4.app.FragmentManagerImpl.executeOps(FragmentManager.java:2596)
      at android.support.v4.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2383)
      at android.support.v4.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2338)
      at android.support.v4.app.FragmentManagerImpl.execSingleAction(FragmentManager.java:2215)
      at android.support.v4.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:649)
      at android.support.v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:145)
      at android.support.v4.view.ViewPager.populate(ViewPager.java:1238)
      at android.support.v4.view.ViewPager.populate(ViewPager.java:1086)
      at android.support.v4.view.ViewPager$3.run(ViewPager.java:267)
      at android.view.Choreographer$CallbackRecord.run(Choreographer.java:927)
      at android.view.Choreographer.doCallbacks(Choreographer.java:702)
      at android.view.Choreographer.doFrame(Choreographer.java:635)
      at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:913)
      at android.os.Handler.handleCallback(Handler.java:751)
      at android.os.Handler.dispatchMessage(Handler.java:95)
      at android.os.Looper.loop(Looper.java:154)
      at android.app.ActivityThread.main(ActivityThread.java:6688)
      at java.lang.reflect.Method.invoke(Method.java:-1)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1468)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1358)

延伸阅读

为何TextView也是相同ID,但没有这个bug

我们知道EditText是继承于TextView的,所以翻下源码发现默认是不去保存数据。

 @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();

        // Save state if we are forced to
        final boolean freezesText = getFreezesText();
        boolean hasSelection = false;
        int start = -1;
        int end = -1;

        if (mText != null) {
            start = getSelectionStart();
            end = getSelectionEnd();
            if (start >= 0 || end >= 0) {
                // Or save state if there is a selection
                hasSelection = true;
            }
        }

        if (freezesText || hasSelection) {
            //保存数据操作...
        }
}

从上面的if条件,我们可以得出 要保存数据,必须满足下述变量为true

  1. freezesText
    TextView默认false ,在EditText里被重写为true
  2. hasSelection
    TextView 默认false,因为 getSelectionStart()getSelectionStart()都是-1

你可能感兴趣的:(同ID输入框,数据在ViewPager里被覆盖的问题)