死磕 Fragment 的生命周期
本文原創(chuàng),轉(zhuǎn)載請注明出處。
歡迎關(guān)注我的 簡書 ,關(guān)注我的專題 Android Class 我會長期堅持為大家收錄簡書上高質(zhì)量的 Android 相關(guān)博文。
本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨家發(fā)布
本文例子中 github 地址:
github BuzzerBeater 項目鏈接
(第一個開源項目,目前在逐步更新一些知識點,希望對你有所幫助)
曾經(jīng)在北京擁擠的13號線地鐵上,一名背著雙肩包穿著格子衫帶著鴨舌帽腳踏帆布鞋的程序員講了一句:
“我覺得 Fragment 真的太難用了”。從而引起一陣躁動激烈的討論。
正方觀點:
Fragment 真的太好用了。要知道因為 Activity 的啟動,涉及到 Android 系統(tǒng)對 ActivityManager 的調(diào)度,會關(guān)聯(lián)許多資源和進行諸多復(fù)雜運算,在一些高端手機上,這個調(diào)度的時長甚至?xí)^ 100 ms。反觀 Fragment,啟動如巧克力入口般順滑,輕量不消耗手機資源。還可以做成一個個模塊,方便 Activity 復(fù)用。并且如果要涉及平板的適配,F(xiàn)ragment 更是必不可少。
反方觀點:
Fragment 難用,屬于坑多難填。Fragment 本質(zhì)上是一個有生命周期的 View,生命周期繁多并且異常難管理,多個 Fragment 嵌套更是坑中之坑(我也遇到過...),連 square 和 FaceBook 都摒棄了 Fragment,更何況我們呢!
好吧,不要吵了,用或者不用,遇到問題如何解決,相信大家心里都有一個自己的答案。結(jié)合我自己開發(fā)時候遇到的問題,下面我們來總結(jié)一下 Fragment 的生命周期管理方式,以及一些技巧和建議。
hide & show
先結(jié)合一張項目截圖,來直觀地看看目前我是如何管理 Fragment 的。
因為我們的項目架構(gòu)是一 Activity 多 Fragment,并且把所有的 Fragment 都裝載到了一個 ViewPager 里面,啟動一個新的 Fragment 的過程也是 ViewPager 滑動翻頁的過程,未來會考慮把這種管理方式總結(jié)成文,整理給大家。總之你目前看到的上圖界面,都是 Fragment 呈現(xiàn)的,并且點擊按鈕什么的,也會跳轉(zhuǎn)到下一個 Fragment,這就涉及到了 Fragment 嵌套。
我也寫了一個 Demo,去模擬了這個頁面的搭建。
這里想多說幾句
通過點擊下方 Tab 管理頁面的方式目前非常主流,這種布局方式事實上是從 iOS 上面借鑒過來的。(反正現(xiàn)在兩大系統(tǒng)都是相互學(xué)習(xí))在前一陣 google 的 support 25 包也終于推出了官方的 BottomNavigationView ,不過我的 Android Studio 安裝 support 25 總是失敗,所以在項目中我選用了一個還不錯的開源庫來做下方的底部導(dǎo)航。
BottomNavigationView 本身有一套 Material Design 的設(shè)計規(guī)范如下:
Material Design Navigation Bottom
感興趣的去閱讀一下,以后對產(chǎn)品、設(shè)計開撕是很有幫助的。其中有這么一條很有意思,是說 BottomNavigationView 并不建議把它設(shè)計成橫向滑動的形式,也就是用 ViewPager 去裝載 Fragment。為什么說這句很有意思呢?事實上市面上很多主流的 app,它們的 BottomNavigationView 確實是不可以橫向滑動的,而我們每個人都在用的,月活8億的國民軟件微信,就恰恰把它的主頁面做成了可以橫向滑動的。
這里我想說下我的個人看法,首先規(guī)范未必需要嚴(yán)格遵守,做什么樣的功能實現(xiàn)什么樣的效果,要結(jié)合自己項目的架構(gòu)和產(chǎn)品做一個合理的需求。拿 360手機助手 這個 app 舉例,它底部的每一個 tab 都搭建了一個非常“重量級”的模塊,并且每個 tab 下界面的內(nèi)部還有許多負(fù)責(zé)的 View 層級和嵌套滑動的 ViewPager,所以試想一下,這樣的頁面要是做成微信那個樣子,不卡頓就怪了~反觀微信,首先我認(rèn)為它的每個界面層級和交互都不復(fù)雜,邏輯也都在頁面內(nèi),所以做成橫向滑動的反而能提升用戶的體驗。
好了,前面說了好多無關(guān)緊要的話,趕緊來看看 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()) {
// 提交事務(wù)
getSupportFragmentManager().beginTransaction().add(R.id.fl_content, searchFragment).commit();
// 記錄當(dāng)前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) {// 導(dǎo)航
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()) { // 如果當(dāng)前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
當(dāng)?shù)谝淮芜M入某個頁面時:
可以看到,當(dāng)我依次點擊下方四個 tab,界面第一次加載的時,會走 Fragment 的創(chuàng)建周期 onAttach -- onResume,也許你會問,上面我執(zhí)行相互切換操作,從第一個頁面切換到第二個的時候,為什么第一個 Fragment 頁面不可見了,不會調(diào)用 onPause 和 onStop 呢?
這是在你了解過 Activity 生命周期并且剛接觸 Fragment 的生命周期時,第一個容易陷入的誤區(qū),事實上 Fragment 的生命周期,除了第一次它創(chuàng)建或銷毀之外,統(tǒng)統(tǒng)都是由 Activity 所驅(qū)動的,正常來說,F(xiàn)ragment 的生命周期應(yīng)該是以 add 到 FragmentTransation 開始,從 FragmentTransation remove 掉結(jié)束。 舉個例子,當(dāng)我點擊 home 鍵回到桌面時:
可以看到我已經(jīng)加載了的幾個 Fragment 齊刷刷的調(diào)用了 onPause 和 onStop,如此得步調(diào)一致是因為這些 Fragment attach 的 Activity 回調(diào)了 onPause 和 onStop。相信肯定會有人說“不對啊,我要是用 replace 的方式去切換 Fragment,我打包票 Fragment 會像 Activity 一樣,完整得走完生命周期“
你說的沒錯,因為 replace 這種切換方式就是始終上面我總結(jié)的那句“首次創(chuàng)建或銷毀“,并且在 BottomNavigation 這樣的使用場景中,沒人會用這種 replace 的方式,因為每次切換都要重新創(chuàng)建 Fragment,用戶看了下流量估計會打 12315 了。
(如果 Fragment 代表前男/女友,據(jù)說男人是用 add 保存,女人使用 replace 替換 hhh...)
當(dāng)?shù)撞康乃膫€ Fragment 都已經(jīng)加載完成之后咧?再一起看下 log:
當(dāng)?shù)撞克膫€ Fragment 全部創(chuàng)建入棧之后,show 和 hide 來管理 Fragment,此時只有 onHiddenChanged
方法回調(diào),這時候顯然你可以在 hide = false
時,做一些刷新操作,在 hide = true
時,做一些資源回收操作。
在剛才我們打印的方法中,好像有一個一直沒出現(xiàn),沒錯就是 setUserVisibleHint
,如果你做過 Fragment+ViewPager 的懶加載(下文我們會講這個),相信你對它就比較熟悉了,通過名字就能猜測出來,這個方法是我們主動設(shè)置給 Fragment 的,那我們就來試試看:
改造 addOrShowFragment
/**
* 添加或者顯示 fragment
*
* @param transaction
* @param fragment
*/
private void addOrShowFragment(FragmentTransaction transaction, Fragment fragment) {
if (currentFragment == fragment)
return;
if (!fragment.isAdded()) { // 如果當(dāng)前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
設(shè)置為 false,要展現(xiàn)的 Fragment setUserVisibleHint
設(shè)置為 true,打印 log 看看:
可以看到目前我們用 setUserVisibleHint 也達到了與 onHiddenChanged 一樣的效果。
文章寫到現(xiàn)在,我們來做一個總結(jié),上文的這種方式就是主流通過 BottomNavigation 管理 Fragment 的方式,這種方式有什么好處呢?毫無疑問會節(jié)省資源,不點開的界面不去創(chuàng)建它(這一點 ViewPager 做不到),只創(chuàng)建一次,未來僅僅更新界面數(shù)據(jù)就可以了。
ViewPager & Fragment
ViewPager 和 Fragment 配合使用相信大多數(shù)人都很熟悉了,所以來快速地給大家總結(jié)一下我認(rèn)為需要梳理清楚的幾個知識點,先來看我搭建的頁面:
我在導(dǎo)航的這個模塊中,搭建了一個 TabLayout+ViewPager+Fragment 的頁面結(jié)構(gòu),當(dāng)啟動 app,進入首頁,各個 Fragment 的生命周期方法是怎樣的呢?
可以看到,當(dāng)我進入 app 的時候,所有 TabLayout 所在的父容器 SearchFragment 創(chuàng)建,調(diào)用了 onAttach --> onResume 這當(dāng)然是我們預(yù)料之中的,我們 ViewPager 第一個裝載并展示的是 ScienceFragment,ScienceFragment 創(chuàng)建沒有問題,可是第二個 tab GameFragment 為什么加載了呢?
沒錯這就是 ViewPager 的預(yù)加載機制。
ViewPager 出于優(yōu)化體驗的好心,默認(rèn)去加載相鄰兩頁,來盡可能保證滑動的流暢性,可是假如我們這是一個新聞資訊類的 app,每一個 tab 涉及了復(fù)雜的頁面和大量的網(wǎng)絡(luò)請求,這種預(yù)加載的機制帶來的可能就是麻煩了。所以我們尋找一些辦法試圖去掉 ViewPager 的預(yù)加載。
ViewPager 自身提供了一個方法,mViewPager.setOffscreenPageLimit()
,這個方法的官方文檔的解釋:
它的意思就是設(shè)置 ViewPager 左右兩側(cè)緩存頁的數(shù)量,默認(rèn)是1,那我們給它設(shè)置為0,是不是就能取消預(yù)加載了呢?再看看這段蜜汁源碼:
總之源碼的意思就是你設(shè)置為小于1的值就默認(rèn)為1,反正這條路目前行不通了。
當(dāng)然還有一個辦法,你直接修改源碼以后重新打一個 v4 包,不過非常不建議這樣做,未來會產(chǎn)生一些兼容問題。
好吧,你應(yīng)該知道馬上就要說 ViewPager 的懶加載了, 就是要用到上文我們提到的 setUserVisibleHint 方法,當(dāng)我左右滑動時,來看看打印的 log:
從 log 中可以分析到兩個問題,首先 setUserVisibleHint 這個方法可能會在 onAttach 之前就調(diào)用,其次在滑動中設(shè)置緩存頁數(shù)之外的頁確實是銷毀了。
回顧下前文, 明明說 setUserVisibleHint 這個方法需要主動調(diào)用,那在 ViewPager 中,F(xiàn)ragment 的 setUserVisibleHint 方法是誰在何時調(diào)用的呢?
我的 ViewPager Adapter 在這里:
public class MainTabAdapter extends FragmentStatePagerAdapter {
private List<Fragment> mList;
private String mTabTitle[] = new String[]{"科技", "游戲", "裝備", "創(chuàng)業(yè)", "想法"};
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 。
有兩處位置調(diào)用了 setUserVisibleHint ,第一個位置:
第二個位置:
這里源碼處理的邏輯是這樣子的:
在 instantiateItem 方法中,在我的數(shù)據(jù)集合中取出對應(yīng) positon 的 Fragment,直接給它的 setUserVisibleHint 設(shè)置為 false,然后才把它 add 進 FragmentTransaction 中,這恰恰解釋了為什么 setUserVisibleHint 的第一次調(diào)用是在 onAttach 之前。
下一步 setUserVisibleHint 的設(shè)置是在 setPrimaryItem
中,setPrimaryItem
這個方法可以得到當(dāng)前 ViewPager 正在展示的 Fragment,并且將上一個 Fragment 的 setUserVisibleHint 置為 false,將要展示的 setUserVisibleHint 置為 true。
通過閱讀源碼,我們明白了原理,所以直接給大家上在 BaseFragment 實現(xiàn)懶加載代碼:
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 中實現(xiàn)懶加載
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:
可以看到,懶加載就這樣實現(xiàn)了。
這里我們再做一個階段總結(jié),首先大家心里要清楚 setUserVisibleHint 這個是 ViewPager 的行為,它始終都是先行與 Fragment 的生命周期調(diào)用的。我們之所以能用懶加載這種辦法,主要是因為預(yù)加載的 Fragment 已經(jīng)創(chuàng)建完成一路調(diào)用了 onAttach --> onPause,也就是說這個 Fragment 此時可用的,懶加載才有理由生效。不知道這樣描述是否難懂,但是跑一下本文的例子就肯定能明白上面這段話了。
所以當(dāng) Fragment 第一次創(chuàng)建時,懶加載不會同時調(diào)用,所以我們來繼續(xù)優(yōu)化優(yōu)化,我們讓 ViewPager 一起加載這五個 Fragment 的布局,然后懶加載就全程可用啦~
mViewPager.setOffscreenPageLimit(5);
此時無論是我滑動還是點擊上方 tab 跳轉(zhuǎn)到任意一個頁面,lazyLoadData
方法都會調(diào)用,我們可以先加載布局出來,然后可見時刷新數(shù)據(jù)就 OK 了。
關(guān)于 Fragment 的管理,主要就是上文的兩種方式,一是 add 和 hide 管理,二是 ViewPager + Fragment,當(dāng)然具體到每個人自己的項目時,需要分析需求和產(chǎn)品思路找到一個適合自己的方式。當(dāng)我們知道原理時,做操作就心里有底多了,出了問題也可以快速定位。
上面我們 ViewPager 的 adapter 使用的是 FragmentStatePagerAdapter,還有一個 FragmentPagerAdapter,因為本文的篇幅有些過長,下次會總結(jié)出它們在源碼角度的區(qū)別,以及使用過程中踩到的一些坑。(如果有一些奇怪的問題無法解決,建議先使用 FragmentStatePagerAdapter)。
寫在最后:
我們回過頭來看開始那個辯題, Fragment 到底用不用?對于大多數(shù)開發(fā)者來說,當(dāng)然要用,我個人其實還非常喜歡 Fragment,使用 Fragment 能體現(xiàn) Android 組件化的思想,其帶給開發(fā)者的便利遠大于麻煩。雖然其生命周期復(fù)雜,棧又奇怪難管理,不過當(dāng)正確的姿勢使用 Fragment(不要嵌套 Fragment 使用),趟過一個個坑時,對 Fragment 自然也有信心了。
最后再上一張 Fragment 的生命周期圖吧~