Activity Fragment 狀態(tài)保存與恢復

參考資料:

  1. http://www.cnblogs.com/mengdd/p/5582244.html
  2. https://inthecheesefactory.com/blog/fragment-state-saving-best-practices/en
    大部分內(nèi)存摘自上述博客,感謝原作者的分享;

開發(fā)中,由于狀態(tài)保存這種場景需要模擬,會造成了一定的開發(fā)成本,如:內(nèi)存不夠時,app被回收,喚醒時,可能出現(xiàn)錯誤情況;

Activity的銷毀與重建

  1. 正常情況:back鍵,與調(diào)用finish方法;
  2. 特殊情況:當Activity處于onStop狀態(tài)時,如:退到后臺,并且長時間不用時,極有可能會被系統(tǒng)回收,用來釋放一些內(nèi)存;
  3. 旋屏情況:如果Activity支持旋屏,每次旋屏都會導致activity的銷毀與重建;

特殊情況下
當activity回到前臺時,如果被回收了,此時,系統(tǒng)會重新創(chuàng)建新的Activity實例,并利用舊實例存下來的數(shù)據(jù)來恢復界面;這些數(shù)據(jù)稱為:instance state,存在Bundle對象中;

缺省狀態(tài)下,系統(tǒng)會把每一個View對象保存起來(比如EditText對象中的文本,ListView中的滾動條位置等(注意:需要提供android:id)),即如果activity實例被銷毀和重建,那么不需要你編碼,layout狀態(tài)會恢復到前次狀態(tài)。但是如果你的activity需要恢復更多的信息,比如成員變量信息,則需要自己動手寫了。

在這里就涉及到回調(diào)函數(shù)onSaveInstanceState(),注意 Activity onSaveInstanceState() 有2個重載方法,一般我們使用下面的:

 @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
    }

系統(tǒng)會在用戶離開activity的時候調(diào)用這個函數(shù),并且傳遞給它一個Bundle object,如果系統(tǒng)稍后需要重建這個activity實例,它會傳遞同一個Bundle object到onRestoreInstanceState() 和 onCreate() 方法中去。

舉個例子:當ActivityA在前臺時,如果用戶按下home鍵,或者 打開一個新的ActivityB,或來電等情形下,系統(tǒng)會自動調(diào)用 onSaveInstanceState()方法;

Activity - onSaveInstanceState()觸發(fā)的2個情況

  1. 系統(tǒng)回收時,調(diào)用;喚醒時,執(zhí)行回調(diào)onRestoreXXX;
  2. 用戶離開Activity時,調(diào)用;喚醒時,如果系統(tǒng)未回收,不執(zhí)行onRestoreXXX;

** 存儲Activity狀態(tài)**
我們就在 onSaveInstanceState() 方法中來存儲狀態(tài),一定要調(diào)用super;

 @Override
    protected void onSaveInstanceState(Bundle outState) {
      outState.putString(KEY_FRAGMENT_TAG, mFragmentCurrentTag);
        super.onSaveInstanceState(outState);
    }

恢復Activity狀態(tài)
當被回收喚醒時,會執(zhí)行 onCreate() 和onRestoreInstanceState()回調(diào)函數(shù);

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        .....
        .....

        // 如果程序喚醒了
        if (savedInstanceState != null) {
            restoreFragments();
            mFragmentCurrentTag = savedInstanceState.getString(KEY_FRAGMENT_TAG);
            mIsSaveInstanceCalled = true;
        }

Activity的數(shù)據(jù)加載

一般在onCreate中,加載Activity的數(shù)據(jù),其他回調(diào)方法很可能被調(diào)用,比如:如在onStart中加載了數(shù)據(jù),按home,馬上又回到頁面時,onStart會執(zhí)行;
示例代碼:

/**
     * 自動記錄 滾動文字
     */
    ListView listView;
    /**
     * 記錄內(nèi)容
     */
    EditText et;

    // 在這里初始化數(shù)據(jù)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_state1);

        listView = (ListView) findViewById(R.id.list);
        et = (EditText) findViewById(R.id.et_test);

        Log.e(TAG, "onCreate: " + savedInstanceState);

        String[] a = new String[255 - 64 + 1];
        for (int i = 64; i < 255; i++) {
            a[i-64] = ">>>>>" + ((char) i);
        }
        listView.setAdapter(new ArrayAdapter<>(getApplicationContext(), android.R.layout.simple_list_item_1, a));
    }

    @Override
    protected void onStart() {
        super.onStart();
    }

Fragment的狀態(tài)保存和恢復

相對于Activity,F(xiàn)ragment的情況,就顯得特別復雜,如果有嵌套Fragment,則更復雜了。如果頁面不是特別復雜,能不用嵌套fragment,則不用;
** Fragment啟動時的生命周期回調(diào):**

fragment啟動時

** 按home鍵時:**

按home或退到后臺

之所以執(zhí)行 onSaveInstanceXXX是因為Activity執(zhí)行了這個方法;

旋轉(zhuǎn)屏幕時(view的狀態(tài)自己維護了):

旋轉(zhuǎn)時

Fragment add 與 remove

remove()是移除fragment, 如果fragment不加入到back stack, remove()的時候, fragment的生命周期會一直走到onDetach().類似于 按 back,activity 正常結束一樣;

添加到 backStack就不一樣了;remove(), fragment 的生命會走到 onDestroyView(),不會執(zhí)行onDetach(),此時 fragment本身的實例是存在的,成員變量也存在,但是view銷毀了;不要把Fragment的實例狀態(tài)和View狀態(tài)混在一起處理,這點非常重要

當Fragment從back stack中返回, 實際上是經(jīng)歷了一次View的銷毀和重建, 但是它本身并沒有被重建.
即View狀態(tài)需要重建, 實例狀態(tài)不需要重建.

當Fragment被另一個Fragment replace(), 并且壓入back stack中, 此時它的View是被銷毀的, 但是它本身并沒有被銷毀.
也即, 它走到了onDestroyView(), 卻沒有走onDestroy()和onDetact().
等back回來的時候, 它的view會被重建, 重新從onCreateView()開始走生命周期.
在這整個過程中, 該Fragment中的成員變量是保持不變的, 只有View會被重新創(chuàng)建.
在這個過程中, instance state的saving并沒有發(fā)生.

我們來看看:

// 添加FragmentB       
findViewById(R.id.addFragmentB).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentManager fragmentManager = getSupportFragmentManager();
                Fragment f = fragmentManager.findFragmentByTag(FragmentB.class.getName());
                if (f == null) {
                    f = Fragment.instantiate(getApplicationContext(), FragmentB.class.getName());
                }
                // 如果不添加返回棧,remove() 該fragment實例會銷毀的
                fragmentManager.beginTransaction().add(R.id.container, f, FragmentB.class.getName())
                        .addToBackStack(null).commit();
            }
        });

// 移除FragmentB
        findViewById(R.id.removeFragmentB).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentManager fragmentManager = getSupportFragmentManager();
                Fragment f = fragmentManager.findFragmentByTag(FragmentB.class.getName());
                if (f != null) {
                    fragmentManager.beginTransaction().remove(f).commit();
                }
            }
        });

上面的代碼,我們第一段添加FragmentB,第二段,移除FragmentB,我們打印一下生命周期方法:

add, remove

可以看到Fragment并沒有回到onDetach,onDestroy,也即:fragment 是其對應的View消耗了。但是Fragment的示例還是存在的;
下面顯示再次add FragmentB,打印如下:

remove后,再Add

這個時候我們看看成員變量吧:

成員變量存在

如果FragmentB不添加返回鍵,調(diào)用remove(),就類似按返回鍵一樣了,會直接消耗FragmentB,這個機制跟Activity是一致的;

Fragment onCreateView多次執(zhí)行

了解了上面之后,也就明白為什么 onCreateView會多次執(zhí)行了吧。
常見的做法,是記錄 一個 rootView來記錄一下 onCreateView中返回的view,下一次Fragment onCreateView回調(diào)時,判斷rootView是否為null,來進行是否加載數(shù)據(jù),等其他操作;

如下代碼:

    private View rootView;//緩存Fragment view
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if(rootView==null){
            rootView=inflater.inflate(R.layout.tab_fragment, null);
        }
 //緩存的rootView需要判斷是否已經(jīng)被加過parent,
// 如果有parent需要從parent刪除,要不然會發(fā)生這個rootview已經(jīng)有parent的錯誤。
        ViewGroup parent = (ViewGroup) rootView.getParent();
        if (parent != null) {
            parent.removeView(rootView);
        } 
        return rootView;
    }
      

這樣可以解決問題,特別是界面layout元素特別多的時候,這樣可以看上去可以避免 inflate多次執(zhí)行一樣,的確可以這樣,但就是看著特別別扭
但google這樣設計,或許是有其他考慮的吧,這里不是很明白;
;如果界面真的切換頻率過高,可以考慮使用 hide與show來操作了,來避免上面的代碼;

如果不考慮上面的實現(xiàn)方式,我們完全可以不加判斷來做,讓其直接 inflate吧,但是,這里 界面上view元素的 狀態(tài)是如何恢復的呢?也沒看到,調(diào)用onSaveInstacneXX等之類的方法,還是從大神博客中,找到了;

Fragment狀態(tài)保存入口:####

摘自:http://www.cnblogs.com/mengdd/p/5582244.html

Fragment狀態(tài)保存入口

3個入口:

  1. Activity的狀態(tài)保存, 在Activity的onSaveInstanceState()里, 調(diào)用了FragmentManger的saveAllState()方法, 其中會對mActive中各個Fragment的實例狀態(tài)和View狀態(tài)分別進行保存.
  1. FragmentManager還提供了public方法: saveFragmentInstanceState(), 可以對單個Fragment進行狀態(tài)保存, 這是提供給我們用的, 其中調(diào)用的saveFragmentBasicState()方法即為情況一中所用, 圖中已畫出標記.
  2. FragmentManager的moveToState()方法中, 當狀態(tài)回退到ACTIVITY_CREATED, 會調(diào)用saveFragmentViewState()方法, 保存View的狀態(tài).

Fragment狀態(tài)恢復入口:####

狀態(tài)恢復

三個恢復的入口和三個保存的入口剛好對應.

  1. 在Activity重新創(chuàng)建的時候, 恢復所有的Fragment狀態(tài).
  1. 如果調(diào)用了FragmentManager的方法: saveFragmentInstanceState(), 返回值得到的狀態(tài)可以用Fragment的setInitialSavedState()方法設置給新的Fragment實例, 作為初始狀態(tài).
  2. FragmentManager的moveToState()方法中, 當狀態(tài)正向創(chuàng)建到CREATED時, Fragment自己會恢復View的狀態(tài).

這三個入口分別對應的情況是:

  1. 入口1對應系統(tǒng)銷毀和重建新實例.
  1. 入口2對應用戶自定義銷毀和創(chuàng)建新Fragment實例的狀態(tài)傳遞.
  2. 入口3對應同一Fragment實例自身的View狀態(tài)重建.

Fragment狀態(tài)保存恢復和Activity關聯(lián):

對應入口1的情況,類似于Activity狀態(tài)保存于恢復處理;比較好理解,不進行分析了;

Fragment同一實例的View狀態(tài)恢復

對應入口3的情況,也即:activity是resume狀態(tài)下,切換fragment,是如何保存自己的狀態(tài)的?
Fragment被add過,當remove()此fragment時,發(fā)現(xiàn) view 的 onSaveInstanceState會被調(diào)用,調(diào)用棧如下:

remove時,view狀態(tài)保存調(diào)用棧

因為 commit不是立刻執(zhí)行,所以跟蹤的堆棧,commit那部分調(diào)用丟失了,在這里,可以看到 moveToState, saveFragmentViewState調(diào)用了,這也就說明了,remove時,fragment其內(nèi)部的view會保存狀態(tài);

來看看立即執(zhí)行 commit的調(diào)用棧:

立刻執(zhí)行commit,remove時,view狀態(tài)保存調(diào)用棧

不同F(xiàn)ragment實例間的狀態(tài)保存和恢復

