首页 > 代码库 > View的onSaveInstanceState和onRestoreInstanceState过程分析
View的onSaveInstanceState和onRestoreInstanceState过程分析
为什么要介绍这2个方法呢?这是因为在我们的开发中最近遇到了一个很诡异的bug。大体是这样的:在我们的ViewPager中
有2页的root view都是ScrollView,我们在xml里面都用了android:id="@+id/scroll_view"这样的代码,即2个布局里面的
ScrollView用了同一个id。我们重载了ScrollView的onSaveInstanceState()用来save当前的scrollX和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; // 并且这里只save了mScrollY,可能你还需要更多的,比如mScrollX, return ss; // 所以有这些原因在你一般都想要继承ScrollView然后实现自己的。 } @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中的实现:
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上关于这个问题的回答:
http://stackoverflow.com/questions/3542333/how-to-prevent-custom-views-from-losing-state-across-screen-orientation-changes
现在为止,我们可以重新审视下Android中关于View id的说法了。官方文档的说法是你最好尽量保证一个View有unique的id,或者
至少在一个局部的layout文件中,因为很显然如果你同一个layout文件中有2个id都是"android:id="@+id/button"的Button,那你通过
findViewById的时候只能找到前面的button,后面的那个就没机会被找到了,所以Android的说法是合理的。只是在本文一开始那里的情况下,
它没有提及,所以还应该加上特别重要的一条:当你的View确定要save/restore状态的时候,一定要保证他们有unique的id,因为Android
内部用id作为保存、恢复状态时使用的Key(SparseArray的key),否则就会发生一个覆盖另一个的悲剧而你却得不到任何提示或警告。
这篇文章算是实际开发中的经验之谈,希望对大家的日常开发有所帮助,也希望能少一个走弯路、深夜debug的poor dev,enjoy。。。