Android提升篇系列:Activity recreate(Activity 重新創建/自我恢復)機制(一)


注:本文中的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狀態可以通過其中任意一種方式進行。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM