Activity的狀態(tài)保存和恢復(fù)
Activity的狀態(tài)什么時候需要保存和恢復(fù)
Activity的銷毀一般分為兩種情況:
- 當用戶按返回按鈕或你的Activity通過調(diào)用
finish()銷毀時,這屬于正常銷毀,此時是不需要恢復(fù)狀態(tài)的,因為下次回來又是重新創(chuàng)建新的實例。 - 如果Activity當前被停止或長期未使用,或者前臺Activity需要更多資源以致系統(tǒng)必須關(guān)閉后臺進程恢復(fù)內(nèi)存,系統(tǒng)也可能會銷毀Activity,這屬于非正常銷毀,盡管Activity實例被銷毀,但系統(tǒng)會保存其狀態(tài),這樣,如果用戶導(dǎo)航回該Activity,系統(tǒng)會使用保存了該Activity被銷毀時的狀態(tài)數(shù)據(jù)來創(chuàng)建Activity的新實例。
屏幕旋轉(zhuǎn)、鍵盤可用性改變、 語言改變都可以歸結(jié)為第二種情況;值得一提的是,如果需要模擬這種情況的Activity銷毀,可以打開開發(fā)者選項,選擇不保留活動(英文為Do not keep activities),即可模擬內(nèi)存不足時的系統(tǒng)行為。
保存和恢復(fù)Activity狀態(tài)
什么時候調(diào)用Activity的onSaveInstanceState()
- 屏幕旋轉(zhuǎn)重建會調(diào)用onSaveInstanceState()
- 啟動另一個activity: 當前activity在離開前會調(diào)用onSaveInstanceState()
- 按Home鍵的情形和啟動另一個activity一樣, 當前activity在離開前會onSaveInstanceState()
什么時候不調(diào)用Activity的onRestoreInstanceState()
- 屏幕旋轉(zhuǎn)重建會調(diào)用onRestoreInstanceState()
- 啟動另一個activity,返回時如果因為被系統(tǒng)殺死需要重建, 則會從onCreate()重新開始生命周期, 調(diào)用onRestoreInstanceState()
- 按Home鍵的情形和啟動另一個activity一樣,用戶再次點擊應(yīng)用圖標返回時, 如果重建發(fā)生, 則會調(diào)用onCreate()和onRestoreInstanceState()
什么時候都不調(diào)用
- 用戶主動finish()掉的activity不會調(diào)用onSaveInstanceState(), 包括主動按back退出的情況.
- 新建的activity, 從onCreate()開始, 不會調(diào)用onRestoreInstanceState().

