爲何要介紹這2個方法呢?這是由於在咱們的開發中最近遇到了一個很詭異的bug。大致是這樣的:在咱們的ViewPager中html
有2頁的root view都是ScrollView,咱們在xml裏面都用了android:id="@+id/scroll_view"這樣的代碼,即2個佈局裏面的android
ScrollView用了同一個id。咱們重載了ScrollView的onSaveInstanceState()用來save當前的scrollX和scrollY,在使用過程當中app
發現restore回來的時候其中一個的scrollY老是不對而且好像等於另外一個的scrollY。這讓咱們非常疑惑,最終咱們的一個工程師發現ide
了問題所在,就是由於2個ScrollView用了同一個id,因此致使系統在save state的時候一個覆蓋了另外一個的結果。接下來的內容,我佈局
們就重點來看看這個save的過程。固然了,可能有人會問咱們爲啥要本身save ScrollView的滾動位置呢,難道Android系統本身沒作嗎?ui
答案是,是的,至少能夠說在各個版本的Android之間沒作好,看眼源碼:this
@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的同名方法調用開始的,spa
這裏咱們看下Activity的對應實現,代碼以下:debug
/** * 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中的實現:rest
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。。。