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

若是你問我Android系統框架裏哪三個問題最複雜,那麼Fragment的生命週期確定會成爲其中的一個。android

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.bash

「將單一的Activity拆分紅多個獨立的部件」的想法很是好。 然而,從今天Fragment的的實際使用效果來看,這一API的實現和演變並不理想。app

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

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

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

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

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的複雜性對我來講是可管理的。

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

相關文章
相關標籤/搜索