[譯] android應用開發者,你們真的瞭解Activity的生命週期嗎?

ronaldong發表於2019-03-01

你沒看錯,現在是2018年,我正在寫一篇連我自己都感到很驚訝的關於Android系統的Activity生命週期的文章。

一開始,我覺得Activity的生命週期雖然過於複雜,但它不應該是一個難題。我的意思是:對於Android開發新手來說,如何正確地處理Activity生命週期可能有點困難,但是我無法想象對於那些富有經驗的android開發者來說,這依然是一個棘手的問題。

我還是想的太簡單了。

一會兒我會告訴你整個故事,但是先讓我簡述下我寫這篇文章的目的。

我想要與你們分享我是如何處理Activity的生命週期的,它比官方文件裡描述的更簡單,而且還涵蓋絕大多數棘手的極端情形。

Activity的生命週期仍然是一個難題:

一個星期內發生的兩件事情讓我意識到:即使在今天,Activity的生命週期仍然是Android開發人員面臨的一個難題。

幾周前一位Redditor在“androiddev”裡分享了一篇關於Activity的生命週期的文章。這篇文章的寫作基礎來源於我在StackOverflow社群裡分享的一個答案。當時提問者推薦了一種處理Activity的生命週期的方法,我覺得並不準確,於是我立即提交了一條評論,希望其他讀者能意識到這一點。

然後陸續有幾位redditor回答並質疑了我的觀點,進而演變成了一場漫長而非常有見地的討論。在討論過程中,我觀察到了一些有趣的關於Activity的生命週期的現象:

  1. Activity的生命週期讓許多有經驗的android開發人員感到困惑。
  2. 官方文件裡仍然存在著一些關於Activity的生命週期的過時資訊和矛盾資訊。
  3. 即使是編寫Google官方教程的開發人員們也沒有真正地理解Activity的生命週期以及它的影響。

幾天之後,我正在訪問一個潛在客戶。該公司僱傭了一批富有經驗的Android開發人員,他們嘗試在老的程式碼庫裡新增新功能,這不是一件容易的事。

在快速程式碼審查期間,我注意到其中一位維護人員決定在Activity的onCreate(Bundle)中訂閱EventBus,並在onDestroy()中取消訂閱。這樣做會帶來很大的麻煩,所以我建議他們重構這部分程式碼。

My take on Activity life-cycle::

上述兩件事讓我意識到還是有許多android開發人員對Activity的生命週期充滿困惑。現在,我想嘗試通過分享我的經驗來給其他開發人員提供便利。

我不打算在這裡為Activity的生命週期重新編寫一份文件,這代價太大了。我會告訴你如何在Activity生命週期的各個方法之間劃分邏輯以實現最簡單的設計並避免最常見的陷阱。

onCreate(Bundle):

Android framework並沒有提供Activity的建構函式,它會自動建立Activity類的例項。那麼我們應該在哪裡初始化Activity呢?

你可以把所有應該在Activity的建構函式裡處理的邏輯放在onCreate(Bundle)這個方法裡。

理想情況下,建構函式只會初始化物件的成員變數:

public ObjectWithIdealConstructor(FirstDependency firstDependency, 
                                  SecondDependency secondDependency) {
    mFirstDependency = firstDependency;
    mSecondDependency = secondDependency;
}
複製程式碼

除了初始化成員變數外, onCreate(Bundle)方法還承擔著另外兩個職責:恢復之前儲存的狀態並呼叫setContentView()方法。

所以,onCreate(Bundle)方法中的基本功能結構應該是:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 
    getInjector().inject(this); // inject dependencies
 
    setContentView(R.layout.some_layout);
     
    mSomeView = findViewById(R.id.some_view);
 
    if (savedInstanceState != null) {
        mWelcomeDialogHasAlreadyBeenShown = savedInstanceState.getBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN);
    }
 
}

複製程式碼

顯然,這並不適用於所有人,你可能會有一些不同的結構。不過沒關係,只要你在onCreate(Bundle)中沒有以下內容:

  • Subscription to observables
  • Functional flows
  • Initialization of asynchronous functional flows
  • Resources allocations

每當我需要決定某一段邏輯是否應該放在onCreate(Bundle)方法裡時,我就會問自己這個問題:這段邏輯與Activity的初始化有關嗎?如果答案是否定的,我就會另尋別處。

當Android framework建立了一個Activity例項,而且它的onCreate(Bundle) 方法執行完畢之後,這個Activity就會處於已建立狀態。

處於已建立狀態的Activity 並不會觸發資源分配,也不會接收到系統裡其他物件發出的事件。從這種意義上來講,“created”狀態是一種已經準備好了,但是不活躍和隔離的狀態。

onStart():

為了使Activity能夠與使用者互動,Android framework接著會呼叫Activity的onStart()方法使其處於活躍狀態。onStart() 方法中的一些基本的邏輯處理包括:

  1. Registration of View click listeners
  2. Subscription to observables (general observables, not necessarily Rx)
  3. Reflect the current state into UI (UI update)
  4. Functional flows
  5. Initialization of asynchronous functional flows
  6. Resources allocations

對於第1點和第2點,你可能會感到很驚訝。因為在大多數官方文件和教程裡,它們都會被放在onCreate(Bundle)裡。我認為這是對複雜的Activity的生命週期的一種誤解,我會在接下來的onStop()方法裡講到這一點。

所以,onStart()方法中的基本功能結構應該是:

@Override
public void onStart() {
    super.onStart();
 
    mSomeView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            handleOnSomeViewClick();
        }
    });
 
    mFirstDependency.registerListener(this);
 
    switch (mSecondDependency.getState()) {
        case SecondDependency.State.STATE_1:
            updateUiAccordingToState1();
            break;
        case SecondDependency.State.STATE_2:
            updateUiAccordingToState2();
            break;
        case SecondDependency.State.STATE_3:
            updateUiAccordingToState3();
            break;
    }
 
    if (mWelcomeDialogHasAlreadyBeenShown) {
        mFirstDependency.intiateAsyncFunctionalFlowAndNotify();
    } else {
        showWelcomeDialog();
        mWelcomeDialogHasAlreadyBeenShown = true;
    }
}

複製程式碼

需要強調的一點是,你實際在onStart()中所執行的操作是由每個特定Activity的詳細需求決定的。

在Android framework呼叫完onStart()方法後,Activity將從已建立狀態轉換為已啟動狀態。在此狀態下,它可以正常工作,並且可以與系統的其他元件協作。

onResume():

關於onResume()方法的第一條準則是你不需要覆蓋onResume()方法。第二條準則也是你不需要覆蓋onResume()方法。第三條準則是在某些特殊情況下你才會確實需要覆蓋onResume()方法。

我搜尋了我的一個專案的程式碼庫,發現有32個onStart()方法被覆蓋重寫,平均每段程式碼約5行,一共有大約150多行程式碼。相反,只有2個onResume()方法被覆蓋重寫,一共8行程式碼,這8行程式碼主要是用於恢復播放動畫和視訊。

這總結了我對onResume()的看法:它只能用於在螢幕上啟動或恢復一些動態的物件。你想在onResume()而不是onStart()中做這件事的原因將在稍後的onPause()部分討論。

在Android framework呼叫完onResume()方法後,Activity將從已啟動狀態轉換為”resumed“狀態。在此狀態下,使用者可以與它進行互動。

onPause():

在這個方法裡,您應該暫停或停止在onResume()中恢復或啟動的動畫和視訊。就像onResume()一樣,你幾乎不需要重寫onPause()。在onPause()方法而不是onStop()方法中處理動畫的原因是因為當Activity被部分隱藏(比如,彈出系統對話方塊)或者是在多視窗模式下失去焦點的時候,只有onPause()方法會被呼叫。因此如果你想在 只有使用者正在與Activity互動的情況下播放動畫,同時避免在多視窗模式下分散使用者注意力並延長電池壽命,onPause() 是你唯一可以信賴的選擇。
這一結論的前提是你希望你的動畫或視訊在使用者進入多視窗模式時停止播放,如果你希望你的動畫或視訊在使用者進入多視窗模式時繼續播放,那麼你不應該在onPause()中暫停它。在這種情況下,你需要將onResume()/ onPause()中的邏輯移動到onStart()/ onStop()。

一種特殊的情況是相機的使用,由於相機是所有應用程式共享的單一資源,因此通常您會想要在onPause()方法中釋放它。

在Android framework呼叫完onPause()方法後,Activity將從”resumed“狀態轉換為已啟動狀態。

onStop():

在這個方法裡,您將登出所有觀察者和監聽者,並釋放onStart()中分配的所有資源。

所以,onStop()方法裡的基本功能結構應該是:

@Override
public void onStop() {
    super.onStop();
 
    mSomeView.setOnClickListener(null);
 
    mFirstDependency.unregisterListener(this);
}

複製程式碼

讓我們來討論一個問題:為什麼你需要在這個方法裡取消註冊?

首先,如果此Activity不被mFirstDependency取消註冊的話,您可能會洩漏它。這不是我願意承擔的風險。

所以,問題變成了為什麼要在onStop()方法裡取消註冊,而不是onPause()方法或者是onDestroy()方法?

想要快速清晰地解釋這些確實有點棘手。

如果在onPause()方法裡呼叫mFirstDependency.unregisterListener(this),那麼Activity將不會收到相關非同步流程完成的通知。因此,它不能讓使用者感知到這一事件,從而完全違背了多視窗模式的設計初衷,這不是一種好的處理方式。

如果在onDestroy()方法裡呼叫mFirstDependency.unregisterListener(this),這同樣不是一種好的處理方式。

當應用被使用者推到後臺(例如,點選“home”按鈕)時,Activity的onStop()將被呼叫,從而使得其返回到“已建立”狀態,這個Activity可以在幾天甚至幾周的時間內保持這個狀態。

如果這時候mFirstDependency產生了連續的事件流,那麼在這幾天甚至幾周的時間裡,Activity可以都處理這些事件,即使使用者在這段時間內從未真正與它互動過。這將是對使用者電池壽命的不負責任的浪費,而且在這種情況下,應用消耗的記憶體會逐漸增多,應用程式被OOM(Out Of Memory)Killer殺死的可能性也會增大。

因此,在onPause()和onDestroy()裡呼叫mFirstDependency.unregisterListener(this)都不是一種好的處理方式,您應該在onStop()中執行此操作。

關於登出View的事件監聽器,我想多說幾句。

由於Android UI框架的不合理設計,像這個問題中所描述的奇怪場景是可能會發生的。有幾種方法可以解決這個問題,但是從onStop()中登出View的事件監聽器是最乾淨的一種。

或者,您可以完全忽略這種罕見的情況。這是大多數Android開發人員所做的,但是,您的使用者偶爾會遇到一些奇怪的行為和崩潰。

在Android framework呼叫完onStop()方法後,Activity將從已啟動狀態轉換為已建立狀態。

onDestroy():

永遠都不要覆蓋重寫這個方法。

我搜尋了我所有的專案程式碼庫,沒有一個地方需要我重寫onDestroy()方法。

如果你思考一下我前面對onCreate(Bundle)的職責的描述,你會發現這是完全合理的,它僅僅執行了初始化Activity物件的操作,所以完全沒有必要手動完成清理工作。

當我在檢視新的Android程式碼庫時,我通常會搜尋幾個常見的錯誤模式,這使我能夠快速地瞭解程式碼的質量。 onDestroy()的覆蓋重寫是我尋找的第一個錯誤模式。

onSaveInstanceState(Bundle):

此方法用於儲存一些臨時的狀態,在這個方法裡你位移需要做的就是將你想儲存的狀態存入Bundle資料結構中:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN, mWelcomeDialogHasAlreadyBeenShown);
}

複製程式碼

在這裡,我想提一個與Fragment有關的常見陷阱。

如果你在這個方法執行完之後,提交Fragment事務,那麼應用程式會丟擲IllegalStateException異常而導致崩潰。

你需要知道的一點是onSaveInstanceState(Bundle)將在onStop()之前被呼叫。

好訊息是,經過多年的努力,谷歌終於意識到了這個bug。 在Android P中,onStop()將在onSaveInstanceState(Bundle)之前被呼叫。

總結:

是時候結束這篇文章了。

雖然本文的內容既不簡短也不簡單,但我認為它比大多數文件(包括官方教程)更簡單,更完整。

我知道一些經驗豐富的專業開發人員會質疑我處理Activity生命週期的方式,沒關係。事實上,我非常期待您的質疑。

不過,請記住,我已經使用這個方案好幾年了。根據我的經驗,使用這種方法編寫的程式碼比許多專案中看到的對於Activity生命週期的處理邏輯要清晰簡潔的多。

這種方法的最終驗證來自Google本身。

從Nougat開始,Android系統支援多屏任務。我在這篇文章中與你分享的方法幾乎不需要任何調整。這基本上證實,相對於官方文件,它包含了對Activity生命週期更深入的見解。

另外,Android P中對於Activity的onStop()和onSaveInstanceState(Bundle)方法之間的呼叫順序的調整將使得這種方法對於Fragments的使用來說是最安全的。

我不認為這是巧合。

相關文章