當你的Acivity開始被停止時,系統(tǒng)會調(diào)用onSaveInstanceState()方法,以便你的Activity可以使用鍵值對集合來保存狀態(tài)信息。此方法默認實現(xiàn)了自動保存有關(guān)Activity的視圖層次結(jié)構(gòu)的狀態(tài)信息,例如EditText中的文本信息或ListView的滾動位置。為了保存其他狀態(tài)信息,你必須在onSaveInstanceState()方法中將鍵值對添加到Bundle對象。例如:
public class MainActivity extends Activity {
static final String SOME_VALUE = "int_value";
static final String SOME_OTHER_VALUE = "string_value";
@Override
protected void onSaveInstanceState(Bundle savedInstanceState) {
// Save custom values into the bundle
savedInstanceState.putInt(SOME_VALUE, someIntValue);
savedInstanceState.putString(SOME_OTHER_VALUE, someStringValue);
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(savedInstanceState);
}
}
系統(tǒng)會在Activity被銷毀前調(diào)用上述方法。然后系統(tǒng)會調(diào)用onRestoreInstanceState方法,我們可以從bundle中恢復(fù)狀態(tài):
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
// Always call the superclass so it can restore the view hierarchy
super.onRestoreInstanceState(savedInstanceState);
// Restore state members from saved instance
someIntValue = savedInstanceState.getInt(SOME_VALUE);
someStringValue = savedInstanceState.getString(SOME_OTHER_VALUE);
}
您可以選擇實現(xiàn)系統(tǒng)在 onStart() 方法之后調(diào)的 onRestoreInstanceState(),而不是在 onCreate()期間恢復(fù)狀態(tài)。系統(tǒng)只在存在要恢復(fù)的已保存狀態(tài)時調(diào)用 onRestoreInstanceState(),因此你無需檢查 Bundle是否為 null,實例狀態(tài)也可以在ActivityonCreate()方法中恢復(fù),但是onRestoreInstanceState()方法中是最方便的。這確保所有的初始化已經(jīng)完成,并允許子類來決定是否使用默認實現(xiàn)。
注意:onSaveInstanceState和onRestoreInstanceState不能保證一起調(diào)用。當有可能銷毀活動的可能性時,Android調(diào)用onSaveInstanceState()。 但是,有些情況下調(diào)用onSaveInstanceState,但不會銷毀活動,因此不會調(diào)用onRestoreInstanceState。
保存和恢復(fù)Fragment狀態(tài)
Fragments也有onSaveInstanceState()方法:
public class MySimpleFragment extends Fragment {
private int someStateValue;
private final String SOME_VALUE_KEY = "someValueToSave";
// Fires when a configuration change occurs and fragment needs to save state
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt(SOME_VALUE_KEY, someStateValue);
super.onSaveInstanceState(outState);
}
}
然后,我們可以從onCreateView中保存的狀態(tài)中取出數(shù)據(jù):
public class MySimpleFragment extends Fragment {
// ...
// Inflate the view for the fragment based on layout XML
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.my_simple_fragment, container, false);
if (savedInstanceState != null) {
someStateValue = savedInstanceState.getInt(SOME_VALUE_KEY);
// Do something with value if needed
}
return view;
}
}
為了正確保存Fragments狀態(tài),我們需要確保在配置更改時避免不必要地重新創(chuàng)建Fragments。在配置更改后,在Activity中初始化的任何Fragments都需要通過tag進行查找:
public class ParentActivity extends AppCompatActivity {
private MySimpleFragment fragmentSimple;
private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";
@Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) { // saved instance state, fragment may exist
// look up the instance that already exists by tag
fragmentSimple = (MySimpleFragment)
getSupportFragmentManager().findFragmentByTag(SIMPLE_FRAGMENT_TAG);
} else if (fragmentSimple == null) {
// only create fragment if they haven't been instantiated already
fragmentSimple = new MySimpleFragment();
}
}
}
注意:這就需要我們在使用事務(wù)將Fragment放入Activity中要包含一個用于查找的標記。
public class ParentActivity extends AppCompatActivity {
private MySimpleFragment fragmentSimple;
private final String SIMPLE_FRAGMENT_TAG = "myfragmenttag";
@Override
protected void onCreate(Bundle savedInstanceState) {
// ... fragment lookup or instantation from above...
// Always add a tag to a fragment being inserted into container
if (!fragmentSimple.isInLayout()) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container, fragmentSimple, SIMPLE_FRAGMENT_TAG)
.commit();
}
}
}


對于Fragment來說,有一些特殊情況不同于Activity,我覺得你需要知道這些情況。一旦Fragment從后退棧中返回,它的View 會被銷毀,并重新創(chuàng)建

在這種情況下,F(xiàn)ragment并不會被銷毀,只有Fragment中的View 會被銷毀。 結(jié)果是,并不會發(fā)生任何實例狀態(tài)的保存。但是上面展示的這些View在Fragment生命周期中被重新創(chuàng)建時,發(fā)生了什么?
在這種情況下,F(xiàn)ragment中的View 狀態(tài)的保存/恢復(fù)會被內(nèi)部調(diào)用。結(jié)果就是,每一個實現(xiàn)了內(nèi)部View 狀態(tài)保存/恢復(fù)的View ,將會被自動的保存并且恢復(fù)狀態(tài),例如帶有android:freezeText="true"屬性的EditText或者TextView。就之前完美顯示的一樣。
注意,在這種情況下,只有View 被銷毀(并重建)了。Fragment仍然在這里,就像它內(nèi)部的成員變量一樣。所以你不需要對它們做任何事情。
View的狀態(tài)保存和恢復(fù)
注意: 每一個你需要開啟View狀態(tài)保存和恢復(fù)的View設(shè)置android:id屬性,不然它們的狀態(tài)不能正確的恢復(fù)。
ListView
通常當你旋轉(zhuǎn)屏幕時,應(yīng)用程序?qū)G失屏幕上列表的滾動位置和其他狀態(tài)。要正確保留ListView的狀態(tài),可以在onPause()中存儲實例狀態(tài)并在onViewCreated中恢復(fù)狀態(tài),如下所示:
// YourActivity.java
private static final String LIST_STATE = "listState";
private Parcelable mListState = null;
// Write list state to bundle
@Override
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
mListState = getListView().onSaveInstanceState();
state.putParcelable(LIST_STATE, mListState);
}
// Restore list state from bundle
@Override
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
mListState = state.getParcelable(LIST_STATE);
}
@Override
protected void onResume() {
super.onResume();
loadData(); // make sure data has been reloaded into adapter first
// ONLY call this part once the data items have been loaded back into the adapter
// for example, inside a success callback from the network
if (mListState != null) {
myListView.onRestoreInstanceState(mListState);
mListState = null;
}
}
注意:在調(diào)用onRestoreInstanceState之前,必須把數(shù)據(jù)源加載到適配器中,換句話說就是直到從網(wǎng)絡(luò)或數(shù)據(jù)庫加載數(shù)據(jù)之后,再讓ListView調(diào)用onRestoreInstanceState。
RecyclerView
與ListView類似:
// YourActivity.java
public final static int LIST_STATE_KEY = "recycler_list_state";
Parcelable listState;
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
// Save list state
listState = mLayoutManager.onSaveInstanceState();
state.putParcelable(LIST_STATE_KEY, mListState);
}
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
// Retrieve list state and list/item positions
if(state != null)
listState = state.getParcelable(LIST_STATE_KEY);
}
@Override
protected void onResume() {
super.onResume();
if (mListState != null) {
mLayoutManager.onRestoreInstanceState(listState);
}
}
處理運行時變更
有些設(shè)備配置可能會在運行時發(fā)生變化(例如屏幕方向、鍵盤可用性及語言)。 發(fā)生這種變化時,Android 會重啟正在運行的 Activity(先后調(diào)用 onDestroy()和 onCreate())。重啟行為旨在通過利用與新設(shè)備配置匹配的備用資源自動重新加載您的應(yīng)用,來幫助它適應(yīng)新配置。
要妥善處理重啟行為,Activity 必須通過常規(guī)的Activity 生命周期恢復(fù)其以前的狀態(tài),在 Activity 生命周期中,Android 會在銷毀 Activity 之前調(diào)用 onSaveInstanceState(),以便你保存有關(guān)應(yīng)用狀態(tài)的數(shù)據(jù)。 然后,你可以在 onCreate() 或 onRestoreInstanceState()期間恢復(fù) Activity 狀態(tài)。
要測試應(yīng)用能否在保持應(yīng)用狀態(tài)完好的情況下自行重啟,您應(yīng)該在應(yīng)用中執(zhí)行各種任務(wù)時調(diào)用配置變更(例如,更改屏幕方向)。 您的應(yīng)用應(yīng)該能夠在不丟失用戶數(shù)據(jù)或狀態(tài)的情況下隨時重啟,以便處理如下事件:配置發(fā)生變化,或者用戶收到來電并在應(yīng)用進程被銷毀很久之后返回到應(yīng)用。
解決方案
你可能會遇到這種情況:重啟應(yīng)用并恢復(fù)大量數(shù)據(jù)不僅成本高昂,而且給用戶留下糟糕的使用體驗。 在這種情況下,您有兩個其他選擇:
在配置變更期間保留對象允許 Activity 在配置變更時重啟,但是要將有狀態(tài)對象傳遞給 Activity 的新實例。
自行處理配置變更阻止系統(tǒng)在某些配置變更期間重啟 Activity,但要在配置確實發(fā)生變化時接收回調(diào),這樣,您就能夠根據(jù)需要手動更新 Activity。
在配置變更期間保留對象
當 Android 系統(tǒng)因配置變更而關(guān)閉 Activity 時,不會銷毀您已標記為要保留的 Activity 的片段。 您可以將此類片段添加到 Activity 以保留有狀態(tài)的對象。
要在運行時配置變更期間將有狀態(tài)的對象保留在片段中,請執(zhí)行以下操作:
- 擴展 Fragment類并聲明對有狀態(tài)對象的引用。
- 在創(chuàng)建Fragment后調(diào)用 setRetainInstance(boolean)。
- 將Fragment添加到 Activity。
- 重啟 Activity 后,使用 FragmentManager檢索片段。
例如,按如下方式定義片段:
public class RetainedFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
public void setData(MyDataObject data) {
this.data = data;
}
public MyDataObject getData() {
return data;
}
}
注意:盡管你可以存儲任何對象,但是切勿傳遞與 Activity綁定的對象,例如,Drawable、Adapter、View或其他任何與 Context關(guān)聯(lián)的對象。否則,它將泄漏原始 Activity 實例的所有視圖和資源。 (泄漏資源意味著應(yīng)用將繼續(xù)持有這些資源,但是無法對其進行垃圾回收,因此可能會丟失大量內(nèi)存。)
然后,使用 FragmentManager將片段添加到 Activity。在運行時配置變更期間再次啟動 Activity 時,您可以獲得片段中的數(shù)據(jù)對象。 例如,按如下方式定義 Activity:
public class MyActivity extends Activity {
private RetainedFragment dataFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
// add the fragment
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, “data”).commit();
// load the data from the web
dataFragment.setData(loadMyData());
}
// the data is available in dataFragment.getData()
...
}
@Override
public void onDestroy() {
super.onDestroy();
// store the data in the fragment
dataFragment.setData(collectMyLoadedData());
}
}
在此示例中,onCreate()
添加了一個片段或恢復(fù)了對它的引用。此外,onCreate()還將有狀態(tài)的對象存儲在片段實例內(nèi)部。onDestroy()對所保留的片段實例內(nèi)的有狀態(tài)對象進行更新。
自行處理配置變更
如果應(yīng)用在特定配置變更期間無需更新資源,并且因性能限制您需要盡量避免重啟,則可聲明 Activity 將自行處理配置變更,這樣可以阻止系統(tǒng)重啟 Activity。
注:自行處理配置變更可能導(dǎo)致備用資源的使用更為困難,因為系統(tǒng)不會為您自動應(yīng)用這些資源。 只能在您必須避免 Activity 因配置變更而重啟這一萬般無奈的情況下,才考慮采用自行處理配置變更這種方法,而且對于大多數(shù)應(yīng)用并不建議使用此方法。
要聲明由 Activity 處理配置變更,請在清單文件中編輯相應(yīng)的 <activity>
元素,以包含 android:configChanges
屬性以及代表要處理的配置的值。android:configChanges
屬性的文檔中列出了該屬性的可能值(最常用的值包括 "orientation"和 "keyboardHidden"
,分別用于避免因屏幕方向和可用鍵盤改變而導(dǎo)致重啟)。
例如,以下清單文件代碼聲明的 Activity 可同時處理屏幕方向變更和鍵盤可用性變更:
<activity android:name=".MyActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name">
現(xiàn)在,當其中一個配置發(fā)生變化時,MyActivity不會重啟。相反,MyActivity會收到對 onConfigurationChanged()的調(diào)用。向此方法傳遞Configuration對象指定新設(shè)備配置。您可以通過讀取 Configuration中的字段,確定新配置,然后通過更新界面中使用的資源進行適當?shù)母?。調(diào)用此方法時,Activity 的 Resources對象會相應(yīng)地進行更新,以根據(jù)新配置返回資源,這樣,您就能夠在系統(tǒng)不重啟 Activity 的情況下輕松重置 UI 的元素。
注意:從 Android 3.2(API 級別 13)開始,當設(shè)備在縱向和橫向之間切換時,“屏幕尺寸”也會發(fā)生變化。因此,在開發(fā)針對 API 級別 13 或更高版本(正如 minSdkVersion
和 targetSdkVersion
屬性中所聲明)的應(yīng)用時,若要避免由于設(shè)備方向改變而導(dǎo)致運行時重啟,則除了 "orientation"
值以外,您還必須添加 "screenSize"
值。 也就是說,您必須聲明 android:configChanges="orientation|screenSize"
。但是,如果您的應(yīng)用面向 API 級別 12 或更低版本,則 Activity 始終會自行處理此配置變更(即便是在 Android 3.2 或更高版本的設(shè)備上運行,此配置變更也不會重啟 Activity)。
例如,以下 onConfigurationChanged() 實現(xiàn)檢查當前設(shè)備方向:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Checks the orientation of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
}
}
Configuration 對象代表所有當前配置,而不僅僅是已經(jīng)變更的配置。大多數(shù)時候,您并不在意配置具體發(fā)生了哪些變更,而且您可以輕松地重新分配所有資源,為您正在處理的配置提供備用資源。 例如,由于 Resources 對象現(xiàn)已更新,因此您可以通過 setImageResource() 重置任何 ImageView,并且使用適合于新配置的資源(如提供資源中所述)。
參考資料