View的onSaveInstanceState和onRestoreInstanceState過程分析

  爲何要介紹這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上關於此主題的問答帖:

http://stackoverflow.com/questions/3542333/how-to-prevent-custom-views-from-losing-state-across-screen-orientation-changes

  如今爲止,咱們能夠從新審視下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。。。

相關文章
相關標籤/搜索