fragment的狀態(tài)保存&恢復(fù)過程分析

緣起

之前已經(jīng)做了關(guān)于fragment源碼的分析烫幕,但貌似把fragment關(guān)于保存砸喻、恢復(fù)的內(nèi)容給忽略了欢瞪,再加上上周5在開發(fā)一個(gè)功能時(shí)遇到了一個(gè)奇怪的現(xiàn)象敌完,當(dāng)時(shí)真是怎么也想不明白,而且單步調(diào)試發(fā)現(xiàn)狀態(tài)也都是對(duì)的钝荡,但是界面渲染完就又不對(duì)了街立,呃。埠通。赎离。

這里先解釋下我那個(gè)功能的大概樣子:fragment包括一個(gè)可以滾動(dòng)的LinearLayout,可以動(dòng)態(tài)添加多個(gè)item view端辱,item view是包括了一個(gè)checkbox梁剔,多個(gè)textview這樣的東西,同時(shí)只有一個(gè)item view可以被選中舞蔽。奇怪的現(xiàn)象是:fragment第2次show出來的時(shí)候荣病,默認(rèn)應(yīng)該選中的項(xiàng)沒有被選中,第1次就完全沒這個(gè)問題渗柿,這里我重用了同一個(gè)fragment的實(shí)例个盆。

此現(xiàn)象的原因分析

沒辦法這個(gè)問題的原因不查出來,真是吃飯都走神。老話講颊亮,大膽假設(shè)柴梆,小心求證。既然checkbox的狀態(tài)不對(duì)终惑,那么我立馬感覺只能是checkbox的setChecked方法被我還不知道的地方調(diào)用了绍在,于是我將系統(tǒng)的checkbox換成了自己繼承的MyCheckbox,然后override了setChecked方法狠鸳,并在那里加了些log揣苏,再次run了一遍悯嗓,果然不出所料件舵,輸出了一堆日志。那就證明了我的猜想脯厨,確實(shí)是不知道什么地方再次調(diào)用了setChecked方法铅祸,覆蓋了我自己在onCreateView里面關(guān)于checkbox的設(shè)置。接下來就很簡單了合武,我在重載的方法那里掛了個(gè)斷點(diǎn)临梗,很快就知道了調(diào)用棧,如下:


在本文的最后稼跳,我們?cè)賮碓敿?xì)分析為什么會(huì)發(fā)生這個(gè)行為盟庞。

fragment保存、恢復(fù)過程

View樹的狀態(tài)保存汤善、恢復(fù)一文中我們已經(jīng)分析了絕大部分的內(nèi)容什猖,那里忽略了關(guān)于fragment的代碼,我們?cè)谶@里著重講解下红淡,來回顧下Activity.onSaveInstanceState方法:

protected void onSaveInstanceState(Bundle outState) {
        outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
        // view樹的狀態(tài)保存完之后不狮,處理fragment相關(guān)的
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
        getApplication().dispatchActivitySaveInstanceState(this, outState);
    }

protected void onCreate(@Nullable Bundle savedInstanceState) {
        if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState);
        if (mLastNonConfigurationInstances != null) {
            mFragments.restoreLoaderNonConfig(mLastNonConfigurationInstances.loaders);
        }
        if (mActivityInfo.parentActivityName != null) {
            if (mActionBar == null) {
                mEnableDefaultActionBarUp = true;
            } else {
                mActionBar.setDefaultDisplayHomeAsUpEnabled(true);
            }
        }
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            // 恢復(fù)fragment相關(guān)的狀態(tài)
            mFragments.restoreAllState(p, mLastNonConfigurationInstances != null
                    ? mLastNonConfigurationInstances.fragments : null);
        }
        mFragments.dispatchCreate();
        getApplication().dispatchActivityCreated(this, savedInstanceState);
        if (mVoiceInteractor != null) {
            mVoiceInteractor.attachActivity(this);
        }
        mCalled = true;
    }

