死磕 Fragment 的生命週期

死磕 Fragment 的生命週期

本文原創,轉載請註明出處。
歡迎關注個人 簡書 ,關注個人專題 Android Class 我會長期堅持爲你們收錄簡書上高質量的 Android 相關博文。
本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈html

本文例子中 github 地址:
github BuzzerBeater 項目連接
(第一個開源項目,目前在逐步更新一些知識點,但願對你有所幫助)git

曾經在北京擁擠的13號線地鐵上,一名揹着雙肩包穿着格子衫帶着鴨舌帽腳踏帆布鞋的程序員講了一句:
「我以爲 Fragment 真的太難用了」。從而引發一陣躁動激烈的討論。程序員

正方觀點:github

Fragment 真的太好用了。要知道由於 Activity 的啓動,涉及到 Android 系統對 ActivityManager 的調度,會關聯許多資源和進行諸多複雜運算,在一些高端手機上,這個調度的時長甚至會超過 100 ms。反觀 Fragment,啓動如巧克力入口般順滑,輕量不消耗手機資源。還能夠作成一個個模塊,方便 Activity 複用。而且若是要涉及平板的適配,Fragment 更是必不可少。緩存

反方觀點:微信

Fragment 難用,屬於坑多難填。Fragment 本質上是一個有生命週期的 View,生命週期繁多而且異常難管理,多個 Fragment 嵌套更是坑中之坑(我也遇到過...),連 square 和 FaceBook 都摒棄了 Fragment,更況且咱們呢!網絡

好吧,不要吵了,用或者不用,遇到問題如何解決,相信你們內心都有一個本身的答案。結合我本身開發時候遇到的問題,下面咱們來總結一下 Fragment 的生命週期管理方式,以及一些技巧和建議。架構

hide & show

先結合一張項目截圖,來直觀地看看目前我是如何管理 Fragment 的。app


項目截圖

由於咱們的項目架構是一 Activity 多 Fragment,而且把全部的 Fragment 都裝載到了一個 ViewPager 裏面,啓動一個新的 Fragment 的過程也是 ViewPager 滑動翻頁的過程,將來會考慮把這種管理方式總結成文,整理給你們。總之你目前看到的上圖界面,都是 Fragment 呈現的,而且點擊按鈕什麼的,也會跳轉到下一個 Fragment,這就涉及到了 Fragment 嵌套。ide

我也寫了一個 Demo,去模擬了這個頁面的搭建。

這裏想多說幾句

經過點擊下方 Tab 管理頁面的方式目前很是主流,這種佈局方式事實上是從 iOS 上面借鑑過來的。(反正如今兩大系統都是相互學習)在前一陣 google 的 support 25 包也終於推出了官方的 BottomNavigationView ,不過個人 Android Studio 安裝 support 25 老是失敗,因此在項目中我選用了一個還不錯的開源庫來作下方的底部導航。

BottomNavigationView 自己有一套 Material Design 的設計規範以下:

Material Design Navigation Bottom

感興趣的去閱讀一下,之後對產品、設計開撕是頗有幫助的。其中有這麼一條頗有意思,是說 BottomNavigationView 並不建議把它設計成橫向滑動的形式,也就是用 ViewPager 去裝載 Fragment。爲何說這句頗有意思呢?事實上市面上不少主流的 app,它們的 BottomNavigationView 確實是不能夠橫向滑動的,而咱們每一個人都在用的,月活8億的國民軟件微信,就偏偏把它的主頁面作成了能夠橫向滑動的。

這裏我想說下個人我的見解,首先規範未必須要嚴格遵照,作什麼樣的功能實現什麼樣的效果,要結合本身項目的架構和產品作一個合理的需求。拿 360手機助手 這個 app 舉例,它底部的每個 tab 都搭建了一個很是「重量級」的模塊,而且每一個 tab 下界面的內部還有許多負責的 View 層級和嵌套滑動的 ViewPager,因此試想一下,這樣的頁面要是作成微信那個樣子,不卡頓就怪了~反觀微信,首先我認爲它的每一個界面層級和交互都不復雜,邏輯也都在頁面內,因此作成橫向滑動的反而能提高用戶的體驗。

好了,前面說了好多可有可無的話,趕忙來看看 demo 中經過 hide 和 show 的方式如何來管理 Fragment。
MainActivity

private SearchFragment searchFragment; private MusicFragment musicFragment; private CarFragment carFragment; private SettingFragment settingFragment; private BaseFragment currentFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); setSupportActionBar(toolbar); initView(); initData(); initListener(); } private void initData() { if (searchFragment == null) { searchFragment = SearchFragment.newInstance(getString(R.string.tab_1)); } if (!searchFragment.isAdded()) { // 提交事務 getSupportFragmentManager().beginTransaction().add(R.id.fl_content, searchFragment).commit(); // 記錄當前Fragment currentFragment = searchFragment; } } private void initListener() { bottomNavigation.setOnTabSelectedListener(new AHBottomNavigation.OnTabSelectedListener() { @Override public boolean onTabSelected(int position, boolean wasSelected) { Log.d(TAG, "onTabSelected: position:" + position + ",wasSelected:" + wasSelected); if (position == 0) {// 導航 clickSearchLayout(); } else if (position == 1) {// 音樂 clickMusicLayout(); } else if (position == 2) {// 車輛 clickCarLayout(); } else if (position == 3) { clickSettingLayout(); } else if (position == 4) { clickToysLayout(); } return true; } }); } private void clickSearchLayout() { if (searchFragment == null) { searchFragment = SearchFragment.newInstance(getString(R.string.tab_1)); } addOrShowFragment(getSupportFragmentManager().beginTransaction(), searchFragment); } private void clickMusicLayout() { if (musicFragment == null) { musicFragment = MusicFragment.newInstance(getString(R.string.tab_2)); } addOrShowFragment(getSupportFragmentManager().beginTransaction(), musicFragment); } private void clickCarLayout() { if (carFragment == null) { carFragment = CarFragment.newInstance(getString(R.string.tab_3)); } addOrShowFragment(getSupportFragmentManager().beginTransaction(), carFragment); } private void clickSettingLayout() { if (settingFragment == null) { settingFragment = SettingFragment.newInstance(getString(R.string.tab_4)); } addOrShowFragment(getSupportFragmentManager().beginTransaction(), settingFragment); } /** * 添加或者顯示 fragment * * @param transaction * @param fragment */ private void addOrShowFragment(FragmentTransaction transaction, Fragment fragment) { if (currentFragment == fragment) return; if (!fragment.isAdded()) { // 若是當前fragment未被添加,則添加到Fragment管理器中 transaction.hide(currentFragment).add(R.id.fl_content, fragment).commit(); } else { transaction.hide(currentFragment).show(fragment).commit(); } currentFragment = (BaseFragment) fragment; }

代碼理解起來很是容易,我經過 add & show / hide 的方式來管理底部的四個 tab 相互切換,而且打印了這四個 Fragment 的全部生命週期方法,包括onHiddenChangedsetUserVisibleHint

當第一次進入某個頁面時:


首次進入某個頁面時

能夠看到,當我依次點擊下方四個 tab,界面第一次加載的時,會走 Fragment 的建立週期 onAttach -- onResume,也許你會問,上面我執行相互切換操做,從第一個頁面切換到第二個的時候,爲何第一個 Fragment 頁面不可見了,不會調用 onPause 和 onStop 呢?

這是在你瞭解過 Activity 生命週期而且剛接觸 Fragment 的生命週期時,第一個容易陷入的誤區,事實上 Fragment 的生命週期,除了第一次它建立或銷燬以外,通通都是由 Activity 所驅動的。 舉個例子,當我點擊 home 鍵回到桌面時:


點擊 home 鍵

能夠看到我已經加載了的幾個 Fragment 齊刷刷的調用了 onPause 和 onStop,如此得步調一致是由於這些 Fragment attach 的 Activity 回調了 onPause 和 onStop。相信確定會有人說「不對啊,我要是用 replace 的方式去切換 Fragment,我打包票 Fragment 會像 Activity 同樣,完整得走完生命週期「

你說的沒錯,由於 replace 這種切換方式就是始終上面我總結的那句「首次建立或銷燬「,而且在 BottomNavigation 這樣的使用場景中,沒人會用這種 replace 的方式,由於每次切換都要從新建立 Fragment,用戶看了下流量估計會打 12315 了。

(若是 Fragment 表明前男/女朋友,聽說男人是用 add 保存,女人使用 replace 替換 hhh...)

當底部的四個 Fragment 都已經加載完成以後咧?再一塊兒看下 log:


onHiddenChanged 回調

當底部四個 Fragment 所有建立入棧以後,show 和 hide 來管理 Fragment,此時只有 onHiddenChanged 方法回調,這時候顯然你能夠在 hide = false 時,作一些資源回收操做,在 hide = true 時,作一些刷新操做。

在剛纔咱們打印的方法中,好像有一個一直沒出現,沒錯就是 setUserVisibleHint,若是你作過 Fragment+ViewPager 的懶加載(下文咱們會講這個),相信你對它就比較熟悉了,經過名字就能猜想出來,這個方法是咱們主動設置給 Fragment 的,那咱們就來試試看:

改造 addOrShowFragment

/** * 添加或者顯示 fragment * * @param transaction * @param fragment */ private void addOrShowFragment(FragmentTransaction transaction, Fragment fragment) { if (currentFragment == fragment) return; if (!fragment.isAdded()) { // 若是當前fragment未被添加,則添加到Fragment管理器中 transaction.hide(currentFragment).add(R.id.fl_content, fragment).commit(); } else { transaction.hide(currentFragment).show(fragment).commit(); } currentFragment.setUserVisibleHint(false); currentFragment = (BaseFragment) fragment; currentFragment.setUserVisibleHint(true); }

在切換時,咱們對上一個 fragment setUserVisibleHint設置爲 false,要展示的 Fragment setUserVisibleHint 設置爲 true,打印 log 看看:


setUserVisibleHint

能夠看到目前咱們用 setUserVisibleHint 也達到了與 onHiddenChanged 同樣的效果。

文章寫到如今,咱們來作一個總結,上文的這種方式就是主流經過 BottomNavigation 管理 Fragment 的方式,這種方式有什麼好處呢?毫無疑問會節省資源,不點開的界面不去建立它(這一點 ViewPager 作不到),只建立一次,將來僅僅更新界面數據就能夠了。

ViewPaager & Fragment

ViewPager 和 Fragment 配合使用相信大多數人都很熟悉了,因此來快速地給你們總結一下我認爲須要梳理清楚的幾個知識點,先來看我搭建的頁面:


TabLayout+ViewPager

我在導航的這個模塊中,搭建了一個 TabLayout+ViewPager+Fragment 的頁面結構,當啓動 app,進入首頁,各個 Fragment 的生命週期方法是怎樣的呢?


首頁和 Tab 頁生命週期方法

能夠看到,當我進入 app 的時候,全部 TabLayout 所在的父容器 SearchFragment 建立,調用了 onAttach --> onResume 這固然是咱們預料之中的,咱們 ViewPager 第一個裝載並展現的是 ScienceFragment,ScienceFragment 建立沒有問題,但是第二個 tab GameFragment 爲何加載了呢?

沒錯這就是 ViewPager 的預加載機制。

ViewPager 出於優化體驗的好心,默認去加載相鄰兩頁,來儘量保證滑動的流暢性,但是假如咱們這是一個新聞資訊類的 app,每個 tab 涉及了複雜的頁面和大量的網絡請求,這種預加載的機制帶來的可能就是麻煩了。因此咱們尋找一些辦法試圖去掉 ViewPager 的預加載。

ViewPager 自身提供了一個方法,mViewPager.setOffscreenPageLimit(),這個方法的官方文檔的解釋:


setOffscreenPageLimit 官方文檔

它的意思就是設置 ViewPager 左右兩側緩存頁的數量,默認是1,那咱們給它設置爲0,是否是就能取消預加載了呢?再看看這段蜜汁源碼:


setOffscreenPageLimit 源碼

總之源碼的意思就是你設置爲小於1的值就默認爲1,反正這條路目前行不通了。

固然還有一個辦法,你直接修改源碼之後從新打一個 v4 包,不過很是不建議這樣作,將來會產生一些兼容問題。

好吧,你應該知道立刻就要說 ViewPager 的懶加載了, 就是要用到上文咱們提到的 setUserVisibleHint 方法,當我左右滑動時,來看看打印的 log:


滑動時 log

從 log 中能夠分析到兩個問題,首先 setUserVisibleHint 這個方法可能會在 onAttach 以前就調用,其次在滑動中設置緩存頁數以外的頁確實是銷燬了。

回顧下前文, 明明說 setUserVisibleHint 這個方法須要主動調用,那在 ViewPager 中,Fragment 的 setUserVisibleHint 方法是誰在什麼時候調用的呢?

個人 ViewPager Adapter 在這裏:

public class MainTabAdapter extends FragmentStatePagerAdapter { private List<Fragment> mList; private String mTabTitle[] = new String[]{"科技", "遊戲", "裝備", "創業", "想法"}; public MainTabAdapter(FragmentManager fm, List<Fragment> list) { super(fm); mList = list; } @Override public Fragment getItem(int position) { return mList.get(position); } @Override public int getCount() { return mList.size(); } @Override public CharSequence getPageTitle(int position) { return mTabTitle[position]; } }

看來看去也就 FragmentStatePagerAdapter 能作這件事了,點進去看看:

FragmentStatePagerAdapter 這個類其實就是一個對 PagerAdapter 的一個封裝類,不到200行的代碼,果然找到了 setUserVisibleHint 。

有兩處位置調用了 setUserVisibleHint ,第一個位置:


instantiateItem

第二個位置:


setPrimaryItem

這裏源碼處理的邏輯是這樣子的:

在 instantiateItem 方法中,在個人數據集合中取出對應 positon 的 Fragment,直接給它的 setUserVisibleHint 設置爲 false,而後才把它 add 進 FragmentTransaction 中,這偏偏解釋了爲何 setUserVisibleHint 的第一次調用是在 onAttach 以前。

下一步 setUserVisibleHint 的設置是在 setPrimaryItem中,setPrimaryItem這個方法能夠獲得當前 ViewPager 正在展現的 Fragment,而且將上一個 Fragment 的 setUserVisibleHint 置爲 false,將要展現的 setUserVisibleHint 置爲 true。

經過閱讀源碼,咱們明白了原理,因此直接給你們上在 BaseFragment 實現懶加載代碼:

public class BaseFragment extends Fragment { protected boolean isViewCreated = false; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return super.onCreateView(inflater, container, savedInstanceState); } @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (getUserVisibleHint()) { lazyLoadData(); } } /** * 懶加載方法 */ protected void lazyLoadData() { } }

在具體的 Fragment 中實現懶加載

public class ScienceFragment extends BaseFragment { @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_science, container, false); Log.e(TAG, "onCreateView"); isViewCreated = true; return view; } @Override protected void lazyLoadData() { super.lazyLoadData(); if (isViewCreated) { Log.e(TAG, "lazyLoadData..."); } } }

看下 log:


懶加載 log

能夠看到,懶加載就這樣實現了。

這裏咱們再作一個階段總結,首先你們內心要清楚 setUserVisibleHint 這個是 ViewPager 的行爲,它始終都是先行與 Fragment 的生命週期調用的。咱們之因此能用懶加載這種辦法,主要是由於預加載的 Fragment 已經建立完成一路調用了 onAttach --> onPause,也就是說這個 Fragment 此時可用的,懶加載纔有理由生效。不知道這樣描述是否難懂,可是跑一下本文的例子就確定能明白上面這段話了。

因此當 Fragment 第一次建立時,懶加載不會同時調用,因此咱們來繼續優化優化,咱們讓 ViewPager 一塊兒加載這五個 Fragment 的佈局,而後懶加載就全程可用啦~

mViewPager.setOffscreenPageLimit(5);

此時不管是我滑動仍是點擊上方 tab 跳轉到任意一個頁面,lazyLoadData 方法都會調用,咱們能夠先加載佈局出來,而後可見時刷新數據就 OK 了。

關於 Fragment 的管理,主要就是上文的兩種方式,一是 add 和 hide 管理,二是 ViewPager + Fragment,固然具體到每一個人本身的項目時,須要分析需求和產品思路找到一個適合本身的方式。當咱們知道原理時,作操做就內心有底多了,出了問題也能夠快速定位。

上面咱們 ViewPager 的 adapter 使用的是 FragmentStatePagerAdapter,還有一個 FragmentPagerAdapter,由於本文的篇幅有些過長,下次會總結出它們在源碼角度的區別,以及使用過程當中踩到的一些坑。(若是有一些奇怪的問題沒法解決,建議先使用 FragmentStatePagerAdapter)。

寫在最後:

咱們回過頭來看開始那個辯題, Fragment 到底用不用?對於大多數開發者來講,固然要用,我我的其實還很是喜歡 Fragment,使用 Fragment 能體現 Android 組件化的思想,其帶給開發者的便利遠大於麻煩。雖然其生命週期複雜,棧又奇怪難管理,不過當正確的姿式使用 Fragment(不要嵌套 Fragment 使用),趟過一個個坑時,對 Fragment 天然也有信心了。

最後再上一張 Fragment 的生命週期圖吧~


fragment 生命週期
相關文章
相關標籤/搜索