Categories
程式開發

第一行代碼——Android(二):掌握活動的生命週期


編者按:本文節選自郭霖著《第一行代碼——Android》一書中的部分章節。

活動的生命週期

掌握活動的生命週期對任何Android開發者來說都非常重要,當你深入理解活動的生命週期之後,就可以寫出更加連貫流暢的程序,並在如何合理管理應用資源方面發揮得游刃有餘。你的應用程序將會擁有更好的用戶體驗。

活動的生命週期:返回棧

經過前面幾節的學習,我相信你已經發現了這一點,Android中的活動是可以層疊的。我們每啟動一個新的活動,就會覆蓋在原活動之上,然後點擊Back鍵會銷毀最上面的活動,下面的一個活動就會重新顯示出來。

其實Android是使用任務(Task)來管理活動的,一個任務就是一組存放在棧裡的活動的集合,這個棧也被稱作返回棧(Back Stack)。棧是一種後進先出的數據結構,在默認情況下,每當我們啟動了一個新的活動,它會在返回棧中入棧,並處於棧頂的位置。而每當我們按下Back鍵或調用finish()方法去銷毀一個活動時,處於棧頂的活動會出棧,這時前一個入棧的活動就會重新處於棧頂的位置。系統總是會顯示處於棧頂的活動給用戶。

示意圖1展示了返回棧是如何管理活動入棧出棧操作的。

第一行代碼——Android(二):掌握活動的生命週期 1

圖 1 返回棧工作示意圖

活動的生命週期:活動狀態

每個活動在其生命週期中最多可能會有4種狀態。

  1. 運行狀態

當一個活動位於返回棧的棧頂時,這時活動就處於運行狀態。系統最不願意回收的就是處於運行狀態的活動,因為這會帶來非常差的用戶體驗。

  1. 暫停狀態

當一個活動不再處於棧頂位置,但仍然可見時,這時活動就進入了暫停狀態。你可能會覺得既然活動已經不在棧頂了,還怎麼會可見呢?這是因為並不是每一個活動都會佔滿整個屏幕的,比如對話框形式的活動只會佔用屏幕中間的部分區域,你很快就會在後面看到這種活動。處於暫停狀態的活動仍然是完全存活著的,系統也不願意去回收這種活動(因為它還是可見的,回收可見的東西都會在用戶體驗方面有不好的影響),只有在內存極低的情況下,系統才會去考慮回收這種活動。

  1. 停止狀態

當一個活動不再處於棧頂位置,並且完全不可見的時候,就進入了停止狀態。系統仍然會為這種活動保存相應的狀態和成員變量,但是這並不是完全可靠的,當其他地方需要內存時,處於停止狀態的活動有可能會被系統回收。

  1. 銷毀狀態

當一個活動從返回棧中移除後就變成了銷毀狀態。系統會最傾向於回收處於這種狀態的活動,從而保證手機的內存充足。

活動的生命週期:活動的生存期

Activity類中定義了7個回調方法,覆蓋了活動生命週期的每一個環節,下面就來一一介紹這7個方法。

  • onCreate()。這個方法你已經看到過很多次了,每個活動中我們都重寫了這個方法,它會在活動第一次被創建的時候調用。你應該在這個方法中完成活動的初始化操作,比如說加載佈局、綁定事件等。
  • onStart()。這個方法在活動由不可見變為可見的時候調用。
  • onResume()。這個方法在活動準備好和用戶進行交互的時候調用。此時的活動一定位於返回棧的棧頂,並且處於運行狀態。
  • onPause()。這個方法在系統準備去啟動或者恢復另一個活動的時候調用。我們通常會在這個方法中將一些消耗CPU的資源釋放掉,以及保存一些關鍵數據,但這個方法的執行速度一定要快,不然會影響到新的棧頂活動的使用。
  • onStop()。這個方法在活動完全不可見的時候調用。它和onPause()方法的主要區別在於,如果啟動的新活動是一個對話框式的活動,那麼onPause()方法會得到執行,而onStop()方法並不會執行。
  • onDestroy()。這個方法在活動被銷毀之前調用,之後活動的狀態將變為銷毀狀態。
  • onRestart()。這個方法在活動由停止狀態變為運行狀態之前調用,也就是活動被重新啟動了。

以上7個方法中除了onRestart()方法,其他都是兩兩相對的,從而又可以將活動分為3種生存期。

  • 完整生存期。活動在onCreate()方法和onDestroy()方法之間所經歷的,就是完整生存期。一般情況下,一個活動會在onCreate()方法中完成各種初始化操作,而在onDestroy()方法中完成釋放內存的操作。
  • 可見生存期。活動在onStart()方法和onStop()方法之間所經歷的,就是可見生存期。在可見生存期內,活動對於用戶總是可見的,即便有可能無法和用戶進行交互。我們可以通過這兩個方法,合理地管理那些對用戶可見的資源。比如在onStart()方法中對資源進行加載,而在onStop()方法中對資源進行釋放,從而保證處於停止狀態的活動不會佔用過多內存。
  • 前台生存期。活動在onResume()方法和onPause()方法之間所經歷的就是前台生存期。在前台生存期內,活動總是處於運行狀態的,此時的活動是可以和用戶進行交互的,我們平時看到和接觸最多的也就是這個狀態下的活動。

