Android中SaveState原理分析

在Activity被回收之前,系統(tǒng)會(huì)調(diào)用Activity#onSaveInstanceState(Bundle outState)來保存View的狀態(tài)到傳入的outState對(duì)象中。
在Activity被重新創(chuàng)建時(shí),Activity#onCreate(Bundle savedInstanceState)Activity#onRestoreInstanceState(Bundle savedInstanceState)會(huì)傳入保存的狀態(tài)信息并恢復(fù)View的狀態(tài)。
用戶也可以重載Activity#onSaveInstanceState()方法來保存額外的Activity狀態(tài),并在Activity.onCreate()或者Activity#onRestoreInstanceState()獲取這些狀態(tài)。
這里主要看View的狀態(tài)是怎么保存和重新獲取的。

先說一下Bundle類,在Android代碼里的注釋是:
A mapping from String values to various Parcelable types.
可以簡單把Bundle看成一個(gè)Map<String, Object>,其中Object都實(shí)現(xiàn)了Parcelable接口。Bundle類有一系列的set和get方法用來操作Map中的值。

protected void onSaveInstanceState(Bundle outState) {
    outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    getApplication().dispatchActivitySaveInstanceState(this, outState);
}

這個(gè)方法做了三件事,1.保存Activity對(duì)應(yīng)的Window的State信息,并存放在outState中,2.保存所有Fragements的信息,如果不為零,也存放在outState中,3.調(diào)用外部注冊(cè)的一些回調(diào)方法。保存Fragments的信息是通過保存Fragment的根View的狀態(tài)實(shí)現(xiàn)的,這和保存Window的State信息類似。所以看mWindow.saveHierarchyState()方法,這里mWindow是一個(gè)PhoneWindow對(duì)象,找到PhoneWindow#saveHierarchyState()方法:

public Bundle saveHierarchyState() {
    //step 1:
    Bundle outState = new Bundle();
    if (mContentParent == null) {
        return outState;
    }
    // step 2: save View states
    SparseArray<Parcelable> states = new SparseArray<Parcelable>();
    mContentParent.saveHierarchyState(states);
    outState.putSparseParcelableArray(VIEWS_TAG, states);

    // step 3: 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.");
            }
        }
    }

    // step 4: save the panels state
    SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>();
    savePanelState(panelStates);
    if (panelStates.size() > 0) {
        outState.putSparseParcelableArray(PANELS_TAG, panelStates);
    }
    // step 5: save actionBar state
    if (mDecorContentParent != null) {
        SparseArray<Parcelable> actionBarStates = new SparseArray<Parcelable>();
        mDecorContentParent.saveToolbarHierarchyState(actionBarStates);
        outState.putSparseParcelableArray(ACTION_BAR_TAG, actionBarStates);
    }

    return outState;
}

這個(gè)方法依次做了這些事情:
1.創(chuàng)建Bundle對(duì)象用來返回,判斷Window是否有對(duì)應(yīng)的mContentParent這個(gè)View對(duì)象,如果沒有直接返回
2.保存View的信息
3.保存當(dāng)前View焦點(diǎn)的信息
4.保存抽屜信息
5.保存ActionBar信息。
我們關(guān)注第二步保存View信息: mContentParent.saveHierarchyState(states);.這里states是一個(gè)SparseArray<Parcelable>對(duì)象,所有View信息都會(huì)保存在states中。SparseArray可以理解為Map<Integer,Object>,是Android系統(tǒng)為了優(yōu)化內(nèi)存創(chuàng)建的類。
看一下View#saveHierarchyState(SparseArray<Parcelable> container):

public void saveHierarchyState(SparseArray<Parcelable> container) {
    dispatchSaveInstanceState(container);
}

直接調(diào)用了View#dispatchSaveInstanceState(SparseArray<Parcelable> container):

protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
        mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
        Parcelable state = onSaveInstanceState();
        if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
            throw new IllegalStateException(
                    "Derived class did not call super.onSaveInstanceState()");
        }
        if (state != null) {
            // Log.i("View", "Freezing #" + Integer.toHexString(mID)
            // + ": " + state);
            container.put(mID, state);
        }
    }
}

檢查一下View是否設(shè)置了ID,如果沒有id直接返回。這里我們可以看到,如果View沒有設(shè)置id,是不會(huì)保存它的狀態(tài)的。設(shè)置了ID之后,獲取狀態(tài),并將狀態(tài)以ID為key存儲(chǔ)在SparseArray中。這里可以看出,如果View樹中兩個(gè)View的id相同,那么后一個(gè)View的SavedState會(huì)覆蓋前一個(gè)SavedState。當(dāng)然這種情況下findViewById()也會(huì)出問題。
保存View狀態(tài)是在onSaveInstanceState()中實(shí)現(xiàn)的:

protected Parcelable onSaveInstanceState() {
    mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
    if (mStartActivityRequestWho != null) {
        BaseSavedState state = new BaseSavedState(AbsSavedState.EMPTY_STATE);
        state.mStartActivityRequestWhoSaved = mStartActivityRequestWho;
        return state;
    }
    return BaseSavedState.EMPTY_STATE;
}

這個(gè)方法檢查了一下mStartActivityRequestWho這個(gè)String對(duì)象是否為null,mStartActivityRequestWho這個(gè)對(duì)象只有在調(diào)用了View#startActivityForResult時(shí)才會(huì)設(shè)置,這時(shí)標(biāo)記一下state中的狀態(tài),為null時(shí),直接返回一個(gè)空狀態(tài)。
在View類中保存狀態(tài)里用了三個(gè)方法,其中saveHierachyState()方法直接傳遞給了dispatchSaveInstanceState()方法。dispatchSaveInstanceState()用來分發(fā)保存狀態(tài)的行為,這個(gè)方法的默認(rèn)行為是在有ID的情況下保存自身的state,沒有id的情況下什么都不做。所以ViewGroup可以選擇重寫這個(gè)方法,將保存狀態(tài)的行為分發(fā)到子類中。onSaveInstanceState()方法用來具體地保存當(dāng)前View的狀態(tài),自定義View可以選擇重寫這個(gè)方法。

看一下ViewGroup#dispatchSaveInstanceState()方法

protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    super.dispatchSaveInstanceState(container);
    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        View c = children[i];
        if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
            c.dispatchSaveInstanceState(container);
        }
    }
}

首先調(diào)用View#dispatchSaveInstanceState()保存自身的狀態(tài),然后遍歷子View,調(diào)用對(duì)應(yīng)的dispatchSaveInstanceState()方法,這里實(shí)現(xiàn)的View樹的遍歷。
看一下TextView#onSaveInstanceState()是怎么保存狀態(tài)的:

public Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();

    // Save state if we are forced to
    boolean save = mFreezesText;
    int start = 0;
    int end = 0;

    if (mText != null) {
        start = getSelectionStart();
        end = getSelectionEnd();
        if (start >= 0 || end >= 0) {
            // Or save state if there is a selection
            save = true;
        }
    }

    if (save) {
        SavedState ss = new SavedState(superState);
        // XXX Should also save the current scroll position!
        ss.selStart = start;
        ss.selEnd = end;

        if (mText instanceof Spanned) {
            Spannable sp = new SpannableStringBuilder(mText);

            if (mEditor != null) {
                removeMisspelledSpans(sp);
                sp.removeSpan(mEditor.mSuggestionRangeSpan);
            }

            ss.text = sp;
        } else {
            ss.text = mText.toString();
        }

        if (isFocused() && start >= 0 && end >= 0) {
            ss.frozenWithFocus = true;
        }

        ss.error = getError();

        if (mEditor != null) {
            ss.editorState = mEditor.saveInstanceState();
        }
        return ss;
    }

    return superState;
}

首先判斷TextView是否需要保存狀態(tài),如果mFreezesText設(shè)置為true,或者TextView處于被選中的狀態(tài),那么需要保存狀態(tài)。不許要保存保存狀態(tài)的話直接返回super.onSaveInstanceState().
TextView中有個(gè)內(nèi)部類SavedState用來表示狀態(tài),將對(duì)應(yīng)的狀態(tài)設(shè)置好之后直接返回即可。

至此,整個(gè)保存狀態(tài)的過程已經(jīng)走完,總結(jié)一下:
1.在Activity被回收時(shí),會(huì)觸發(fā)一個(gè)SaveState的事件。
2.跟其他的事件一樣,SaveState事件從Activity->Window->View傳遞到最大的View,然后遍歷View樹保存狀態(tài)
3.狀態(tài)保存在一個(gè)SparseArray中,以View的ID作為key。
4.自定義View可以重載onSaveInstanceState()來保存自己的狀態(tài),參考TextView的實(shí)現(xiàn)方法。

Restore的過程:Activity#onRestoreInstanceState()方法:

protected void onRestoreInstanceState(Bundle savedInstanceState) {
    if (mWindow != null) {
        Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
        if (windowState != null) {
            mWindow.restoreHierarchyState(windowState);
        }
    }
}

跟保存的過程一樣,傳遞到Window#restoreHierarchyState()方法中:

public void restoreHierarchyState(Bundle savedInstanceState) {
    // step 1.
    if (mContentParent == null) {
        return;
    }
    // step 2.
    SparseArray<Parcelable> savedStates
            = savedInstanceState.getSparseParcelableArray(VIEWS_TAG);
    if (savedStates != null) {
        mContentParent.restoreHierarchyState(savedStates);
    }

    // step 3. 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.");
        }
    }

    // step 4. restore the panels
    SparseArray<Parcelable> panelStates = savedInstanceState.getSparseParcelableArray(PANELS_TAG);
    if (panelStates != null) {
        restorePanelState(panelStates);
    }
    // step 5.
    if (mDecorContentParent != null) {
        SparseArray<Parcelable> actionBarStates =
                savedInstanceState.getSparseParcelableArray(ACTION_BAR_TAG);
        if (actionBarStates != null) {
            doPendingInvalidatePanelMenu();
            mDecorContentParent.restoreToolbarHierarchyState(actionBarStates);
        } else {
            Log.w(TAG, "Missing saved instance states for action bar views! " +
                    "State will not be restored.");
        }
    }
}

跟保存的過程完全一致,分五步。我們只看第三步:mContentParent.restoreHierarchyState(savedStates),這里調(diào)用View#restoreHierarchyState(savedStates)方法:

public void restoreHierarchyState(SparseArray<Parcelable> container) {
    dispatchRestoreInstanceState(container);
}

一樣直接傳遞到View#dispatchRestoreInstanceState()方法:

protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID) {
        Parcelable state = container.get(mID);
        if (state != null) {
            // Log.i("View", "Restoreing #" + Integer.toHexString(mID)
            // + ": " + state);
            mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
            onRestoreInstanceState(state);
            if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
                throw new IllegalStateException(
                        "Derived class did not call super.onRestoreInstanceState()");
            }
        }
    }
}

對(duì)于View的這個(gè)方法,從SparseArray中用mID獲取到state,然后調(diào)用View#onRestoreInstanceState()方法。看一下ViewGroup對(duì)應(yīng)的方法:

protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    super.dispatchRestoreInstanceState(container);
    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        View c = children[i];
        if ((c.mViewFlags & PARENT_SAVE_DISABLED_MASK) != PARENT_SAVE_DISABLED) {
            c.dispatchRestoreInstanceState(container);
        }
    }
}

先調(diào)用View版本的dispatchRestoreInstanceState()方法,然后遍歷childView,調(diào)用對(duì)應(yīng)的dispatchRestoreInstanceState()方法。
最后看一下View#onRestoreInstanceState()方法:

protected void onRestoreInstanceState(Parcelable state) {
    mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
    if (state != null && !(state instanceof AbsSavedState)) {
        throw new IllegalArgumentException("Wrong state class, expecting View State ");
    }
    if (state != null && state instanceof BaseSavedState) {
        mStartActivityRequestWho = ((BaseSavedState) state).mStartActivityRequestWhoSaved;
    }
}

這個(gè)方法很簡單,直接獲取mStartActivityRequestWho對(duì)象,對(duì)于自定義View,需要重寫這個(gè)方法,獲取自己的狀態(tài)。同樣看一下TextView#onRestoreInstanceState()方法:

public void onRestoreInstanceState(Parcelable state) {
    if (!(state instanceof SavedState)) {
        super.onRestoreInstanceState(state);
        return;
    }

    SavedState ss = (SavedState)state;
    super.onRestoreInstanceState(ss.getSuperState());

    // XXX restore buffer type too, as well as lots of other stuff
    if (ss.text != null) {
        setText(ss.text);
    }

    if (ss.selStart >= 0 && ss.selEnd >= 0) {
        if (mText instanceof Spannable) {
            int len = mText.length();

            if (ss.selStart > len || ss.selEnd > len) {
                String restored = "";

                if (ss.text != null) {
                    restored = "(restored) ";
                }

                Log.e(LOG_TAG, "Saved cursor position " + ss.selStart +
                      "/" + ss.selEnd + " out of range for " + restored +
                      "text " + mText);
            } else {
                Selection.setSelection((Spannable) mText, ss.selStart, ss.selEnd);

                if (ss.frozenWithFocus) {
                    createEditorIfNeeded();
                    mEditor.mFrozenWithFocus = true;
                }
            }
        }
    }

    if (ss.error != null) {
        final CharSequence error = ss.error;
        // Display the error later, after the first layout pass
        post(new Runnable() {
            public void run() {
                if (mEditor == null || !mEditor.mErrorWasChanged) {
                    setError(error);
                }
            }
        });
    }

    if (ss.editorState != null) {
        createEditorIfNeeded();
        mEditor.restoreInstanceState(ss.editorState);
    }
}

首先判斷state是否為TextView保存的狀態(tài),如果不是,直接調(diào)用super,然后獲取對(duì)應(yīng)的狀態(tài)設(shè)置給TextView。

可以看出Restore過程和Save過程完全相同。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,786評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,964評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,354評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,554評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,106評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,918評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,093評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,342評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,839評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,107評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容