1. 程式人生 > >Fragment的狀態儲存和恢復

Fragment的狀態儲存和恢復

前言

我們知道,在activity中,當配置發生改變,比如螢幕方向發生變化時,activity會被銷燬,然後重新建立。在activity中有兩個方法用於儲存和恢復狀態,分別是:

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

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

當配置發生變化時,onSavaInstanceState會被呼叫,我們可以用它的引數outState來儲存資料和狀態;當activity重建的時候,會呼叫onRestoreInstanceState,然後利用它的引數savedInstanceState將activity恢復到銷燬前的狀態。另外在onCreate中也可以用於資料恢復,它的引數savedInstanceState和onRestoreInstanceState是一樣的。
同樣的google在fragment中也實現了類似的狀態儲存和恢復功能,以下我們分析下fragment的狀態儲存和恢復。

fragment的狀態儲存和恢復

實際上,fragment的狀態儲存和恢復機制和activity是完全一致的。說明解決方案之前,我們首先應該弄清楚下邊的幾個問題:

  1. 什麼時候儲存狀態,什麼時候恢復狀態
  2. 儲存和恢復什麼狀態(fragment的狀態還是view的狀態?)
  3. setRetainInstance(true)

什麼時候儲存狀態,什麼時候恢復狀態

當系統認為你的fragment存在被銷燬的可能時(不包括使用者主動退出fragment導致其被銷燬,比如按BACK鍵後fragment被主動銷燬), onSaveInstanceState 就會被呼叫,給你一個機會來儲存狀態。以下幾種情況可能導致fragment被異常銷燬;

  1. 按HOME鍵返回桌面時
  2. 按選單鍵回到系統後臺,並選擇了其他應用時
  3. 按電源鍵時
  4. 螢幕方向切換時

這四種情況中,前三種情況都是因為應用處於後臺,根據Android系統的快取機制,為了保持系統的流暢執行,處於後臺的應用有很大的可能被清除,既然應用已經不在了,fragment自然也被銷燬了;最後一種情況是由於螢幕方向切換導致配置改變,activity被銷燬,fragment也隨之被銷燬了。
在這些情況下,我們就可以通過 onSaveInstanceState 方法將資料儲存到它的引數bundle物件中了。以上觸發onSaveInstanceState 的狀況和activity完全一致。
有了儲存,就應該有恢復。和activity不同的是,fragment沒有onRestoreInstanceState方法,但是我們可以在onActivityCreated中恢復資料,它的引數中的bundle物件包含了在異常銷燬前儲存的資料。

儲存和恢復什麼狀態(fragment的狀態還是view的狀態?)

在說明這個問題之前,我們應該做兩個小實驗

  1. 第一個實驗:在fragment中的佈局中加入一個EditText,設定寬高和id,不做其他設定,然後執行程式。在橫屏的時候,在EditText輸入隨意的內容,然後橫屏,你會發現EditText中的內容依然存在,難道fragment可以自動儲存view的狀態?
  2. 第二個實驗:在fragment中的佈局中加入一個EditText,只設置寬高,不做其他設定,然後執行程式。在橫屏的時候,在EditText輸入隨意的內容,然後橫屏,你會發現EditText中的內容不在了,難道fragment又不能儲存view的狀態了?
  3. 第三實驗:在fragment的佈局中加入一個EditText,一個TextView,一個Button,當點選button的時候,將Edittext的內容賦給TextView,程式碼如下:
    佈局檔案中三個view

    <EditText
    android:id="@+id/editText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>
    
    <TextView
    android:id="@+id/show"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>   
    
    <Button
    android:id="@+id/btn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="按鈕"/>
    
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.d(TAG, ">>>onCreateView: ");
        View view = inflater.inflate(R.layout.fragment_retain, null);
        final EditText editText = (EditText) view.findViewById(R.id.editText);
        final TextView textView = (TextView) view.findViewById(R.id.show);
        Button btn = (Button) view.findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!editText.getText().toString().equals("")) {
                    textView.setText(editText.getText());
                }
            }
        });
        return view;
    }

