為什么要介紹這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上關於此主題的問答帖:
現在為止,我們可以重新審視下Android中關於View id的說法了。官方的說法是在整個view樹中id不一定非要唯一,但你至少要
保證在你搜索的這部分view樹中是唯一的(局部唯一)。因為很顯然,如果同一個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。。。