從上面的源碼可以看出,fragment的狀態(tài)保存在旱、恢復(fù)是根據(jù)外面的act來進(jìn)行的摇零,也就是說它的調(diào)用時(shí)機(jī)是和act一致的。
mFragments.saveAllState();
這行代碼最終會(huì)調(diào)到FragmentManagerImpl.saveAllState方法桶蝎,代碼如下:

Parcelable saveAllState() {
        // Make sure all pending operations have now been executed to get
        // our state update-to-date.
        execPendingActions();

        mStateSaved = true;

        if (mActive == null || mActive.size() <= 0) {
            return null;
        }
        
        // First collect all active fragments.
        int N = mActive.size();
        FragmentState[] active = new FragmentState[N];
        boolean haveFragments = false;
        for (int i=0; i<N; i++) {
            Fragment f = mActive.get(i);
            if (f != null) {
                if (f.mIndex < 0) {
                    throwException(new IllegalStateException(
                            "Failure saving state: active " + f
                            + " has cleared index: " + f.mIndex));
                }

                haveFragments = true;

                FragmentState fs = new FragmentState(f);
                active[i] = fs;
                
                if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {
                    fs.mSavedFragmentState = saveFragmentBasicState(f);

                    if (f.mTarget != null) {
                        if (f.mTarget.mIndex < 0) {
                            throwException(new IllegalStateException(
                                    "Failure saving state: " + f
                                    + " has target not in fragment manager: " + f.mTarget));
                        }
                        if (fs.mSavedFragmentState == null) {
                            fs.mSavedFragmentState = new Bundle();
                        }
                        putFragment(fs.mSavedFragmentState,
                                FragmentManagerImpl.TARGET_STATE_TAG, f.mTarget);
                        if (f.mTargetRequestCode != 0) {
                            fs.mSavedFragmentState.putInt(
                                    FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG,
                                    f.mTargetRequestCode);
                        }
                    }

                } else {
                    fs.mSavedFragmentState = f.mSavedFragmentState;
                }
                
                if (DEBUG) Log.v(TAG, "Saved state of " + f + ": "
                        + fs.mSavedFragmentState);
            }
        }
        
        if (!haveFragments) {
            if (DEBUG) Log.v(TAG, "saveAllState: no fragments!");
            return null;
        }
        
        int[] added = null;
        BackStackState[] backStack = null;
        
        // Build list of currently added fragments.
        if (mAdded != null) {
            N = mAdded.size();
            if (N > 0) {
                added = new int[N];
                for (int i=0; i<N; i++) {
                    added[i] = mAdded.get(i).mIndex;
                    if (added[i] < 0) {
                        throwException(new IllegalStateException(
                                "Failure saving state: active " + mAdded.get(i)
                                + " has cleared index: " + added[i]));
                    }
                    if (DEBUG) Log.v(TAG, "saveAllState: adding fragment #" + i
                            + ": " + mAdded.get(i));
                }
            }
        }
        
        // Now save back stack.
        if (mBackStack != null) {
            N = mBackStack.size();
            if (N > 0) {
                backStack = new BackStackState[N];
                for (int i=0; i<N; i++) {
                    backStack[i] = new BackStackState(this, mBackStack.get(i));
                    if (DEBUG) Log.v(TAG, "saveAllState: adding back stack #" + i
                            + ": " + mBackStack.get(i));
                }
            }
        }
        
        FragmentManagerState fms = new FragmentManagerState();
        fms.mActive = active;
        fms.mAdded = added;
        fms.mBackStack = backStack;
        return fms;
    }

從上面的代碼我們可以看出驻仅,重點(diǎn)是對(duì)mActive的fragment的狀態(tài)保存,調(diào)用的是以下方法:

Bundle saveFragmentBasicState(Fragment f) {
        Bundle result = null;

        if (mStateBundle == null) {
            mStateBundle = new Bundle();
        }
        // 回調(diào)callback函數(shù)登渣,保存fragment的狀態(tài)
        f.performSaveInstanceState(mStateBundle);
        if (!mStateBundle.isEmpty()) {
            // 確實(shí)有保存的東西則設(shè)置result
            result = mStateBundle;
            mStateBundle = null;
        }
        // 保存fragment view樹的狀態(tài)
        if (f.mView != null) {
            saveFragmentViewState(f);
        }
        if (f.mSavedViewState != null) {
            if (result == null) {
                result = new Bundle();
            }
            // 將view樹的狀態(tài)作為一個(gè)key噪服、value放到result中
            result.putSparseParcelableArray(
                    FragmentManagerImpl.VIEW_STATE_TAG, f.mSavedViewState);
        }
        if (!f.mUserVisibleHint) {
            if (result == null) {
                result = new Bundle();
            }
            // Only add this if it's not the default value
            result.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG, f.mUserVisibleHint);
        }

        return result; // 返回最終的result作為fragment保存的狀態(tài)
    }

void saveFragmentViewState(Fragment f) {
        if (f.mView == null) {
            return;
        }
        if (mStateArray == null) {
            mStateArray = new SparseArray<Parcelable>();
        } else {
            mStateArray.clear();
        }
        f.mView.saveHierarchyState(mStateArray);
        if (mStateArray.size() > 0) {
            f.mSavedViewState = mStateArray;
            mStateArray = null;
        }
    }

在繼續(xù)分析之前,我們先來說下源碼里的這2個(gè)字段之間的關(guān)系:



從上面的源碼中我們可以看到mSavedViewState是作為fragment狀態(tài)mSavedFragmentState的一個(gè)key/value對(duì)存在的绍豁,即只是fragment狀態(tài)的一部分芯咧。

文章開頭現(xiàn)象的原因分析

這里的重點(diǎn)還是fragment源碼分析那篇文章中最后提到的FragmentManagerImpl的void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)方法,這里我們截取關(guān)鍵的片段分析:

  1. 當(dāng)fragment的狀態(tài)在向前move的時(shí)候,即變大到resume狀態(tài)的過程中敬飒,會(huì)經(jīng)歷這樣的代碼:

    f.restoreViewState的源碼如下:
final void restoreViewState(Bundle savedInstanceState) {
        // 第一次show的時(shí)候邪铲,這個(gè)字段為null,所以不會(huì)觸發(fā)
        // restoreHierarchyState方法
        if (mSavedViewState != null) {
            mView.restoreHierarchyState(mSavedViewState);
            mSavedViewState = null;
        }
        mCalled = false;
        onViewStateRestored(savedInstanceState);
        if (!mCalled) {
            throw new SuperNotCalledException("Fragment " + this
                    + " did not call through to super.onViewStateRestored()");
        }
    }

同時(shí)從源碼我們也可以看出无拗,此方法的調(diào)用時(shí)機(jī)是位于onActivityCreate之后带到,onStart之前,但都在onCreateView之后英染,所以如果mView.restoreHierarchyState起作用的話揽惹,那么我們?cè)趏nCreateView里面的設(shè)置自然會(huì)被沖掉。

  1. 當(dāng)fragment的狀態(tài)在向后move的時(shí)候四康,即變小到INITIALIZING的過程中搪搏,會(huì)經(jīng)歷這樣的代碼:


saveFragmentViewState(f)方法會(huì)在onStop之后,onDestroyView之前被調(diào)用闪金,這個(gè)方法我們前面介紹過疯溺,經(jīng)過這個(gè)方法之后,f.mSavedViewState會(huì)被設(shè)置為有效值哎垦。所以在我們前面的例子里囱嫩,當(dāng)下次重用同一個(gè)實(shí)例時(shí),由于f.mSavedViewState有值(即使fragment執(zhí)行了onDestroyView之類的callback漏设,調(diào)用了其initState方法墨闲,但mSavedViewState并不會(huì)被重置),所以就發(fā)生了前面截圖里的調(diào)用棧了郑口,即mView.restoreHierarchyState被調(diào)用了鸳碧。而我們知道checkbox關(guān)于狀態(tài)恢復(fù)的實(shí)現(xiàn)正好就是調(diào)用setChecked方法(在我們的案例里有多個(gè)checkbox,但只有1個(gè)是選中了潘酗,后面的所有都是沒選中的杆兵,再加上是從同一個(gè)layout文件中inflate出來的,大家都有相同的id仔夺,在保存的過程中后面的checkbox狀態(tài)就覆蓋了前面選中了的checkbox狀態(tài))琐脏,至此為止,真相大白缸兔,一切都解釋通了日裙。

總結(jié)

  1. fragment的狀態(tài)保存、恢復(fù)和f.mView的狀態(tài)保存惰蜜、恢復(fù)發(fā)生的時(shí)機(jī)不一樣昂拂,這兩者不能等同;

  2. fragment的狀態(tài)保存總體上是跟隨外部的act的狀態(tài)保存抛猖,當(dāng)fragment保存狀態(tài)時(shí)會(huì)觸發(fā)f.mView的保存格侯;fragment狀態(tài)的恢復(fù)大體上是重建其mActive鼻听、mAdded、mBackStack這3個(gè)字段联四,時(shí)機(jī)是在外部act.onCreate里觸發(fā)的撑碴;

  3. f.mView會(huì)在onDestroyView之前自動(dòng)保存view樹狀態(tài),并且在(同一個(gè)實(shí)例)onActivityCreate之后朝墩、onStart之前自動(dòng)恢復(fù)之前保存的狀態(tài)醉拓;

  4. 盡量別重用fragment實(shí)例,fragment最好不要有自己的字段收苏,需要的參數(shù)可以通過setArguments(Bundle args)這樣的方法傳遞進(jìn)去亿卤;

  5. 如果遇上會(huì)產(chǎn)生相同view id的布局時(shí),要特別關(guān)注下狀態(tài)保存鹿霸、恢復(fù)這方面排吴,看看是否正常work,多實(shí)際測試杜跷;

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末傍念,一起剝皮案震驚了整個(gè)濱河市矫夷,隨后出現(xiàn)的幾起案子葛闷,更是在濱河造成了極大的恐慌,老刑警劉巖双藕,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淑趾,死亡現(xiàn)場離奇詭異,居然都是意外死亡忧陪,警方通過查閱死者的電腦和手機(jī)扣泊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嘶摊,“玉大人延蟹,你說我怎么就攤上這事∫抖眩” “怎么了阱飘?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長虱颗。 經(jīng)常有香客問我沥匈,道長,這世上最難降的妖魔是什么忘渔? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任高帖,我火速辦了婚禮,結(jié)果婚禮上畦粮,老公的妹妹穿的比我還像新娘散址。我一直安慰自己乖阵,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布预麸。 她就那樣靜靜地躺著义起,像睡著了一般。 火紅的嫁衣襯著肌膚如雪师崎。 梳的紋絲不亂的頭發(fā)上默终,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音犁罩,去河邊找鬼齐蔽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛床估,可吹牛的內(nèi)容都是我干的含滴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼丐巫,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼谈况!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起递胧,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤碑韵,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后缎脾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體祝闻,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年遗菠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了联喘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辙纬,死狀恐怖豁遭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贺拣,我是刑警寧澤蓖谢,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站纵柿,受9級(jí)特大地震影響蜈抓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜昂儒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一沟使、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧渊跋,春花似錦腊嗡、人聲如沸着倾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卡者。三九已至腰湾,卻和暖如春脐湾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颤枪。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國打工底挫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恒傻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓建邓,卻偏偏與公主長得像盈厘,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子官边,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

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