為了幫助你能夠更好地理解,Android官方提供了一張活動生命週期的示意圖,如圖2所示。

第一行代碼——Android(二):掌握活動的生命週期 2

圖 2 活動的生命週期

活動的生命週期:體驗活動的生命週期

講了這麼多理論知識,也是時候該實戰一下了,下面我們將通過一個實例,讓你可以更加直觀地體驗活動的生命週期。

這次我們不准備在ActivityTest這個項目的基礎上修改了,而是新建一個項目。因此,首先關閉ActivityTest項目,點擊導航欄File→Close Project。然後再新建一個ActivityLifeCycleTest項目,新建項目的過程你應該已經非常清楚了,不需要我再進行贅述,這次我們允許Android Studio幫我們自動創建活動和佈局,這樣可以省去不少工作,創建的活動名和佈局名都使用默認值。

這樣主活動就創建完成了,我們還需要分別再創建兩個子活動——NormalActivity和DialogActivity,下面一步步來實現。

右擊com.example.activitylifecycletest包→New→Activity→Empty Activity,新建NormalActivity,佈局起名為normal_layout。然後使用同樣的方式創建DialogActivity,佈局起名為dialog_layout。

現在編輯normal_layout.xml文件,將裡面的代碼替換成如下內容:



    


這個佈局中我們就非常簡單地使用了一個TextView,用於顯示一行文字,在下一章中你將會學到更多關於TextView的用法。

然後再編輯dialog_layout.xml文件,將裡面的代碼替換成如下內容:



    


兩個佈局文件的代碼幾乎沒有區別,只是顯示的文字不同而已。

NormalActivity和DialogActivity中的代碼我們保持默認就好,不需要改動。

其實從名字上你就可以看出,這兩個活動一個是普通的活動,一個是對話框式的活動。可是我們並沒有修改活動的任何代碼,兩個活動的代碼應該幾乎是一模一樣的,在哪裡有體現出將活動設成對話框式的呢?別著急,下面我們馬上開始設置。修改AndroidManifest.xml的標籤的配置,如下所示:





這裡是兩個活動的註冊代碼,但是DialogActivity的代碼有些不同,我們給它使用了一個android:theme屬性,這是用於給當前活動指定主題的,Android系統內置有很多主題可以選擇,當然我們也可以定制自己的主題,而這裡@style/Theme.AppCompat.Dialog則毫無疑問是讓DialogActivity使用對話框式的主題。

接下來我們修改activity_main.xml,重新定制主活動的佈局,將裡面的代碼替換成如下內容:



    

可以看到,我們在LinearLayout中加入了兩個按鈕,一個用於啟動NormalActivity,一個用於啟動DialogActivity。

最後修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity {

    public static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG,"onCreate");
        setContentView(R.layout.activity_main);
        Button startNormalActivity = (Button) findViewById(R.id.start_normal_
            activity);
        Button startDialogActivity = (Button) findViewById(R.id.start_dialog_
            activity);
        startNormalActivity.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, NormalActivity.class);
                startActivity(intent);
            }
        });
        startDialogActivity.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, DialogActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG, "onRestart");
    }

}

onCreate()方法中,我們分別為兩個按鈕註冊了點擊事件,點擊第一個按鈕會啟動NormalActivity,點擊第二個按鈕會啟動DialogActivity。然後在Activity的7個回調方法中分別打印了一句話,這樣就可以通過觀察日誌的方式來更直觀地理解活動的生命週期。

現在運行程序,效果如圖3所示。

第一行代碼——Android(二):掌握活動的生命週期 3

圖 3 MainActivity界面

這時觀察logcat中的打印日誌,如圖4所示。

第一行代碼——Android(二):掌握活動的生命週期 4

圖 4 啟動程序時的打印日誌

可以看到,當MainActivity第一次被創建時會依次執行onCreate()onStart()onResume()方法。然後點擊第一個按鈕,啟動NormalActivity,如圖5所示。

第一行代碼——Android(二):掌握活動的生命週期 5

圖 5 NormalActivity界面

此時的打印信息如圖6所示。

第一行代碼——Android(二):掌握活動的生命週期 6

圖 6 打開NormalActivity時的打印日誌

由於NormalActivity已經把MainActivity完全遮擋住,因此onPause()onStop()方法都會得到執行。然後按下Back鍵返回MainActivity,打印信息如圖7所示。

第一行代碼——Android(二):掌握活動的生命週期 7

