2018
寫在前面:這篇文章主要分析導航欄,也就是ViewPager+Fragment+FragmentPagerAdapter的懶加載模式,重點還是Fragment生命週期的應用。
如果對如何使用導航欄還不太瞭解,可以看看 底部導航欄標籤切換的實現 這篇文章。
(一)Fragment生命週期
大家都知道 Fragment 是綁定 Activity 的,不過,很多人會忽視了它們的生命週期也是會一定程度上同步的。下面表格的場景是在 Activity 內部實例化 Fragment,Fragment 與 Activity 生命週期之間的關係。
Activity | Fragment |
---|---|
onCreate() | onAttach(), onCreateView(), onCreateView, onActivityCreate() |
onStart() | onStart() |
onResume() | onResume() |
onPause() | onPause() |
onStop() | onStop() |
onDestroy() | onDestroyView(), onDestroy(), onDetach() |
大多數時候我們將實例化 Fragment 放在 Activity.onCreate()
中,如new
一個實例對象。不難理解 Fragment 在Activity.onCreate()
之後纔開始一系列生命週期方法的調用。而其他方法執行順序會是下面這樣:
Activity.onStart()-->Fragment.onStart()
Activity.onResume()-->Fragment.onResume()
Fragment.onPause()-->Activity.onPause()
Fragment.onStop()-->Activity.onStop()
Fragment.onDestroyView(),onDestroy(),onDetach()-->Activity.onDestroy()
Fragment 依賴於 Activity,故而,Fragment創建於Acivity之後,銷燬於Acrivity之前。
ViewPager中Fragment的生命週期
結合 ViewPager 後,頁面切換會有哪些生命週期方法的調用?
看過上一篇博文的朋友應該都比較清楚了,這裏就再不厭其煩地給沒看過的朋友講解一下。
單獨使用Fragment沒什麼好說的,調用的方法和上表一樣,會和 Activity 同步。而結合 ViewPager 取決於選擇哪一種適配器,現在大多數使用 FragmentPagerAdapter或者 FragmentPagerStateAdapter,兩者的區別是到底會不會回收Fragment的內存。
前者只回收View,除非內存不足,否則不會銷燬加載好的Fragment;後者爲達到節省內存的目的,對於不在當前頁面左右兩邊的其他Page,將會銷燬其Fragment,只保留最多三個頁面在內存中。
針對 FragmentPagerAdapter 的測試
Fragment-->
代表的是第一個Fragment 的生命週期,Activity-->
代表與之關聯的活動的生命週期:
// 程序啓動,初始化活動
I/MyTest: Activity-->onCreate()
I/MyTest: Activity-->onStart()
Activity-->onResume()
// 加載Fragment
I/MyTest: Fragment-->getUserVisibleHint:true
I/MyTest: Fragment-->setUserVisibleHint(false)
Fragment-->getUserVisibleHint:false
Fragment-->setUserVisibleHint(true)
I/MyTest: Fragment-->onAttach()
Fragment-->onCreate()
I/MyTest: Fragment-->onCreateView()
I/MyTest: Fragment-->onActivityCreated()
Fragment-->onStart()
Fragment-->onResume()
//點擊第三個頁面
I/MyTest: Fragment-->getUserVisibleHint:true
Fragment-->setUserVisibleHint(false)
I/MyTest: Fragment-->onPause()
I/MyTest: Fragment-->onStop()
Fragment-->onDestroyView() // View被回收,但是內存沒有回收
//返回第一個頁面
I/MyTest: Fragment-->getUserVisibleHint:false
Fragment-->setUserVisibleHint(false)
Fragment-->getUserVisibleHint:false
Fragment-->setUserVisibleHint(true)
Fragment-->onCreateView()
Fragment-->onActivityCreated()
Fragment-->onStart()
Fragment-->onResume()
//銷燬活動,與Activity生命週期同步
I/MyTest: Fragment-->onPause()
Activity-->onPause()
I/MyTest: Fragment-->onStop()
I/MyTest: Activity-->onStop()
I/MyTest: Fragment-->onDestroyView()
Fragment-->onDestroy()
Fragment-->onDetach()
Activity-->onDestroy()
發現
- 切換到不是 當前頁面左右兩邊的頁面 時,當前頁面會被回收View,但不被銷燬,原因是Adapter默認只完整加載左右兩邊頁面。
- 切換到 當前頁面左右兩邊的頁面 時,只會調用
setUserVisibleHint(true)
。 - Fragment 的生命週期與Activity息息相關。經過另外的測試,如果Activity變得不可見(比如開啓新活動),那麼所有加載好的Fragment會和Activity一起執行
onPause()
,onStop()
,返回時又會一起執行onStart()
,onResume()
,這就是生命週期的同步。 - 初始化的時候
setUserVisibleHint()
比Fragment任何生命週期方法要早調用。而從較遠頁面切換回來setUserVisibleHint()
也至少比onCreateView()
先執行,如頁面3到頁面1。
還是貼張圖表示這個過程:
(二)懶加載在不同業務場景下的實現
有了上面的知識儲備,靈活地使用“懶加載”也不難了,根據業務需要,我們可以設置不同的加載方式。
基本方法
使用 setUserVisibleHint()
,在進入一個頁面之前我們肯定會需要調用這個方法。
場景1
只需要拉取一次數據,但是很可能整個活動期間都不會切換到這個頁面,所以希望省點流量,只有切換到才加載,而且只有一次。寫法如下:
private boolean hasLoad;
@Override
public void setUserVisibleHint(boolean isVisible){
//設置爲可見並且沒有加載過數據的時候進行網絡請求
if(isVisible && (!hasLoad)){
/*
* 網路請求數據
*/
hasLoad = true;
}
super.setUserVisibleHint(isVisible);
}
場景2
每次進入頁面都要拉取數據,但只要加載數據成員不需要更新UI,下面這種寫法就可以了。
@Override
public void setUserVisibleHint(boolean isVisible){
if(isVisible){
/*
* 網路請求數據
*/
}
super.setUserVisibleHint(isVisible);
}
場景3
每次進入都需要拉取數據,數據加載後要更新UI。
爲什麼和第一種區分,留意“發現”的最後一點的兩種情況,此時Fragment還沒有加載View實例,而更新UI必須要確保已經獲取View實例。那不妨使用一個boolean變量來標識獲取View的狀態,確保View獲取了就加載,反之不加載。很有道理,不過還有缺陷,因爲等View加載完,我們已經不會調用setUserVisibleHint()
了,所以還要在View獲取後完成一次補充加載。
下面這種寫法就能確保無論是初始化、還是從較遠頁面切換過來或者從左右頁面切換過來,都能馬上拉取數據。大家可以直接用這種格式哦。
private boolean hasView;
@Override
public void setUserVisibleHint(boolean isVisible){
if(isVisible && hasView){
/*
* 網路請求數據 + 更新UI
*/
}
super.setUserVisibleHint(isVisible);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_layout_1, container,false); //動態加載佈局
/*
* 使用 view.findViewById(long resourceId)來獲取View實例
*/
hasView = true; //View加載好了
if(getUserVisibleHint()){
/*
* 數據請求 + UI更新
*/
}
return view;
}
@Override
public void onDestroyView(){
hasView=false; // View被回收了
super.onDestroyView();
}
留意上面onCreateView()
中的 if
代碼塊,這就是我們說的,如果是沒有View的情況下不能直接使用 setUserVisibleHint()
加載,需要之後採取一個補充加載。
(三)強調ViewPager中四種不同的Fragment狀態
- 當前頁面:Fragment 生命週期執行到
onResume()
,可見性getUserVisibleHint()
爲true
。 - 預加載頁面:位於當前頁面左右兩邊,Fragment 生命週期執行到
onResume()
,
可見性getUserVisibleHint()
爲false
。 - 曾經加載過但是被回收了View的頁面:肯定不在當前頁面左右,Fragment 生命週期執行到
onCreate()
,getUserVisibleHint()
爲false
。 - 從來沒有加載過的頁面:也肯定不在當前頁面左右,所有方法都還沒調用,
getUserVisibleHint()
爲false
。
暫時寫到這裏,關於數據懶加載我們也瞭解得差不多了,如有不正之處歡迎指正。