橫屏的時候輸入內容,然後點選按鈕,textview被賦值,然後旋轉螢幕,textview的內容不見了。(實際情況是,第一次選擇的時候內容還在,然後再次旋轉回來以後內容就不在了,對於這個現象還沒有找到原因)

通過以上的實驗,我們發現即使我們沒有在onSaveInstanceState 中顯示的儲存view的狀態,但是有時候view的狀態還是儲存並恢復了,這是怎麼回事那?

其實通過前兩個實驗我們可以看出view的狀態是否能被自動儲存和id是有關的,通過後兩個實驗我們發現除了和id有關應該還和其他的設定有關。

原來,在Android中當Activity的onSaveInstanceState呼叫的時候,Activity會自動收集View層級中每個View的狀態。請注意只有在內部實現了View的儲存/恢復狀態方法的View才會被收集到。一旦onRestoreInstanceState方法被呼叫,Activity會把收集的資料傳送回給View結構樹中具有相同android:id配置的View。

這裡要注意兩點,
第一,view必須已經實現了儲存/恢復狀態方法

@Override
public Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
// Save current View's state here
return bundle;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
super.onRestoreInstanceState(state);
// Restore View's state here
}

我們平時常用的控制元件基本都實現了這兩個方法,所以可以自動儲存狀態,如果我們要自定義View的話,也應該實現這兩個方法用來儲存狀態。這裡有個例外,textview需要宣告android:freezeText=”true”才能儲存和恢復狀態。

第二,必須有Android:id屬性
如果我們沒有id屬性的話,系統就沒法找到我們的view,也就無法恢復狀態。

知道了上邊這一點之後,我們知道了,原來在Android的view設計中,view本身是具有狀態儲存和恢復功能的,所以我認為當我們在寫程式碼的時候不應該打破這種設計模式,我們不應該在fragment的onSaveInstanceState中儲存view的狀態,而應該只用來儲存fragment本身的狀態和資料,也就是它的成員變數。這種思路是正確的。我們想一下,當我們將一個fragment加入到回退棧之後,然後在這個fragment中再開啟另外一個fragment的時候,前一個fragment的檢視會被銷燬,但是例項還在,例項在,說明fragment的狀態還在,檢視銷燬了,說明view的狀態不在了;但是當我們從後面的fragment返回時會發現,view的狀態又恢復了,我們並沒有做任何額外的工作,這就說明了view是自動恢復狀態的,不需要我們過多的干涉。

setRetainInstance

我們還可以通過在onCreate方法中設定setRetianInstance(true)的方法來達到儲存資料的目的;當我們設定為true時,在螢幕方向改變時,fragment的例項不會被銷燬,它只是被銷燬了檢視,並且從activity上解綁,然後重新建立的時候只會建立檢視,因為例項還存在,所以不走onCreate方法。整個生命週期如下:

onDestroyView-->onCreateView-->onActivityCreated

需要特別注意的時,使用這種方法的時候,fragment不能加入到回退棧中,而且只適用於因為配置改變比如螢幕方向改變導致的fragment例項可能被銷燬的狀況,如果因為應用處於後臺或者記憶體緊張等原因,fragment的例項還是可能被銷燬的。具體原因可以瀏覽Stack Overflow

出於以上的原因,使用這種方式儲存狀態就有很大的侷限性了。一般情況下,它可以用於儲存activity的資料;比如一個activity中從網路上下載了很多資料,保留圖片之類的,當螢幕方向改變時,呼叫onSaveInstance,儲存資料到bundle中並不能有多大效果,因為bundle是一個適用於小資料的容器,如果過大可能有OOM的風險或其他問題。這個時候可以在onSaveInstance中開啟一個沒有檢視的fragment,然後將資料儲存到fragment中,然後當activity重建時,在onCreate中獲得這個fragment的例項,取出資料,並且remove這個fragment,具體實現可以參考Android應用開發:Fragment與大型資料快取