如果你問我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生命週期的複雜性。這很恐怖。
幸運的是,您在應用中使用Fragment的時候無需瞭解整個生命週期的轉換過程。
在這篇文章中,我將介紹一些處理Fragment生命週期的方法,它隱藏了大部分的複雜細節。 我使用這種方法已經有好幾年了,它確實有效。
Activity的生命週期
正如你即將看到的,當我在使用Fragments的時候,我會盡可能地將它們從Activity的生命週期中解耦出來。但是,這並不能改變它們之間有許多相似之處的事實。
我已經寫了一篇關於我是如何處理Activity的生命週期的文章:android應用開發者,你們真的瞭解Activity的生命週期嗎?。 該文章收到了非常積極的反饋,並在不到一個月的時間內成為了我部落格上最受歡迎的文章之一。
正如我所說的,Activity和Fragment的生命週期在很多方面都是相似的。 所以,在這篇文章中,我會在他們之間進行很多類比。但是,我不想做重複的工作。 因此,我假設你已經閱讀過有關Activity生命週期的文章。
My Take on Fragment Lifecycle
我處理Fragment生命週期的方法旨在實現兩個目標:
- 使Fragment的生命週期處理邏輯儘可能類似於Activity的處理邏輯。
- 使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()之前的任何時候呼叫。 這裡有兩個主要問題:
-
正如Dianne說的那樣,與Fragment相關聯的View hierarchy可以被銷燬而不實際儲存其狀態。 因此,如果您想在View onCreateView(LayoutInflater,ViewGroup,Bundle)中恢復Fragment的狀態,那麼您將冒著覆蓋最新狀態的風險。這是一個非常嚴重的錯誤, 這正是我告訴你只能在onCreate(Bundle)中恢復狀態的原因。
-
如果在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的複雜性對我來說是可管理的。
我相信自己在寫這篇文章時錯過了一些重要的觀點。 因此,如果您有任何補充或者想要指出本文中的任何缺陷,請隨時在下面的評論中提出來。