入口1與入口3都是自動處理,入口2需要用戶手動來處理;
如果需要在不同fragment實例間傳遞狀態(tài),就需要用到入口2了,手動調(diào)用
FragmentManager 的 saveFragmentInstanceState 方法:

public abstract Fragment.SavedState saveFragmentInstanceState(Fragment f);

// 具體實現(xiàn)為:
@Override
    public Fragment.SavedState saveFragmentInstanceState(Fragment fragment) {
        if (fragment.mIndex < 0) {
            throwException( new IllegalStateException("Fragment " + fragment
                    + " is not currently in the FragmentManager"));
        }
        if (fragment.mState > Fragment.INITIALIZING) {
            Bundle result = saveFragmentBasicState(fragment);
            return result != null ? new Fragment.SavedState(result) : null;
        }
        return null;
    }

恢復時,調(diào)用Fragment setInitialSavedState 來實現(xiàn);

 /**
     * Set the initial saved state that this Fragment should restore itself
     * from when first being constructed, as returned by
     * {@link FragmentManager#saveFragmentInstanceState(Fragment)
     * FragmentManager.saveFragmentInstanceState}.
     *
     * @param state The state the fragment should be restored from.
     */
    public void setInitialSavedState(SavedState state) {
        if (mIndex >= 0) {
            throw new IllegalStateException("Fragment already active");
        }
        mSavedFragmentState = state != null && state.mState != null
                ? state.mState : null;
    }

注意: 只能在Fragment被加入之前設置(add, replace).
利用這兩個方法可以更加自由地保存和恢復狀態(tài), 而不依賴于Activity.
這樣處理以后, 不必保存Fragment的引用, 每次切換的時候雖然都new了新的實例, 但是舊的實例的狀態(tài)可以設置給新實例.

詳細例子請參考:
http://www.cnblogs.com/mengdd/p/5582244.html

 final String STATE_ = "state_";
 // 存儲fragment狀態(tài)
 SparseArray<Fragment.SavedState> savedStateSparseArray = new SparseArray<>();

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_state_restore_demo);

        // 恢復
        if (savedInstanceState != null) {
            savedStateSparseArray = savedInstanceState.getSparseParcelableArray(STATE_);
        }
          
        // 切換
        findViewById(R.id.tab1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // save current tab
                Fragment tab2Fragment = getSupportFragmentManager().findFragmentByTag(FragmentF.class.getName());
                if (tab2Fragment != null) {
                    // 保存 tab2Fragment的狀態(tài)
                    saveFragmentState(1, tab2Fragment);
                }

                // restore last state, 每次都new
                FragmentE tab1Fragment = new FragmentE();
                restoreFragmentState(0, tab1Fragment);

                // show new tab
                getSupportFragmentManager().beginTransaction()
                        .replace(R.id.content_container, tab1Fragment, FragmentE.class.getName())
                        .commit();
            }
        });

        findViewById(R.id.tab2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Fragment tab1Fragment = getSupportFragmentManager().findFragmentByTag(FragmentE.class.getName());
                if (tab1Fragment != null) {
                    saveFragmentState(0, tab1Fragment);
                }

                // 每次都new
                FragmentF tab2Fragment = new FragmentF();
                restoreFragmentState(1, tab2Fragment);

                getSupportFragmentManager().beginTransaction()
                        .replace(R.id.content_container, tab2Fragment, FragmentF.class.getName())
                        .commit();
            }
        });

    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSparseParcelableArray(STATE_, savedStateSparseArray);
    }

/**
     * 手動存狀態(tài)
     *
     * @param index
     * @param fragment
     */
    private void saveFragmentState(int index, Fragment fragment) {
        Fragment.SavedState savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment);
        savedStateSparseArray.put(index, savedState);
    }

    /**
     * 手動調(diào)用 恢復狀態(tài)
     *
     * @param index
     * @param fragment
     */
    private void restoreFragmentState(int index, Fragment fragment) {
        Fragment.SavedState savedState = savedStateSparseArray.get(index);
        fragment.setInitialSavedState(savedState);
    }
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容