View树状态的保存、恢复过程分析

作者: tmp_zhao | 来源:发表于2016-05-02 16:02 被阅读533次

    缘起

    本文要重点分析的是2个方法即onSaveInstanceStateonRestoreInstanceState,为什么要介绍这2个方法呢?这是因为笔者在开发过程中遇到了一个很诡异的现象,大体是这样的:在我们的ViewPager中有2页的root view都是ScrollView,我们在xml里面都用了android:id="@+id/scroll_view"这样的代码,即2个布局里面的ScrollView用了同一个id。我们重载了ScrollView的onSaveInstanceState()用来save当前的scrollY,在使用过程中发现restore回来的时候其中一个的scrollY总是不对并且好像等于另一个的scrollY。这让我们很是疑惑,最终发现是因为2个ScrollView用了同一个id,所以导致系统在save state的时候一个覆盖了另一个的结果。接下来的内容,我们就重点来看看整个save的过程。当然了,可能有人会问我们为啥要自己save ScrollView的滚动位置呢,难道Android系统自己没做吗?答案是:是的,至少可以说在各个版本的Android之间没做好,看眼源码:

        @Override
        protected Parcelable onSaveInstanceState() {
            if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                // Some old apps reused IDs in ways they shouldn't have.
                // Don't break them, but they don't get scroll state restoration.
                return super.onSaveInstanceState(); // 看到了没,这里有个版本检测,还有一段原因,所以各个版本的Android就有了不一致的行为
            }                                       // 所以在4.3(包括)以前ScrollView的scroll state是不会保存的。
            Parcelable superState = super.onSaveInstanceState();
            SavedState ss = new SavedState(superState);
            ss.scrollPosition = mScrollY; 
            return ss; 
        }
    
        @Override
        protected void onRestoreInstanceState(Parcelable state) {
            if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                // Some old apps reused IDs in ways they shouldn't have.
                // Don't break them, but they don't get scroll state restoration.
                super.onRestoreInstanceState(state);
                return;
            }
            SavedState ss = (SavedState) state;
            super.onRestoreInstanceState(ss.getSuperState()); // 用super的state调用super的实现
            mSavedState = ss;
            requestLayout(); // 状态恢复了之后记得重新layout下,以便展现出来
        }
    
    

    过程分析

    好了言归正传,View的onSaveInstanceState和onRestoreInstanceState方法调用都是从Activity或Dialog的同名方法调用开始的,这里我们看下Activity的对应实现,代码如下:

    /**
         * Called to retrieve per-instance state from an activity before being killed
         * so that the state can be restored in {@link #onCreate} or
         * {@link #onRestoreInstanceState} (the {@link Bundle} populated by this method
         * will be passed to both).
         *
         * <p>This method is called before an activity may be killed so that when it
         * comes back some time in the future it can restore its state.  For example,
         * if activity B is launched in front of activity A, and at some point activity
         * A is killed to reclaim resources, activity A will have a chance to save the
         * current state of its user interface via this method so that when the user
         * returns to activity A, the state of the user interface can be restored
         * via {@link #onCreate} or {@link #onRestoreInstanceState}.
         *
         * <p>Do not confuse this method with activity lifecycle callbacks such as
         * {@link #onPause}, which is always called when an activity is being placed
         * in the background or on its way to destruction, or {@link #onStop} which
         * is called before destruction.  One example of when {@link #onPause} and
         * {@link #onStop} is called and not this method is when a user navigates back
         * from activity B to activity A: there is no need to call {@link #onSaveInstanceState}
         * on B because that particular instance will never be restored, so the
         * system avoids calling it.  An example when {@link #onPause} is called and
         * not {@link #onSaveInstanceState} is when activity B is launched in front of activity A:
         * the system may avoid calling {@link #onSaveInstanceState} on activity A if it isn't
         * killed during the lifetime of B since the state of the user interface of
         * A will stay intact.
         *
         * <p>The default implementation takes care of most of the UI per-instance
         * state for you by calling {@link android.view.View#onSaveInstanceState()} on each
         * view in the hierarchy that has an id, and by saving the id of the currently
         * focused view (all of which is restored by the default implementation of
         * {@link #onRestoreInstanceState}).  If you override this method to save additional
         * information not captured by each individual view, you will likely want to
         * call through to the default implementation, otherwise be prepared to save
         * all of the state of each view yourself.
         *
         * <p>If called, this method will occur before {@link #onStop}.  There are
         * no guarantees about whether it will occur before or after {@link #onPause}.
         * 
         * @param outState Bundle in which to place your saved state.
         * 
         * @see #onCreate
         * @see #onRestoreInstanceState
         * @see #onPause
         */
        protected void onSaveInstanceState(Bundle outState) { // 此方法的doc非常长且详细,你需要认真阅读下
            outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState()); // 注意这里的mWindow.saveHierarchyState()调用
            Parcelable p = mFragments.saveAllState();                               // 从这里开始会调用到View层次结构中的对应方法
            if (p != null) {
                outState.putParcelable(FRAGMENTS_TAG, p);
            }
            getApplication().dispatchActivitySaveInstanceState(this, outState);
        }
    
        /**
         * This method is called after {@link #onStart} when the activity is
         * being re-initialized from a previously saved state, given here in
         * <var>savedInstanceState</var>.  Most implementations will simply use {@link #onCreate}
         * to restore their state, but it is sometimes convenient to do it here
         * after all of the initialization has been done or to allow subclasses to
         * decide whether to use your default implementation.  The default
         * implementation of this method performs a restore of any view state that
         * had previously been frozen by {@link #onSaveInstanceState}.
         * 
         * <p>This method is called between {@link #onStart} and
         * {@link #onPostCreate}.
         * 
         * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}.
         * 
         * @see #onCreate
         * @see #onPostCreate
         * @see #onResume
         * @see #onSaveInstanceState
         */
        protected void onRestoreInstanceState(Bundle savedInstanceState) {
            if (mWindow != null) {
                Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
                if (windowState != null) {
                    mWindow.restoreHierarchyState(windowState); // 同样的调用Window的restoreHierarchyState方法
                }
            }
        }
    

    紧接着,我们看下Window中的实现(真正的实现在PhoneWindow.java中),如下:

    public abstract Bundle saveHierarchyState();
        
        public abstract void restoreHierarchyState(Bundle savedInstanceState);
    
        // 我们看到Window中只是2个抽象方法,其具体实现还得看PhoneWindow类
    
        /** {@inheritDoc} */
        @Override
        public Bundle saveHierarchyState() {
            Bundle outState = new Bundle(); // new一个Bundle(其实现了Parcelable接口)
            if (mContentParent == null) { // 这个字段还有印象吗?如果不清楚了你可以参看前面的这篇文章
                return outState;          // http://www.cnblogs.com/xiaoweiz/p/3787844.html
            }
            // 注意这里的container传递的是一个SparseArray,我们前面介绍过:http://www.cnblogs.com/xiaoweiz/p/3667689.html
            SparseArray<Parcelable> states = new SparseArray<Parcelable>();
            mContentParent.saveHierarchyState(states); // 进入view层次结构的save state
            outState.putSparseParcelableArray(VIEWS_TAG, states);
    
            // save the focused view id
            View focusedView = mContentParent.findFocus();
            if (focusedView != null) {
                if (focusedView.getId() != View.NO_ID) {
                    outState.putInt(FOCUSED_ID_TAG, focusedView.getId());
                } else {
                    if (false) {
                        Log.d(TAG, "couldn't save which view has focus because the focused view "
                                + focusedView + " has no id.");
                    }
                }
            }
    
            // save the panels
            SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>();
            savePanelState(panelStates);
            if (panelStates.size() > 0) {
                outState.putSparseParcelableArray(PANELS_TAG, panelStates);
            }
    
            if (mActionBar != null) {
                SparseArray<Parcelable> actionBarStates = new SparseArray<Parcelable>();
                mActionBar.saveHierarchyState(actionBarStates);
                outState.putSparseParcelableArray(ACTION_BAR_TAG, actionBarStates);
            }
    
            return outState;
        }
    
        /** {@inheritDoc} */
        @Override
        public void restoreHierarchyState(Bundle savedInstanceState) {
            if (mContentParent == null) {
                return;
            }
    
            SparseArray<Parcelable> savedStates
                    = savedInstanceState.getSparseParcelableArray(VIEWS_TAG);
            if (savedStates != null) {
                mContentParent.restoreHierarchyState(savedStates); // 同save的过程
            }
    
            // restore the focused view
            int focusedViewId = savedInstanceState.getInt(FOCUSED_ID_TAG, View.NO_ID);
            if (focusedViewId != View.NO_ID) {
                View needsFocus = mContentParent.findViewById(focusedViewId);
                if (needsFocus != null) {
                    needsFocus.requestFocus();
                } else {
                    Log.w(TAG,
                            "Previously focused view reported id " + focusedViewId
                                    + " during save, but can't be found during restore.");
                }
            }
    
            // restore the panels
            SparseArray<Parcelable> panelStates = savedInstanceState.getSparseParcelableArray(PANELS_TAG);
            if (panelStates != null) {
                restorePanelState(panelStates);
            }
    
            if (mActionBar != null) {
                SparseArray<Parcelable> actionBarStates =
                        savedInstanceState.getSparseParcelableArray(ACTION_BAR_TAG);
                if (actionBarStates != null) {
                    mActionBar.restoreHierarchyState(actionBarStates);
                } else {
                    Log.w(TAG, "Missing saved instance states for action bar views! " +
                            "State will not be restored.");
                }
            }
        }
    

    这里由于ViewGroup没有覆写save/restoreHierarchyState()方法,所以最终调用的是View中的方法,这里我们看下其源码:

    /**
         * Store this view hierarchy's frozen state into the given container.
         *
         * @param container The SparseArray in which to save the view's state.
         *
         * @see #restoreHierarchyState(android.util.SparseArray)
         * @see #dispatchSaveInstanceState(android.util.SparseArray)
         * @see #onSaveInstanceState()
         */
        public void saveHierarchyState(SparseArray<Parcelable> container) {
            dispatchSaveInstanceState(container); // 调相应的dispatchXXX方法
        }
    
        /**
         * Called by {@link #saveHierarchyState(android.util.SparseArray)} to store the state for
         * this view and its children. May be overridden to modify how freezing happens to a
         * view's children; for example, some views may want to not store state for their children.
         *
         * @param container The SparseArray in which to save the view's state.
         *
         * @see #dispatchRestoreInstanceState(android.util.SparseArray)
         * @see #saveHierarchyState(android.util.SparseArray)
         * @see #onSaveInstanceState()
         */
        protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {一个View必须有valid(非0)的mID,也就是说你
            if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) { // 要么在xml里通过android:id指定要么在代码里通过setId
                mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;                // 调用来设置,而且SAVE_DISABLED位没被打开,save才会发生
                Parcelable state = onSaveInstanceState();                 // 换句话说我们本文讲的所有东西都是和有valid id的View相关的,
                if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {     // 和NO_ID的View无关
                    throw new IllegalStateException( // 注意这里的检测,也就是说子类必须要调用父类的onSaveInstanceState()方法,否则会抛异常
                            "Derived class did not call super.onSaveInstanceState()");
                }
                if (state != null) {
                    // Log.i("View", "Freezing #" + Integer.toHexString(mID)
                    // + ": " + state);
                    container.put(mID, state); // 这行代码,将state放进SparseArray中,以view自身的id为key,所以我们一开始的例子在这里
                }                              // 就有问题了,key相同的情况下,后面的put会覆盖掉前面put的结果
            }
        }
    
        /**
         * Hook allowing a view to generate a representation of its internal state
         * that can later be used to create a new instance with that same state.
         * This state should only contain information that is not persistent or can
         * not be reconstructed later. For example, you will never store your
         * current position on screen because that will be computed again when a
         * new instance of the view is placed in its view hierarchy.
         * <p>
         * Some examples of things you may store here: the current cursor position
         * in a text view (but usually not the text itself since that is stored in a
         * content provider or other persistent storage), the currently selected
         * item in a list view.
         *
         * @return Returns a Parcelable object containing the view's current dynamic
         *         state, or null if there is nothing interesting to save. The
         *         default implementation returns null.
         * @see #onRestoreInstanceState(android.os.Parcelable)
         * @see #saveHierarchyState(android.util.SparseArray)
         * @see #dispatchSaveInstanceState(android.util.SparseArray)
         * @see #setSaveEnabled(boolean)
         */
        protected Parcelable onSaveInstanceState() { // callback方法或者也可以叫hook(钩子),允许客户代码覆写来实现自己的save逻辑
            mPrivateFlags |= PFLAG_SAVE_STATE_CALLED; // 设置位标志,在dispatchXXX里当onSaveInstanceState返回时会再次检测这个位
            return BaseSavedState.EMPTY_STATE; // 默认不save任何东西,也即do nothing
        }
    
        /**
         * Restore this view hierarchy's frozen state from the given container.
         *
         * @param container The SparseArray which holds previously frozen states.
         *
         * @see #saveHierarchyState(android.util.SparseArray)
         * @see #dispatchRestoreInstanceState(android.util.SparseArray)
         * @see #onRestoreInstanceState(android.os.Parcelable)
         */
        public void restoreHierarchyState(SparseArray<Parcelable> container) {
            dispatchRestoreInstanceState(container);
        }
    
        /**
         * Called by {@link #restoreHierarchyState(android.util.SparseArray)} to retrieve the
         * state for this view and its children. May be overridden to modify how restoring
         * happens to a view's children; for example, some views may want to not store state
         * for their children.
         *
         * @param container The SparseArray which holds previously saved state.
         *
         * @see #dispatchSaveInstanceState(android.util.SparseArray)
         * @see #restoreHierarchyState(android.util.SparseArray)
         * @see #onRestoreInstanceState(android.os.Parcelable)
         */
        protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
            if (mID != NO_ID) {
                Parcelable state = container.get(mID); // 通过id拿到saved state
                if (state != null) {
                    // Log.i("View", "Restoreing #" + Integer.toHexString(mID)
                    // + ": " + state);
                    mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED; // 关闭位标志,在onRestoreInstanceState里会再次打开它
                    onRestoreInstanceState(state); 
                    if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) { // 检查有没有记得调用super的实现
                        throw new IllegalStateException(
                                "Derived class did not call super.onRestoreInstanceState()");
                    }
                }
            }
        }
    
        /**
         * Hook allowing a view to re-apply a representation of its internal state that had previously
         * been generated by {@link #onSaveInstanceState}. This function will never be called with a
         * null state.
         *
         * @param state The frozen state that had previously been returned by
         *        {@link #onSaveInstanceState}.
         *
         * @see #onSaveInstanceState()
         * @see #restoreHierarchyState(android.util.SparseArray)
         * @see #dispatchRestoreInstanceState(android.util.SparseArray)
         */
        protected void onRestoreInstanceState(Parcelable state) { // callback回调,在这里restore(save的反向过程)
            mPrivateFlags |= PFLAG_SAVE_STATE_CALLED; // 打开位标志
            if (state != BaseSavedState.EMPTY_STATE && state != null) { // 注意这个异常检测。。。
                throw new IllegalArgumentException("Wrong state class, expecting View State but "
                        + "received " + state.getClass().toString() + " instead. This usually happens "
                        + "when two views of different type have the same id in the same hierarchy. "
                        + "This view's id is " + ViewDebug.resolveId(mContext, getId()) + ". Make sure "
                        + "other views do not use the same id.");
            }
        }
    

    最后,为了完整起见,我们看一个典型&简单的View子类对这2个方法的实现,android.widget.CompoundButton,源码如下:

    @Override
        public Parcelable onSaveInstanceState() {
            // Force our ancestor class to save its state
            setFreezesText(true);
            Parcelable superState = super.onSaveInstanceState(); // 记得调用super的实现,否则会抛异常的
    
            SavedState ss = new SavedState(superState);
    
            // 这里将是否勾选保存了起来,以便后续恢复
            ss.checked = isChecked();
            return ss; // 返回我们自己的状态
        }
    
        @Override
        public void onRestoreInstanceState(Parcelable state) {
            SavedState ss = (SavedState) state;
      
            super.onRestoreInstanceState(ss.getSuperState()); // 同样记得调用super的实现
            setChecked(ss.checked); // restore回来。。。
            requestLayout(); // 重新layout下
        }
    

    这里再附上一个StackOverflow上关于此主题的问答帖:自定义view状态保存

    总结

    到现在为止,我们可以重新审视下Android中关于View id的说法了。官方的说法是在整个view树中id不一定非要唯一,但你至少要保证在你搜索的这部分view树中是唯一的(局部唯一)。因为很显然,如果同一个layout文件中有2个id都是android:id="@+id/button"的Button,那你通过findViewById的时候只能找到前面的button,后面的那个就没机会被找到了,所以Android的说法是合理的。只是在本文一开始那里的情况下,它没有提及,所以还应该加上特别重要的一条:当你的View需要save/restore状态的时候(默认是都会需要的),一定要保证他们有全局unique的id!因为Android内部用view id作为保存、恢复状态时使用的Key(SparseArray的key),否则就会发生一个覆盖另一个的悲剧而你却得不到任何提示或警告。不过有时候,我们经常需要将同一个布局文件(比如某个item view)inflate出来后加到一个ViewGroup中,这时候想要有全局唯一的id就有点难了,在这种情况下的状态保存一定要注意下,因为肯定会发生一个覆盖另一个的事情,有时如果你明确知道不需要保存、恢复状态,可以在xml中使用android:saveEnabled="false"来显式关闭这个功能。关于最后这里提到的没法避免的相同view id的事情,可以参考笔者的另一篇文章:fragment状态保存&恢复

    相关文章

      网友评论

      • 2e0e3cb8d1f8:你好,请问如何知道 Window类中的public abstract Bundle saveHierarchyState()和public abstract void restoreHierarchyState(Bundle savedInstanceState)方法真正实现的位置是在PhoneWindow类中呢,我该怎么追踪呢
        2e0e3cb8d1f8:@tmp_zhao 好的 谢谢
        tmp_zhao:一般就是需要查看下Window有没有什么子类,像在这里那唯一的子类就是PhoneWindow了;另外你可以将Android源码导入到某个IDE工程中,这样IDE会直接显示出来这个方法在哪里被override了等等信息。

      本文标题:View树状态的保存、恢复过程分析

      本文链接:https://www.haomeiwen.com/subject/ttnkrttx.html