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樹中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。。。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM