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

ronaldong發表於2018-05-29

如果你問我Android系統框架裡哪三個問題最複雜,那麼Fragment的生命週期肯定會成為其中的一個。

Android Framwork開發人員中的傳奇人物Dianne Hackborn在2010年將Fragment引入了Android,他在提交資訊中寫道:

Author: Dianne Hackborn
Date: Thu Apr 15 14:45:25 2010 -0700
Introducing Fragment.
Basic implementation of an API for organizing a single activity into separate, discrete pieces. Currently supports adding and removing fragments, and performing basic lifecycle callbacks on them.

“將單一的Activity拆分成多個獨立的部件”的想法非常好。 然而,從今天Fragment的的實際使用效果來看,這一API的實現和演變並不理想。

截至目前,Fragment是Android系統框架裡最具有爭議的Android API之一。 許多專業的Android開發人員甚至都不會在他們的專案中使用Fragments。 不是因為他們不瞭解Fragment的好處,而是因為他們清楚地看到了Fragment的缺點和侷限性。

如果你認真的看過下面這個流程圖的話,你就知道我並沒有誇大Fragment生命週期的複雜性。這很恐怖。

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

幸運的是,您在應用中使用Fragment的時候無需瞭解整個生命週期的轉換過程。

在這篇文章中,我將介紹一些處理Fragment生命週期的方法,它隱藏了大部分的複雜細節。 我使用這種方法已經有好幾年了,它確實有效。

Activity的生命週期

正如你即將看到的,當我在使用Fragments的時候,我會盡可能地將它們從Activity的生命週期中解耦出來。但是,這並不能改變它們之間有許多相似之處的事實。

我已經寫了一篇關於我是如何處理Activity的生命週期的文章:android應用開發者,你們真的瞭解Activity的生命週期嗎?。 該文章收到了非常積極的反饋,並在不到一個月的時間內成為了我部落格上最受歡迎的文章之一。

正如我所說的,Activity和Fragment的生命週期在很多方面都是相似的。 所以,在這篇文章中,我會在他們之間進行很多類比。但是,我不想做重複的工作。 因此,我假設你已經閱讀過有關Activity生命週期的文章。

My Take on Fragment Lifecycle

我處理Fragment生命週期的方法旨在實現兩個目標:

  1. 使Fragment的生命週期處理邏輯儘可能類似於Activity的處理邏輯。
  2. 使Fragment的生命週期處理邏輯獨立於Activity的生命週期。

使用類似於處理Activity的生命週期的方法來處理Fragment的生命週期,能大幅降低應用程式的整體複雜性。 開發人員只需要學習一種方法,而不是兩種不同的方法。 這意味著開發過程中的工作量會減少,維護會更容易,新團隊成員熟悉專案的速度會更快。 我也完全確定這樣做可以降低產生bug的風險,儘管這只是我個人的一種主觀想法。

通過實現上述兩個目標,我大大降低了與Fragment相關的問題的複雜性,從而使它更具吸引力。

onCreate(Bundle)

請記住,您無權訪問Activity的建構函式,因此您無法通過它來將依賴注入到Activity中。好訊息是:Fragment有一個公開的建構函式,我們甚至可以定義更多的建構函式。 壞訊息是:這樣做會導致嚴重的bug,所以我們不能這樣做。

當Activity被強制銷燬,之後又被自動恢復的時候,Android系統會在這一過程中銷燬並重新建立Fragment。 重新建立的機制是通過使用反射的方法來呼叫Fragment的無參建構函式來實現的。 因此,如果您是使用帶引數的建構函式來例項化Fragment,並在其中將依賴的物件傳遞給它,那麼在儲存和恢復後,所有這些依賴的物件都將被設定為null。

因此,就像Activity一樣,您需要使用onCreate(Bundle)方法作為建構函式的替代者。 Fragment中依賴物件的注入和初始化就發生在這裡。

但是,與Activity的onCreate(Bundle)方法不同的是, 您不得在Fragment的onCreate(Bundle)方法中執行任何與Android View相關的操作。 這個非常重要,其原因將在下一節中詳細闡述。

