概述
Android日常開發中除了四個組件之外,還有一種使用頻率很高的組件——Fragment。在使用時我們通常需要在Fragment的各種生命週期方法中處理數據加載、頁面刷新和資源釋放等邏輯操作。
但是當Fragment遇上了ViewPager,事情就變得有點不一樣了。Fragment的生命週期變得不再那麼可控,當顯示Fragment A時,相鄰的Fragment B的一些生命週期方法也會觸發。這是因爲ViewPager爲了優化切換效果,使切換更流暢、順滑。引入了預加載和緩存機制,通常會預加載前一個和後一個Fragment,讓前一個和後一個Fragment提前初始化。
當頁面佈局過於複雜或者數據量比較大,甚至當Fragment中有播放器時,預加載會耗費資源,造成頁面卡頓甚至頁面播放器出現異常報錯。
懶加載
使用懶加載的意義就在於只有當Fragment被顯示時,纔會去加載耗費資源的素材和數據,可以節省資源、提升頁面流暢度,而且讓流程變得更可控。
實現思路
Fragment中提供了一對可見性相關的方法setUserVisibleHint(boolean isVisibleToUser)
和getUserVisibleHint()
可以通過重寫setUserVisibleHint()
來監聽頁面可見性變化,當頁面從不可見變爲可見時觸發加載數據方法,反之也可以實現頁面從可見到不可見時部分資源的釋放操作。
實現
先實現一個Fragment + ViewPager的結構(實現很簡單省略了),依次有三個Fragment爲:AFragment、BFragment和CFragemtn,三個Fragment分別繼承基類BaseLazyLoadFragment。
生命週期變化
在基類中添加生命週期方法的打印,如下圖:
從Fragment的生命週期變化可以看出,需要注意的有幾點:
setUserVisibleHint()
方法的調用在onCreateView()
方法之前。- 進入Activity時第一個被顯示的Fragment,會調用兩次
setUserVisibleHint()
第一次值爲false,第二次值爲true。 - ViewPager的預加載會讓還沒顯示的Fragment提前初始化。
- 當AFragment切換到BFragment時,會先調用AFragment的
setUserVisibleHint(false)
方法,後調用BFragment的setUserVisibleHint(true)
,我們可以在AFragment中做部分資源的釋放操作。 - 當BFragment切換到AFragment時,AFragment會執行
onDestroyView()
方法釋放持有的佈局資源,但是AFragment中的數據資源並沒有釋放。 - 當從CFragment切換回BFragment時,AFragment會重新初始化。
代碼實現
基於以上幾點問題,我們通過來通過代碼實現BaseLazyLoadFragment。
public abstract class BaseLazyLoadFragment extends Fragment {
protected String TAG = BaseLazyLoadFragment.class.getSimpleName();
//Root View
protected View view;
//佈局是否初始化完成
private boolean isLayoutInitialized = false;
//懶加載完成
private boolean isLazyLoadFinished = false;
//記錄頁面可見性
private boolean isVisibleToUser = false;
//不可見時釋放部分資源
private boolean isInVisibleRelease = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, getClass().getSimpleName() + " onCreate");
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(TAG, getClass().getSimpleName() + " onCreateView");
view = inflater.inflate(initLayout(),null);
initView();
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.d(TAG, getClass().getSimpleName() + " onDestroyView");
//頁面釋放後,重置佈局初始化狀態變量
isLayoutInitialized = false;
this.view = null;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.d(TAG, getClass().getSimpleName() + " onActivityCreated");
//此方法是在第一次初始化時onCreateView之後觸發的
//onCreateView和onActivityCreated中分別應該初始化哪些數據可以參考:
//https://stackoverflow.com/questions/8041206/android-fragment-oncreateview-vs-onactivitycreated
isLayoutInitialized = true;
//第一次初始化後需要處理一次可見性事件
//因爲第一次初始化時setUserVisibleHint方法的觸發要先於onCreateView
dispatchVisibleEvent();
}
@Override
public void onStart() {
super.onStart();
Log.d(TAG, getClass().getSimpleName() + " onStart");
}
@Override
public void onResume() {
super.onResume();
Log.d(TAG, getClass().getSimpleName() + " onResume");
//頁面從其他Activity返回時,重新加載被釋放的資源
if(isLazyLoadFinished && isLayoutInitialized && isInVisibleRelease){
visibleReLoad();
isInVisibleRelease = false;
}
}
@Override
public void onPause() {
super.onPause();
Log.d(TAG, getClass().getSimpleName() + " onPause");
//當從Fragment切換到其他Activity釋放部分資源
if(isLazyLoadFinished && isVisibleToUser){
//頁面從可見切換到不可見時觸發,可以釋放部分資源,配合reload方法再次進入頁面時加載
inVisibleRelease();
isInVisibleRelease = true;
}
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d(TAG, getClass().getSimpleName() + " onDestroy");
//重置所有數據
this.view = null;
isLayoutInitialized = false;
isLazyLoadFinished = false;
isVisibleToUser = false;
isInVisibleRelease = false;
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.d(TAG, getClass().getSimpleName() + " setUserVisibleHint isVisibleToUser = " + isVisibleToUser);
dispatchVisibleEvent();
}
/**
* 處理可見性事件
*/
private void dispatchVisibleEvent(){
Log.d(TAG, getClass().getSimpleName() + " dispatchVisibleEvent isVisibleToUser = " + getUserVisibleHint()
+ " --- isLayoutInitialized = " + isLayoutInitialized + " --- isLazyLoadFinished = " + isLazyLoadFinished);
if(getUserVisibleHint() && isLayoutInitialized){
//可見
if(!isLazyLoadFinished){
//第一次可見,懶加載
lazyLoad();
isLazyLoadFinished = true;
} else{
//非第一次可見,刷新數據
visibleReLoad();
}
} else{
if(isLazyLoadFinished && isVisibleToUser){
//頁面從可見切換到不可見時觸發,可以釋放部分資源,配合reload方法再次進入頁面時加載
inVisibleRelease();
}
}
//處理完可見性事件之後修改isVisibleToUser狀態
this.isVisibleToUser = getUserVisibleHint();
}
/**
* 初始化View
*/
protected abstract void initView();
/**
* 綁定佈局
* @return 佈局ID
*/
protected abstract int initLayout();
/**
* 懶加載<br/>
* 只會在初始化後第一次可見時調用一次。
*/
protected abstract void lazyLoad();
/**
* 刷新數據加載<br/>
* 配合{@link #lazyLoad()},在頁面非第一次可見時刷新數據
*/
protected abstract void visibleReLoad();
/**
* 當頁面從可見變爲不可見時,釋放部分數據和資源。<br/>
* 比如頁面播放器的釋放或者一些特別佔資源的數據的釋放
*/
protected abstract void inVisibleRelease();
}
代碼註釋比較詳細了,簡單說一下。BaseLazyLoadFragment中提供了
lazyLoad()
方法當頁面被顯示時做懶加載;visibleReLoad()
方法當頁面沒有被釋放且從不可見狀態切換到可見時刷新數據用;inVisibleRelease()
方法當頁面從可見狀態切換到不可見時,做部分資源釋放(如播放器等)。- 同樣支持當切換到其他Activity時,觸發
inVisibleRelease()
方法做資源釋放,從Activity返回頁面時觸發visibleReLoad()
刷新加載數據。
小結
以上封裝的BaseLazyLoadFragment應該能夠滿足Fragment + ViewPager實現方式的大多數需求場景。
針對Fragment + ViewPager的懶加載實現,還有一種實現方式:從ViewPager上入手,既然是因爲ViewPager的預加載導致的Fragment的生命週期不可控,那麼關掉ViewPager的預加載就好了。這種實現方式需要重寫ViewPager,需要閱讀ViewPager源碼針對預加載部分進行修改,而且在不同SDK版本的ViewPager的具體邏輯有差異,只能對某一版本的ViewPager進行修改。
至於那種實現方式更合適,那就需要按具體需求分析了。我個人比較推薦BaseLazyLoadFragment的實現方式,實現簡單、適配性更好也更加優雅。