死磕 Fragment 的生命周期
本文原創,轉載請注明出處。
歡迎關注我的 簡書 ,關注我的專題 Android Class 我會長期堅持為大家收錄簡書上高質量的 Android 相關博文。
本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發布
本文例子中 github 地址:
github BuzzerBeater 項目鏈接
(第一個開源項目,目前在逐步更新一些知識點,希望對你有所幫助)
曾經在北京擁擠的13號線地鐵上,一名背着雙肩包穿着格子衫帶着鴨舌帽腳踏帆布鞋的程序員講了一句:
“我覺得 Fragment 真的太難用了”。從而引起一陣躁動激烈的討論。
正方觀點:
Fragment 真的太好用了。要知道因為 Activity 的啟動,涉及到 Android 系統對 ActivityManager 的調度,會關聯許多資源和進行諸多復雜運算,在一些高端手機上,這個調度的時長甚至會超過 100 ms。反觀 Fragment,啟動如巧克力入口般順滑,輕量不消耗手機資源。還可以做成一個個模塊,方便 Activity 復用。並且如果要涉及平板的適配,Fragment 更是必不可少。
反方觀點:
Fragment 難用,屬於坑多難填。Fragment 本質上是一個有生命周期的 View,生命周期繁多並且異常難管理,多個 Fragment 嵌套更是坑中之坑(我也遇到過...),連 square 和 FaceBook 都摒棄了 Fragment,更何況我們呢!
好吧,不要吵了,用或者不用,遇到問題如何解決,相信大家心里都有一個自己的答案。結合我自己開發時候遇到的問題,下面我們來總結一下 Fragment 的生命周期管理方式,以及一些技巧和建議。
hide & show
先結合一張項目截圖,來直觀地看看目前我是如何管理 Fragment 的。

因為我們的項目架構是一 Activity 多 Fragment,並且把所有的 Fragment 都裝載到了一個 ViewPager 里面,啟動一個新的 Fragment 的過程也是 ViewPager 滑動翻頁的過程,未來會考慮把這種管理方式總結成文,整理給大家。總之你目前看到的上圖界面,都是 Fragment 呈現的,並且點擊按鈕什么的,也會跳轉到下一個 Fragment,這就涉及到了 Fragment 嵌套。
我也寫了一個 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 的所有生命周期方法,包括onHiddenChanged
和 setUserVisibleHint
當第一次進入某個頁面時:

可以看到,當我依次點擊下方四個 tab,界面第一次加載的時,會走 Fragment 的創建周期 onAttach -- onResume,也許你會問,上面我執行相互切換操作,從第一個頁面切換到第二個的時候,為什么第一個 Fragment 頁面不可見了,不會調用 onPause 和 onStop 呢?
這是在你了解過 Activity 生命周期並且剛接觸 Fragment 的生命周期時,第一個容易陷入的誤區,事實上 Fragment 的生命周期,除了第一次它創建或銷毀之外,統統都是由 Activity 所驅動的。 舉個例子,當我點擊 home 鍵回到桌面時:

可以看到我已經加載了的幾個 Fragment 齊刷刷的調用了 onPause 和 onStop,如此得步調一致是因為這些 Fragment attach 的 Activity 回調了 onPause 和 onStop。相信肯定會有人說“不對啊,我要是用 replace 的方式去切換 Fragment,我打包票 Fragment 會像 Activity 一樣,完整得走完生命周期“
你說的沒錯,因為 replace 這種切換方式就是始終上面我總結的那句“首次創建或銷毀“,並且在 BottomNavigation 這樣的使用場景中,沒人會用這種 replace 的方式,因為每次切換都要重新創建 Fragment,用戶看了下流量估計會打 12315 了。
(如果 Fragment 代表前男/女友,據說男人是用 add 保存,女人使用 replace 替換 hhh...)
當底部的四個 Fragment 都已經加載完成之后咧?再一起看下 log:

當底部四個 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 也達到了與 onHiddenChanged 一樣的效果。
文章寫到現在,我們來做一個總結,上文的這種方式就是主流通過 BottomNavigation 管理 Fragment 的方式,這種方式有什么好處呢?毫無疑問會節省資源,不點開的界面不去創建它(這一點 ViewPager 做不到),只創建一次,未來僅僅更新界面數據就可以了。
ViewPaager & Fragment
ViewPager 和 Fragment 配合使用相信大多數人都很熟悉了,所以來快速地給大家總結一下我認為需要梳理清楚的幾個知識點,先來看我搭建的頁面:

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

可以看到,當我進入 app 的時候,所有 TabLayout 所在的父容器 SearchFragment 創建,調用了 onAttach --> onResume 這當然是我們預料之中的,我們 ViewPager 第一個裝載並展示的是 ScienceFragment,ScienceFragment 創建沒有問題,可是第二個 tab GameFragment 為什么加載了呢?
沒錯這就是 ViewPager 的預加載機制。
ViewPager 出於優化體驗的好心,默認去加載相鄰兩頁,來盡可能保證滑動的流暢性,可是假如我們這是一個新聞資訊類的 app,每一個 tab 涉及了復雜的頁面和大量的網絡請求,這種預加載的機制帶來的可能就是麻煩了。所以我們尋找一些辦法試圖去掉 ViewPager 的預加載。
ViewPager 自身提供了一個方法,mViewPager.setOffscreenPageLimit()
,這個方法的官方文檔的解釋:

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

總之源碼的意思就是你設置為小於1的值就默認為1,反正這條路目前行不通了。
當然還有一個辦法,你直接修改源碼以后重新打一個 v4 包,不過非常不建議這樣做,未來會產生一些兼容問題。
好吧,你應該知道馬上就要說 ViewPager 的懶加載了, 就是要用到上文我們提到的 setUserVisibleHint 方法,當我左右滑動時,來看看打印的 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 方法中,在我的數據集合中取出對應 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:

可以看到,懶加載就這樣實現了。
這里我們再做一個階段總結,首先大家心里要清楚 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 的生命周期圖吧~