圖 7 返回MainActivity的打印日誌

由於之前MainActivity已經進入了停止狀態,所以onRestart()方法會得到執行,之後又會依次執行onStart()onResume()方法。注意此時onCreate()方法不會執行,因為MainActivity並沒有重新創建。

然後再點擊第二個按鈕,啟動DialogActivity,如圖8所示。

第一行代碼——Android(二):掌握活動的生命週期 8

圖 8 DialogActivity界面

此時觀察打印信息,如圖9所示。

第一行代碼——Android(二):掌握活動的生命週期 9

圖 9 打開DialogActivity時的打印日誌

可以看到,只有onPause()方法得到了執行,onStop()方法並沒有執行,這是因為DialogActivity並沒有完全遮擋住MainActivity,此時MainActivity只是進入了暫停狀態,並沒有進入停止狀態。相應地,按下Back鍵返回MainActivity也應該只有onResume()方法會得到執行,如圖10所示。

第一行代碼——Android(二):掌握活動的生命週期 10

圖 10 再次返回MainActivity的打印日誌

最後在MainActivity按下Back鍵退出程序,打印信息如圖11所示。

第一行代碼——Android(二):掌握活動的生命週期 11

圖 11 退出程序時的打印日誌

依次會執行onPause()onStop()onDestroy()方法,最終銷毀MainActivity。

這樣活動完整的生命週期你已經體驗了一遍,是不是理解得更加深刻了?

活動的生命週期:活動被回收了怎麼辦

前面我們已經說過,當一個活動進入到了停止狀態,是有可能被系統回收的。那麼想像以下場景:應用中有一個活動A,用戶在活動A的基礎上啟動了活動B,活動A就進入了停止狀態,這個時候由於系統內存不足,將活動A回收掉了,然後用戶按下Back鍵返回活動A,會出現什麼情況呢?其實還是會正常顯示活動A的,只不過這時並不會執行onRestart()方法,而是會執行活動A的onCreate()方法,因為活動A在這種情況下會被重新創建一次。

這樣看上去好像一切正​​常,可是別忽略了一個重要問題,活動A中是可能存在臨時數據和狀態的。打個比方,MainActivity中有一個文本輸入框,現在你輸入了一段文字,然後啟動NormalActivity,這時MainActivity由於系統內存不足被回收掉,過了一會你又點擊了Back鍵回到MainActivity,你會發現剛剛輸入的文字全部都沒了,因為MainActivity被重新創建了。

如果我們的應用出現了這種情況,是會嚴重影響用戶體驗的,所以必須要想想辦法解決這個問題。查閱文檔可以看出,Activity中還提供了一個onSaveInstanceState()回調方法,這個方法可以保證在活動被回收之前一定會被調用,因此我們可以通過這個方法來解決活動被回收時臨時數據得不到保存的問題。

onSaveInstanceState()方法會攜帶一個Bundle類型的參數,Bundle提供了一系列的方法用於保存數據,比如可以使用putString()方法保存字符串,使用putInt()方法保存整型數據,以此類推。每個保存方法需要傳入兩個參數,第一個參數是鍵,用於後面從Bundle中取值,第二個參數是真正要保存的內容。

在MainActivity中添加如下代碼就可以將臨時數據進行保存:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    String tempData = "Something you just typed";
    outState.putString("data_key", tempData);
}

數據是已經保存下來了,那麼我們應該在哪裡進行恢復呢?細心的你也許早就發現,我們一直使用的onCreate()方法其實也有一個Bundle類型的參數。這個參數在一般情況下都是null,但是如果在活動被系統回收之前有通過onSaveInstanceState()方法來保存數據的話,這個參數就會帶有之前所保存的全部數據,我們只需要再通過相應的取值方法將數據取出即可。

修改MainActivity的onCreate()方法,如下所示:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG, "onCreate");
    setContentView(R.layout.activity_main);
    if (savedInstanceState != null) {
        String tempData = savedInstanceState.getString("data_key");
        Log.d(TAG, tempData);
    }
    ...
}

取出值之後再做相應的恢復操作就可以了,比如說將文本內容重新賦值到文本輸入框上,這裡我們只是簡單地打印一下。

不知道你有沒有察覺,使用Bundle來保存和取出數據是不是有些似曾相識呢?沒錯!我們在使用Intent傳遞數據時也是用的類似的方法。這裡跟你提醒一點,Intent還可以結合Bundle一起用於傳遞數據,首先可以把需要傳遞的數據都保存在Bundle對像中,然後再將Bundle對象存放在Intent裡。到了目標活動之後先從Intent中取出Bundle,再從Bundle中一一取出數據。具體的代碼我就不寫了,要學會舉一反三哦。

圖書簡介https://www.ituring.com.cn/book/1841

第一行代碼——Android(二):掌握活動的生命週期 12

相關閱讀

第一行代碼——Android(一):前行必備,如何使用日誌工具