老生常談-Activity

不洗碗工作室發表於2019-02-28

不洗碗工作室 @Author fhyPayaso

對於activity的七個聲生命週期回撥,總是被大家翻來覆去的說,甚至說的都有些厭煩了,這部分知識雖然基礎但也很重要,誰都不想在面試的時候只說出個一知半解,下面的分析是對閱讀《安卓開發藝術探索》第一章後的整理和思考。

一、正常情況下的生命週期分析

先來上一張大家都熟悉的流程圖,來複習一遍活動的生命週期

老生常談-Activity

然後來簡單說說這七個回撥方法

  • onCreate : activity被建立時呼叫,一般在這個方法中進行活動的初始化工作,如設定佈局檔案、載入資料、繫結控制元件等。

  • onStart : 經歷該回撥後,activity由不可見變為可見,但此時處於後臺可見,還不能和使用者進行互動。

  • onResume : 已經可見的activity從後臺來到前臺,可以和使用者進行互動。

  • onPause : 當使用者啟動了新的activity,原來的activity不再處於前臺,也無法和使用者進行互動,並且緊接著就會呼叫onStop()方法,但如果使用者這時立刻按返回鍵回到原activity,就會呼叫onResume()方法讓活動重新回到前臺。而且在官方文件中給出了說明,不允許在onPause()方法中執行耗時操作,因為這會影響到新activity的啟動,具體原因我們在後面分析。

  • onStop : 這個回撥代表了activity由可見變為完全不可見,在這裡可以進行一些稍微重量級的操作。需要注意的是,處於onPause()onStop()回撥後的activity優先順序很低,當有優先順序更高的應用需要記憶體時,該應用就會被殺死,那麼當再次返回原activity的時候,會重新呼叫activity的onCreate()方法。

  • onDestroy : 來到了這個回撥,說明activity即將被銷燬,應該將資源的回收和釋放工作在該方法中執行。

  • onRestart : 這個回撥代表了activity由完全不可見重新變為可見的過程,當activity經歷了onStop()回撥變為完全不可見後,如果使用者返回原activity,便會觸發該回撥,並且緊接著會觸發onStart()來使活動重新可見。

想必大家已經對這個過程非常熟悉了,下面我們通過一些實際的場景來更加深入的理解一下活動的啟動流程。

1、 由活動A啟動活動B時,活動A的onPause()與活動B的onResume()哪一個先執行?

下面建立兩個正常的活動MainActivityFirstActivity,在MainActivity中設定按鈕點選進入FirstActivity,看看會發生什麼:

老生常談-Activity

可以看到,是舊的Activity先執行onPause,新活動才開始啟動。下面點選返回按鈕:

老生常談-Activity

點選返回後,同樣是新Activity先執行onPause,舊的活動才開始重新啟動,進行onRestart->onStart->onResume的流程,也就是說當發生活動切換時,是原活動先執行onPause,然後緊接著目標活動開始建立或重新啟動。

2、dialog是否會對生命週期產生影響

從定義上來說,如果一個活動不在前臺,也並非完全不可見,這個活動就會處在onPause()的暫停狀態,我們來模擬一下這種情況,在MainActivity中設定三個按鈕,第一個按鈕點選後會彈出一個標準的AlertDialog,第二個按鈕會彈出一個全屏的AlertDialog,第三個按鈕點選會出現一個主題為Theme.AppCompat.Dialog的activity然後觀察生命週期的變化:

老生常談-Activity

首先可以看到,無論是正常的dialog還是全屏的dialog,活動依然維持在onResume()的狀態,說明單純的dialog並不會引起生命週期的變化。下面來看dialog主題的activity:

老生常談-Activity

在啟動DialogActivity後,原來的活動進入onPause(),新活動正常進行onCreate->onStart->onResume的流程,而原來的活動因為並沒有完全不可見,所以也沒有執行onStop,事實上除了dialog主題的活動,一些透明主題的活動也能達到同樣的效果,接下來我們點選返回按鈕:

老生常談-Activity

由於MainActivity根本沒有進入onStop的狀態,所以返回時也不會進行onRestart->onStart的流程,而是直接onResume回到前臺。

二、異常狀態下活動的生命週期

當活動在執行過程中發生了某些異常情況時,上述所討論的生命週期流程可能會受到影響,這裡主要討論兩種異常情況。

1、資源配置改變導致activity重建

最常見的一種情況就是橫豎屏的切換導致資源的變化,當程式啟動時,會根據不同的配置載入不同的資源,例如橫豎屏兩個狀態對應著兩張不同的資源圖片。如果在應用使用過程中螢幕突然旋轉,那麼activity就會因為系統配置發生改變而銷燬重建,載入合適的資源。

(1) 資料儲存

對於活動重新建立,我們如何保證activity中的已有資料不丟失呢,系統為我們提供了onSaveInstanceStateonRestoreInstanceState來儲存和獲取資料。

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Log.i(TAG, "onSaveInstanceState: ");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    Log.i(TAG, "onRestoreInstanceState: ");
}
複製程式碼
老生常談-Activity

在活動異常銷燬之前,系統會呼叫onSaveInstanceState,我們可以在Bundle型別的引數中儲存想要的資訊,之後這個Bundle物件會作為引數傳遞給onRestoreInstanceStateonCreat方法,這樣在重新建立時就可以獲取資料了。關於這兩個方法,有幾點需要注意的地方:

  • onSaveInstanceState方法的呼叫時機是在onStop之前,與onPause沒有固定的時序關係。而onRestoreInstanceState方法則是在onStart之後呼叫。

  • 正常情況下的活動銷燬並不會呼叫這兩個方法,只有當活動異常銷燬並且有機會重新展示的時候才會進行呼叫,除了資源配置的改變外,activity因記憶體不足被銷燬也是通過這兩個方法儲存資料。

  • onRestoreInstanceStateonCreat都可以進行資料恢復工作,但是根據官方文件建議採用在onRestoreInstanceState中去恢復。

  • onRestoreInstanceStateonRestoreInstanceState這兩個方法中,系統會預設為我們進行一定的恢復工作,例如EditText中的文字資訊、ListView中的滾動位置等,下面對一些控制元件觀察實際儲存效果。

    • EditText:個人在對EditText實驗的時候,發現轉屏後文字資訊並沒有被儲存,經過查詢,發現了這樣一句話:

      “Note: In order for the Android system to restore the state of the views in your activity, each view must have a unique ID, supplied by the android:id attribute.”
      Android系統儲存和還原View的狀態必須有一個唯一的ID

      果然加上id之後EditText中的資訊可以被自動儲存了。

    • TextView:這裡指的是通過setText方法動態設定文字內容,在這種情況下即使加了id也無法自動儲存,這種情況可以通過給TextView設定freezesText屬性才能自動儲存,當然這條屬性對EditText也同樣適用。

(2) 防止重建

我們已經知道預設情況下,資源配置改變會導致活動的重新建立,但我們可以通過對活動android:configChanges屬性的設定使活動防止重新被建立,我們來看看這個屬性中有哪些內容:

屬性值 含義
mcc SIM卡唯一標識IMSI(國際移動使用者標識碼)中的國家程式碼,由三位數字組成,中國為:460 這裡標識mcc程式碼發生了改變
mnc SIM卡唯一標識IMSI(國際移動使用者標識碼)中的運營商程式碼,有兩位數字組成,中國移動TD系統為00,中國聯通為01,電信為03,此項標識mnc發生了改變
locale 裝置的本地位置發生了改變,一般指的是切換了系統語言
touchscreen 觸控式螢幕發生了改變
keyboard 鍵盤型別發生了改變,比如使用者使用了外接鍵盤
keyboardHidden 鍵盤的可訪問性發生了改變,比如使用者調出了鍵盤
navigation 系統導航方式發生了改變
screenLayout 螢幕佈局發生了改變,很可能是使用者啟用了另外一個顯示裝置
fontScale 系統字型縮放比例發生了改變,比如使用者選擇了個新的字號
uiMode 使用者介面模式發生了改變,比如開啟夜間模式-API8新新增
orientation 螢幕方向發生改變,比如旋轉了手機螢幕
screenSize 當螢幕尺寸資訊發生改變(當編譯選項中的minSdkVersion和targeSdkVersion均低於13時不會導致Activity重啟)API13新新增
smallestScreenSize 裝置的物理螢幕尺寸發生改變,這個和螢幕方向沒關係,比如切換到外部顯示裝置-API13新新增
layoutDirection 當佈局方向發生改變的時候,正常情況下無法修改佈局的layoutDirection的屬性-API17新新增

如果不希望某些資源配置改變時活動被重建,只需在manifest中為相應活動新增屬性即可,例如 configChanges="orientation"可以防止橫豎屏引發的重啟,然而事實上單加這條屬性並沒有什麼效果,因為在api13之後,新新增的屬性screenSize屬性也會跟著裝置的橫豎切換而改變,所以正確的配置應該是configChanges="orientation|screenSize";而在api13之前,正確的配置應該是configChanges="orientation|keyboardHidden"

這裡還要介紹一個重寫方法onConfigurationChanged,用來監聽資源配置的改變,這個方法只有在設定了configChanges並且相應的屬性發生了變化時才會被呼叫,

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
	//監聽橫豎屏的變化
    Log.i(TAG, "onConfigurationChanged: "+newConfig.orientation);
}
複製程式碼

2、低優先順序的activity由於記憶體不足被殺死

這種情況的資料儲存方法和上一種情況相同,在這裡簡單說一下系統回收程式的優先順序:

(1) 前臺程式:
  • 持有使用者正在互動的activity,即生命週期處於onResume狀態的活動。
  • 該程式有繫結到正在互動的Activity的service或前臺service。
(2) 可見程式:

這種程式雖然不在前臺,但是仍然可見。

  • 該程式持有的Activity執行了onPause但未執行onStop。例如原活動啟動了一個 dialog主題的activity,但此時原活動並非完全不可見。
  • 該程式有service繫結到可見的或前臺Activity。
(3) 服務程式:

程式中持有一個service,同時不屬於上面兩種情況。

(4) 後臺程式:

不屬於上面三種情況,但程式持有一個不可見的activity,即執行了onStop但未執行onDestroy的狀態。

(5) 空程式:

不包含任何活躍的應用元件,作用是加快下次啟動這個程式中元件所需要的時間,優先順序最低。

activity的啟動模式

和生命週期一樣,activity的四種launchMode也非常重要但又特別容易混淆,首先,activity是以任務棧的形式建立和銷燬的,棧是一種“後進先出”的資料結構,在預設情況下,啟動第一個activity時,系統將會為它建立一個任務棧並將活動置於棧底,而從這個activity啟動的其他activity將會依次入棧,當使用者連續按下返回鍵時,任務棧中的activity會從棧頂開始依次銷燬。但是這樣有一個弊端,就是對於某些activity我們不希望它總是重新建立,這時就需要採用不同的啟動模式,下面就簡單複習下activity的四種啟動模式 :

  • standard(標準模式) : 這是activity的預設啟動模式,只要啟動activity就會建立一個新例項,例如有兩個活動ActivityA和AciivityB,現在從活動A中連續3次啟動B活動,那麼活動B就會依次建立三個例項,按順序進入ActivityA所在的任務棧中。
老生常談-Activity

執行adb shell dumpsys activity命令觀察任務棧中的實際情況:

老生常談-Activity
  • singleTop(棧頂複用) :在這種啟動模式下,首先會判斷要啟動的活動是否已經存在於棧頂,如果是的話就不建立新例項,直接複用棧頂活動。如果要啟動的活動不位於棧頂或在棧中無例項,則會建立新例項入棧。例如棧中有活動A、B、C,啟動模式全部為singleTop,現在想要新建一個活動C,執行完成後任務棧中的情況依然為A、B、C; 但是如果新建一個活動A,因為A不位於棧頂,所以會重新建立例項入棧,任務棧變為:A、B、C、A,

    老生常談-Activity

    初始任務棧狀態

    老生常談-Activity

    接著啟動活動A

    老生常談-Activity

    可以看到活動A被重新建立入棧,但如果是啟動活動C,棧內活動不會改變,只不過活動C會先經歷onPause,然後回撥onNewIntent方法,緊接著執行onResume

    老生常談-Activity
  • singleTask(棧內複用) : 這種模式比較複雜,是一種棧內單例模式,當一個activity啟動時,會進行兩次判斷

    • 首先會尋找是否有這個活動需要的任務棧,如果沒有就建立這個任務棧並將活動入棧,如果有的話就進入下一步判斷。

    • 第二次判斷這個棧中是否存在該activity的例項,如果不存在就新建activity入棧,如果存在的話就直接複用,並且帶有clearTop效果,會將該例項上方的所有活動全部出棧,令這個activity位於棧頂。

    場景一:假設當前任務棧中只有活動A,想要從A啟動launchModesingleTask的活動B,但是活動B指定的任務棧與A不同,這裡用到了TaskAffinity屬性,相當於指定了想要的任務棧,下面會詳細介紹。

      <activity
          android:name=".test.ActivityA"
          android:taskAffinity="com.example.a41061.task1">
          <intent-filter>
              <action android:name="android.intent.action.MAIN"/>
              <category android:name="android.intent.category.LAUNCHER"/>
          </intent-filter>
      </activity>
    
      <activity
          android:name=".test.ActivityB"
          android:launchMode="singleTask"
          android:taskAffinity="com.example.a41061.task2"/>
    複製程式碼

    啟動後可以看到活動B執行在了一個新task中。

    老生常談-Activity

    場景二: 當前任務棧task1中存在活動A,從A中連續啟動三個活動,順序為B->C->B,B、C的啟動模式均為singleTask,請求棧為task2,最後的啟動結果將和上一種場景一樣,不再重複展示,這裡體現了singleTask模式的clearTop屬性,第二次啟動activityB後會複用棧底的例項,並將activityC出棧。

  • singleInstance(單例) : 這種模式是真正的單例模式,以這種模式啟動的活動會單獨建立一個任務棧,並且依然遵循棧內複用的特性,保證了這個棧中只能存在這一個活動。

還有一些需要注意的屬性

  • **onNewIntent()**方法 : 後三種模式都會出現活動複用的情況,一旦活動被複用,就會回撥用onNewIntent方法,通過這個方法中的Intent引數就可以進行頁面的更新,舉一個實際應用場景的例子:

    • 在活動A點選設定密碼按鈕進入活動B: A->B
    • 在活動B中設定密碼後點選完成後進入活動C: A->B->C
    • 在活動C中點選確認,返回活動A,並且攜帶已經確認的資訊: A->B->C->A
    • 活動A在onNewIntent方法中獲取資訊,將設定密碼字樣改為修改密碼。
  • TaskAffinity屬性 : 這個屬性代表活動的親和性,即一個活動啟動時想要指定的任務棧名字,在預設情況下,所有活動所需的任務棧名字為所應用的包名。

參考文章

相關文章