總而言之,Fragment的onCreate(Bundle)方法的基本的處理邏輯如下所示:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 
    getInjector().inject(this); // inject dependencies
 
    if (savedInstanceState != null) {
        mWelcomeDialogHasAlreadyBeenShown = savedInstanceState.getBoolean(SAVED_STATE_WELCOME_DIALOG_SHOWN);
    }
     
    mActivityAsListener = (ActivityAsListener) requireActivity();
 
}
複製程式碼

還有一點忘了說 - 將Activity轉換為listener也發生在onCreate(Bundle)中。 如果你喜歡這種方式的話,這將比在onAttach(Context)中丟擲一個通用的ClassCastException要有意義的多。

View onCreateView(LayoutInflater, ViewGroup, Bundle)

這個方法是Fragment獨有的,這是它與Activity生命週期最顯著的區別。

但它也是Fragment相關問題的“萬惡之源”。 稍後我會討論它的“邪惡”,但請記住,如果您使用Fragment,最好不要低估View onCreateView(LayoutInflater,ViewGroup,Bundle)的複雜性。

那麼,故事是什麼呢?

Activity在生命週期的轉換過程中都只有同一個View hierarchy。 你在Activity的onCreate(Bundle)中初始化這個View hierarchy,然後它就會一直存在於Activity的整個生命週期,直到Activity被垃圾收集器回收為止。 您可以手動更改Activity的View hierarchy的組成,Android系統是不會為您做任何事情的。

然而,Fragment在其生命週期中可以存在有多個View hierarchy,由Android系統決定何時進行替換。

換句話說,你可以在程式執行的時候動態改變Fragment的View hierarchy,現在你應該清楚為什麼不能在Fragment的onCreate(Bundle)中操作View了吧。 onCreate(Bundle)方法在Fragment被Attach到Activity後僅被呼叫一次,它無法支援Fragment的View hierarchy的動態化。

每次需要建立新的View hierarchy的時候,Android系統都會呼叫onCreateView(LayoutInflater, ViewGroup, Bundle)方法。 您的工作是建立View hierarchy並將其初始化為正確的狀態,然後將它作為該方法的返回值,之後它就會被Android系統接管。

重寫這個方法的主要原則是:Fragment中所有持有與View hierarchy相關的物件的引用的成員變數,必須在View onCreateView(LayoutInflater,ViewGroup,Bundle)中進行初始化。 換句話說,如果Fragment的成員變數持有View或者相關物件的引用,請確保在此方法中初始化這些成員變數,這非常重要。

總而言之,Fragment的View onCreateView(LayoutInflater, ViewGroup, Bundle)方法的基本的處理邏輯如下所示:

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View root = inflater.inflate(R.layout.some_layout, container, false);
    mSomeView = rootView.findViewById(R.id.some_view); // must assign all Fragment's fields which are Views
    mLstSomeList = rootView.findViewById(R.id.lst_some_list); // must assign all Fragment's fields which are Views
    mAdapter = new SomeAdapter(getContext()); // must assign all Fragment's fields related to View hierarchy
    mLstSomeList.setAdapter(mAdapter);
    return rootView;
}
複製程式碼

這個方法的另一個有趣的地方是它接收了一個引數:儲存狀態的Bundle。 老實說,我覺得這樣很麻煩。Android系統框架開發的人員似乎自己也不確定應該在哪裡恢復這些狀態,所以他們將這個Bundle引數注入了到這個方法裡,讓我們自己去弄清楚。

不要在這個方法裡恢復狀態。在後面介紹onSaveInstanceState(Bundle)方法時我會解釋不要這樣做的原因。

使用者Boza_s6在Reddit上提交了他(她)對這篇文章的反饋,我們進行了一次非常有趣的討論。 問題的焦點在於當在Fragment裡使用列表和介面卡的時候,我的方法是否會導致記憶體洩漏。根據上面的討論,我想這個問題的答案是清楚的。

如果您遵循我在本文中分享的原則,那麼就不存在記憶體洩漏的風險。事實上,我使用這種方法的部分原因就是為了減輕Fragment記憶體洩漏的內在風險。

我的原則是Fragment裡每個與View hierarchy相關的成員變數都必須在此方法中初始化, 這包括列表介面卡,使用者互動事件的監聽器等。保持Fragment裡的程式碼可維護性的唯一方法是確保此方法在重新建立整個View hierarchy的時候會重新初始化Fragment裡與之相關的成員變數。

onStart()

