注:本文中的recreate是指當內存不足時,Activity被回收,但再次來到此Activity時,系統重新恢復的過程。
例如:當Activity A到Activity B時,如果內存不足,A被回收,但當用戶按下Back鍵返回時,A又會被系統重新創建。
為了便於問題展開,我們首先來看一段最簡單的代碼
----------------代碼片段1------------------
1 package com.example.corn.corntest; 2 3 import android.app.Activity; 4 import android.os.Bundle; 5 6 public class MainActivity extends Activity { 7 8 @Override 9 protected void onCreate(Bundle savedInstanceState) { 10 super.onCreate(savedInstanceState); 11 12 setContentView(R.layout.activity_main); 13 } 14 }
我們發現,onCreated()作為Activity的生命周期,在回調的過程中有一個Bundle類型的形參savedInstanceState,按照中文的大概翻譯是“已經保存的實例狀態”。
那么,相應的問題也就出來了,何為實例狀態?為什么要保存實例狀態?如何保存?
1.何為實例狀態?
Android沿襲Java而來,很多概念與Java保持一致,只是在特性的Android場景中會有些稍微變化,如實例狀態。首先回顧一下Java中的實例狀態:實例及對象,狀態指的是對象的成員屬性。相應的,Android中以Activity實例狀態為例,指的是Activity實例對象中的成員屬性,但相應的有點區別的是,一般意義的理解上,此處的成員屬性並不包括Activity中的視圖級的成員(包括View級和Fragment級等)。
我們繼續看一段代碼
-------------------代碼片段2-----------------
1 public class MainActivity extends Activity { 2 public static final String TAG = MainActivity.class.getSimpleName(); 3 4 private EditText mContentEt; 5 private Button mCountBtn; 6 private TextView mCountTv; 7 private Button mGoBtn; 8 9 private String mContent; 10 private int mCount; 11 12 13 @Override 14 protected void onCreate(Bundle savedInstanceState) { 15 super.onCreate(savedInstanceState); 16 17 setContentView(R.layout.activity_main); 18 19 mContentEt = (EditText) findViewById(R.id.content_et); 20 mCountBtn = (Button) findViewById(R.id.count_btn); 21 mCountTv = (TextView) findViewById(R.id.count_tv); 22 mGoBtn = (Button) findViewById(R.id.go_btn); 23 24 mCount = 100; 25 mCountTv.setText(mCount + ""); 26 mCountBtn.setOnClickListener(new View.OnClickListener() { 27 @Override 28 public void onClick(View v) { 29 mCount ++; 30 mCountTv.setText(mCount + ""); 31 } 32 }); 33 34 mGoBtn.setOnClickListener(new View.OnClickListener() { 35 @Override 36 public void onClick(View v) { 37 Intent intent = new Intent(MainActivity.this, SecondActivity.class); 38 startActivity(intent); 39 } 40 }); 41 } 42 }
對應的xml文件為:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="vertical" 6 android:paddingLeft="100dip" 7 android:paddingTop="20dip"> 8 9 <TextView 10 android:layout_width="wrap_content" 11 android:layout_height="wrap_content" 12 android:text="請輸入內容" /> 13 14 15 <EditText 16 android:id="@+id/content_et" 17 android:layout_width="200dip" 18 android:layout_height="40dip" /> 19 20 <Button 21 android:id="@+id/count_btn" 22 android:layout_width="100dip" 23 android:layout_height="40dip" 24 android:layout_marginTop="40dip" 25 android:text="點擊計數" /> 26 27 <TextView 28 android:id="@+id/count_tv" 29 android:layout_width="200dip" 30 android:layout_height="40dip" 31 android:layout_marginTop="10dip" 32 android:background="#ccc" 33 android:gravity="center_vertical" 34 android:paddingLeft="20dip" 35 android:textColor="#ff0000" 36 android:textSize="13sp" /> 37 38 <Button 39 android:id="@+id/go_btn" 40 android:layout_width="100dip" 41 android:layout_height="40dip" 42 android:layout_marginTop="40dip" 43 android:text="點擊跳轉到第二個Activity" /> 44 45 </LinearLayout>
在此代碼片段中,Android中的實例狀態一般指的是mContent和mCount。
2.為什么要保存實例狀態?
有人可能會有這樣的疑惑,為什么要保存實例狀態,單純的Java代碼中好像沒有這一說法啊。具體的原因還要從Android中本身的內存回收機制說起。手機中給每個Android應用分配的內存都是具有一個上限的,不同的手機此上限值可能不同。Android虛擬機優先確保前台Activity或前台Service等具有的正常內存分配,這使得進入到后台的Activity在內存不足的情況下將會被系統主動銷毀並回收,此時當用戶按下Back鍵等返回時,系統將重新創建此Activity對應的實例。同時,為了確保良好的用戶體驗和邏輯的一致性,在系統主動回收Activity時,為程序提供可能的保存實例狀態的機制,以便當后續重新返回時系統能夠恢復實例狀態。
3.如何保存和恢復?
Android中為Actvity的實例狀態保存和恢復提供了相應的機制,通過提供相應的實例狀態保存和恢復回調,將狀態數據存儲到系統中的Bundle中。相應的回調函數分別為:onSaveInstanceState(Bundle)和onRestoreInstanceState(Bundle)。當然,對於實例狀態的恢復,也可以直接通過onCreate(Bundle)中的Bundle參數進行。onCreate(Bundle)和onRestoreInstanceState(Bundle)都能恢復實例狀態,且一般情況下,兩種方式恢復實例狀態功能相同,唯一比較有差別的地方,在於恢復實例狀態的時機,onRestoreInstanceState(Bundle)回調時機更加靠后。
下面,以代碼2片段為例,來具體看一下如何保存和恢復Activity實例狀態。
這段代碼很容易理解,一個是EditText輸入框,同時還有一個計算器按鈕,其初始值是100,每點擊一次計數按鈕顯示的計數增加1。另一個跳轉按鈕點擊后跳轉到其他Activity。首先,我們運行一下,看看代碼2片段顯示的效果。
在請輸入內如下方的輸入框中輸入內容“Corn”,然后點擊三下“點擊計算”按鈕,此時點擊計數下面的TextView顯示為103,然后點擊“點擊跳轉”,來到SecondActivity,再按下返回鍵,返回到MainActivity(注:為論述方便,本文中將此操作過程簡稱為“操作過程A”)。一般情況下(手機內存足夠),此時MainActivity就是跳轉之前的MainActivity,且顯示效果及內容沒有任何區別。
為了模擬當Activity處於后台時,可能因內存不足而被銷毀,而當再次返回到此Activity時,又會自動創建場景,我們可以進行如下操作:打開開發者選項 >> 不保留活動。
接下來代碼2片段適當修改
-----------------代碼片段3-------------------
1 public class MainActivity extends Activity { 2 public static final String TAG = MainActivity.class.getSimpleName(); 3 4 private EditText mContentEt; 5 private Button mCountBtn; 6 private TextView mCountTv; 7 private Button mGoBtn; 8 9 private String mContent; 10 private int mCount; 11 12 13 @Override 14 protected void onCreate(Bundle savedInstanceState) { 15 Log.d(TAG, "in onCreate >> this" + this + " savedInstanceState:" + savedInstanceState); 16 super.onCreate(savedInstanceState); 17 18 setContentView(R.layout.activity_main); 19 20 mContentEt = (EditText) findViewById(R.id.content_et); 21 mCountBtn = (Button) findViewById(R.id.count_btn); 22 mCountTv = (TextView) findViewById(R.id.count_tv); 23 mGoBtn = (Button) findViewById(R.id.go_btn); 24 25 mCount = 100; 26 mCountTv.setText(mCount + ""); 27 mCountBtn.setOnClickListener(new View.OnClickListener() { 28 @Override 29 public void onClick(View v) { 30 mCount++; 31 mCountTv.setText(mCount + ""); 32 } 33 }); 34 35 mGoBtn.setOnClickListener(new View.OnClickListener() { 36 @Override 37 public void onClick(View v) { 38 Intent intent = new Intent(MainActivity.this, SecondActivity.class); 39 startActivity(intent); 40 } 41 }); 42 } 43 44 45 @Override 46 protected void onSaveInstanceState(Bundle outState) { 47 Log.d(TAG, "in onSaveInstanceState >> this:" + this + " outState:" + outState); 48 super.onSaveInstanceState(outState); 49 } 50 51 @Override 52 protected void onRestoreInstanceState(Bundle savedInstanceState) { 53 Log.d(TAG, "in onRestoreInstanceState >> this:" + this + 54 " savedInstanceState:" + savedInstanceState); 55 super.onRestoreInstanceState(savedInstanceState); 56 } 57 58 @Override 59 protected void onDestroy() { 60 Log.d(TAG, "in onDestroy >> this" + this); 61 super.onDestroy(); 62 } 63 }
再次運行程序,進行“操作過程A”,此時,我們發現當回到MainActiviyt時,頁面執行短暫的白屏了下,然后才馬上顯示出MainActivity界面,但是,之前有所不同的是,此時MainActivity中的點擊計算下方的TextView顯示內容還原成了100。但請輸入內容中的EditText內容卻依然是“Corn”,咦,這到底是怎么回事呢?
為了一探究竟,我們打開AS中的logcat,看看關鍵的執行過程。
------不保留活動 關閉 >> 對應內存相對足夠的一般情況------
D/MainActivity: in onCreate >> thiscom.example.corn.corntest.MainActivity@41e69568 savedInstanceState:null D/MainActivity: in onSaveInstanceState >> this:com.example.corn.corntest.MainActivity@41e69568 outState:Bundle[{}]
------不保留活動 開啟 >> 對應內存不足銷毀相應Activity------
D/MainActivity: in onCreate >> thiscom.example.corn.corntest.MainActivity@41ec5bc8 savedInstanceState:null D/MainActivity: in onSaveInstanceState >> this:com.example.corn.corntest.MainActivity@41ec5bc8 outState:Bundle[{}] D/MainActivity: in onDestroy >> thiscom.example.corn.corntest.MainActivity@41ec5bc8 D/MainActivity: in onCreate >> thiscom.example.corn.corntest.MainActivity@41eef8c0 savedInstanceState:Bundle[mParcelledData.dataSize=620] D/MainActivity: in onRestoreInstanceState >> this:com.example.corn.corntest.MainActivity@41eef8c0 savedInstanceState:Bundle[{android:viewHierarchyState=Bundle[mParcelledData.dataSize=540]}]
通過打印出的日志我們發現:
1.當系統內存不足而將Aticity銷毀時,調用了其對應的生命周期回調方法onDestory,當返回時自動進行了Activity的自動創建過程,但重新創建的Activity與之前的Activity不再是同一個對象實例;
2.Activity重新創建時,重新執行了完整的生命周期方法的回調;
3.與Activity初始進來創建時不同的是,Activity重新創建的onCreate()回調方法中,Bundle形參不再是null,而是已經保存了實例狀態的Bundle對象。同時,我們也發現,onRestoreInstanceState(Bundle)在Activity重新創建過程中也進行了回調,且傳入了已經保存了實例狀態的Bundel對象;
由此,已經很自然的解釋了為什么當Activity重新創建時TextView里面的內容被還原成了100(因為onCreate()方法重新執行了一遍)。
那么我們如何在Activity重新創建時使得TextView中的內容與之前保持一致?
同時我們發現了一個令人不太理解的現象,為什么EditText能夠記住之前的內容且能夠理想的恢復狀態?
下面我們逐一回答:
問題一:如何在Activity重新創建時使得TextView中的內容與之前保持一致呢?
Activity中提供了onSaveInstanceState(Bundle)回調方法,至於onSaveInstanceState具體回調時機,本人在“Android總結篇系列”相關博文中已經做過總結,在此不再贅述。
我們可以通過此方法保存需要保存並恢復的Activiy實例狀態,如本文中的TextView中的內容變量mCount。
1 @Override 2 protected void onSaveInstanceState(Bundle outState) { 3 Log.d(TAG, "in onSaveInstanceState >> this:" + this + " outState:" + outState); 4 super.onSaveInstanceState(outState); 5 6 // 保存mCount值 7 outState.putInt("extra_count", mCount); 8 } 9 10 @Override 11 protected void onCreate(Bundle savedInstanceState) { 12 .... 13 14 if(savedInstanceState == null){ 15 mCount = 100; 16 } else { 17 mCount = savedInstanceState.getInt("extra_count"); 18 } 19 20 ... 21 22 23 }
或者,如前文所說,onCreate也可以不作改動,直接在onRestoreInstanceState(Bundle)中恢復狀態即可。
1 @Override 2 protected void onRestoreInstanceState(Bundle savedInstanceState) { 3 Log.d(TAG, "in onRestoreInstanceState >> this:" + this +
" savedInstanceState:" + savedInstanceState); 4 super.onRestoreInstanceState(savedInstanceState); 5 6 // 恢復mCount值及相應顯示效果 7 mCount = savedInstanceState.getInt("extra_count"); 8 mCountTv.setText(mCount + ""); 9 }
問題二:為什么EditText能夠記住之前的內容且能夠理想的恢復狀態?
這個問題就涉及到Activity中View級別的自我恢復機制,限於篇幅,將在下篇具體闡述。
最后,對本文的論述進行一下總結:
1.Activity中通過onSaveInstanceState(Bundle)回調函數去保存Activity實例狀態,保存實例狀態存在於系統Bundle中;
2.當系統內存不足時,Activity存在重新創建機制,場景的模擬可以通過打開開發者選項 >> 不保留活動進行
(當然,將app先設置成可以橫豎屏切換,然后切換橫豎屏也是可以的,不過,相對沒有“不保留活動”方便);
3.Activity重新創建時具有完整的Activity生命周期,且onCreate(Bundle)中的Bundle不為null,同時也回調了onRestoreInstanceState(Bundle)方法,因此,相應的恢復Activity狀態可以通過其中任意一種方式進行。