Fragment的這個方法與Activity的onStart()方法具有完全相同的職責和指導原則。 你可以閱讀我之前關於Activity的生命週期的文章,android應用開發者,你們真的瞭解Activity的生命週期嗎?

總而言之,Fragment的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;
    }
}

複製程式碼

如您所見,這個方法裡包含了Fragment的大部分功能邏輯。 保持onStart()方法在Activity和Fragment裡的一致性具有很多好處。

onResume()

這個方法的處理邏輯與Activity的onResume()方法相同。

onPause()

這個方法的處理邏輯與Activity的onPause()方法相同。

onStop()

這個方法的處理邏輯同樣與Activity的onStop()方法相同。其基本的處理邏輯如下所示:

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

複製程式碼

這裡有一行有趣而又令人驚訝的程式碼:mSomeView.setOnClickListener(null)。 我之前已經解釋過了為什麼你可能要在Activity的onStop()登出點選事件的監聽器,所以這裡我不會再重複。

不過,我想借此機會回答另一個問題:登出點選事件的監聽器是不是必需的?

據我所知,絕大多數的Android應用程式都不會這樣做,並且仍然執行良好。 所以,我認為這不是強制性的。但是如果你不這樣做,你的app可能會遇到一定數量的bug和崩潰。

onDestroyView()

絕大多數情況下,您都不應該重寫此方法。 我想有些讀者會對此感到驚訝,但我真的是這麼想的。

正如我前面所說的,你必須在onCreateView(LayoutInflater,ViewGroup,Bundle)裡初始化Fragment裡所有持有View或者相關物件的引用的成員變數。這個要求源自於這樣一個事實:Fragment的View hierarchy可以被重新建立,所以所有未在該方法中被初始化的持有View的引用物件都將被置為null。這樣做非常重要,否則你的app可能會遇到一些非常令人討厭的bug和崩潰。

如果你這樣做了,Fragment將一直持有對這些View的強引用,直到下一次呼叫View onCreateView(LayoutInflater,ViewGroup,Bundle)方法或者整個Fragment被銷燬。

現在有一種廣泛的建議是,您應該在onDestroyView()方法裡將所有前面提到的成員變數設定為Null。 目的是儘快釋放這些引用以允許垃圾收集器在onDestroyView()返回後立即回收它們,這樣就能更快地釋放與這些View相關的記憶體空間。

上面的解釋雖然聽起來很合理,但這是過早優化的經典案例。 在絕大多數情況下,你並不需要這種優化。因此,你沒有必要去重寫onDestroyView()的方法,這隻會使得本來已經很複雜的處理Fragment生命週期的邏輯更復雜。

所以,你不需要重寫onDestroyView()方法。

onDestroy()

像Activity一樣,您不需要在Fragment中重寫此方法。

onSaveInstanceState(Bundle)

這個方法的基本的處理邏輯如下所示:

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

複製程式碼

我希望你不要被這個方法的表面的簡單性所誤導。 儲存和恢復流程的錯誤處理是Android應用程式中bug和崩潰的主要原因之一。 在之前關於Activity生命週期的文章中我花了很大的篇幅來討論這個方法,這不是巧合。

因此,在Activity中處理儲存和恢復狀態的過程很麻煩。你可能會認為Fragment不會比這更糟糕。然而,令人驚訝的是,Fragment更糟糕。

2011年2月Dianne Hackborn在Javadoc裡介紹了這個方法,其中包含一些令人可怕的描述:

這個方法對應於Activity的onSaveInstanceState(Bundle)方法,所以對Activity的onSaveInstanceState(Bundle)的大多數討論也適用於此。但請注意:此方法可能在onDestroy()之前的任何時候被呼叫。在許多情況下,Fragment可能會被銷燬(例如,當Fragment被放置在回退棧上而沒有UI顯示時),但是它的狀態不會被儲存,除非其所屬的Activity確實需要儲存其狀態。

如果這個“註釋”不能令你感到驚訝的話,那麼說明你還沒有深入的理解Activity和Fragment生命週期。

根據官方檔案:這個方法可能在onDestroy()之前的任何時候呼叫。 這裡有兩個主要問題:

  1. 正如Dianne說的那樣,與Fragment相關聯的View hierarchy可以被銷燬而不實際儲存其狀態。 因此,如果您想在View onCreateView(LayoutInflater,ViewGroup,Bundle)中恢復Fragment的狀態,那麼您將冒著覆蓋最新狀態的風險。這是一個非常嚴重的錯誤, 這正是我告訴你只能在onCreate(Bundle)中恢復狀態的原因。

  2. 如果在onDestroy()之前的任何時候都可以呼叫onSaveInstanceState(Bundle),那麼您無法保證何時才能安全地更改Fragment的狀態(例如替換巢狀的Fragments)。

我不認為Dianne Hackborn對這個方法的描述是準確的。事實上,我認為Dianne Hackborn在2011年寫這篇文章的時候就已經犯了一個錯誤。“任何時候”的概念意味著某種不確定性或隨機性,我認為它從來就不存在。最有可能的是,只有幾個因素影響了這種行為,Dianne決定不列出它們,因為她認為它們不夠重要。

如果真是這樣的話,那麼她顯然是錯的。

如果這個描述在當時是正確的,那麼就說明設計這個框架的Google開發人員並不知道這樣的行為會導致上面列出的兩個問題。特別是,它意味著包含已儲存狀態的Bundle從未被傳入View onCreateView(LayoutInflater,ViewGroup,Bundle)方法。

setRetainInstance(boolean)

切勿使用retained的Fragment。你不需要它們。

如果你這樣做了的話,請記住它改變了Fragment的生命週期。 那麼本文中所描述的一切內容都是無效的。

為什麼Fragment如此複雜?

正如你所看到的,Fragment確實很複雜,我甚至會說非常複雜。

Fragment最大的問題是它的View hierarchy可以獨立於Fragment物件本身被銷燬和重新建立,如果不是這樣的話,Fragment的生命週期幾乎與Activity的生命週期一模一樣。

造成這種複雜性的原因是什麼?顯然我不知道答案,我只能根據我的理解力去進行推測。

我認為引入這種機制是為了優化記憶體的消耗。當Fragment不可見的時候,銷燬Fragment的View hierarchy允許釋放一些記憶體。 但另一個問題是:Fragment實現了對狀態儲存和恢復流程的支援,Google為什麼要這樣做?

我不知道。

然而,我所知道的是,FragmentStatePagerAdapter並不是採用這種機制去儲存和恢復Fragments的狀態。

就我而言,這整個機制是一種過早的優化。它沒有任何用處,反而會使得Fragment使用更復雜。

比較諷刺的一點在於,Google開發人員似乎自己都不瞭解Fragment的生命週期。

Google釋出了LiveData Architecture Component,但是存在一個嚴重漏洞。 如果有一位開發人員仔細的研究過Fragment,那麼他們就會真正理解Fragment的生命週期,在設計階段他們就會發現這個問題。

Google花了幾個月的時間來修復這個bug。最後,在Google IO 18期間,他們宣佈bug已修復。 解決方法是為Fragment的View hierarchy引入了另一種生命週期。

因此,如果您使用的是LiveData元件,現在您需要記住Fragment物件有兩個不同的生命週期。這讓我非常難過。

結束語

好的,讓我們結束這篇文章。

Fragment真是一團糟。唯一比Fragment更糟糕的是他們的官方文件。 Google開發人員並沒有完全理解Fragment的生命週期,並且會繼續增加它的複雜性。

說了這麼多,其實我一直在使用Fragment,並在將來繼續使用它們。 與“一個介面一個Activity”的方法相比,Fragment可以提供更好的使用者體驗。

為了在使用Fragment的同時並保持我的理智,我正在使用本文所描述的處理Fragment生命週期的方法。它可能不包含所有的情形,但在過去的幾年裡它對我很有幫助。

這種方法有兩個好處:它使得處理Fragment的生命週期非常類似於Activity的生命週期並且獨立於它。您可能已經注意到,我在這篇文章中並沒有提到Activity的狀態。 只要我堅持使用這種方法,我並不在乎Activity會發生什麼,我甚至不用關心這個Fragment是否是巢狀的。

因此,我重寫了Fragment裡最少數量的方法,而且互相之間沒有依賴關係。 這使得Fragment的複雜性對我來說是可管理的。

我相信自己在寫這篇文章時錯過了一些重要的觀點。 因此,如果您有任何補充或者想要指出本文中的任何缺陷,請隨時在下面的評論中提出來。

相關文章