Android - 懶載入
今天我們來聊一聊ViewPager+Fragment的懶載入。
1.什麼是懶載入,為什麼要用懶載入?
如果我們的專案中使用了ViewPager+Framgment實現底部Tab可點可滑,那麼我們都知道ViewPager有預載入功能,通viewpager.setOffscreenPageLimit();來設定,不設定預設載入上一個和下一個Fragment頁面,帶上本身也就是三個頁面(當然如果你剛進入就是首頁,那麼它會載入首頁和下一個頁面,因為首頁上面沒有頁面呀)。預載入功能會暴露一個問題,比如我剛進入載入首頁的資料,但是因為有預載入功能,那麼就會執行下一個Tab對應的Fragmeng的生命週期,如果我下一個Tab頁資料量小還好,如果我有比較耗時的操作或者網路請求,勢必會影響程式的效能,影響使用者的體驗。那麼我們要做的就是禁止ViewPager預載入或者提供一個只在Fragemnt可見的情況下,才去進行耗時操作的方法,只要Fragmeng可見我們就執行該方法。
2.懶載入解決方式
2.1 嘗試設定setOffscreenPageLimit(失敗)
之前想既然setOffscreenPageLimit可以設定,那我就將其設定為0,結果“然並卵”,檢視原始碼如下:

image.png
可以看到如果我們設定的值,小於DEFAULT_OFFSCREEN_PAGES這個常量值,那麼就會被賦值為DEFAULT_OFFSCREEN_PAGES,那麼我們看看DEFAULT_OFFSCREEN_PAGES值是多少:

image.png
2.2 試試懶載入
我們先來看看我們的頁面:

image.png
就是正常的viewpager+fragment,每個頁面有一個TextView,可點可劃。三個Fragment依次是:HomeFragment,InvestFragment,UserFragment,
-
本次要用到的生命週期的方法是:
onCreatedView + onResume + onPause + onDestroyView
-
本次要用到的非生命週期的方法是:setUserVisibleHint
簡單介紹一下此方法: 當fragment被使用者可見時,setUserVisibleHint()會呼叫且傳入true值,當fragment不被使用者可見時,setUserVisibleHint()則得到false值,此方法先於生命週期方法執行
-
Fragment 主要的三個狀態:第一次可見,每次可見,每次不可見
-
對於ViewPager+Fragment使用過程中的三種情況
(1) 使用 FragmentPagerAdapter ,FragmentPagerStateAdapter不設定 setOffscreenPageLimit數,採用預設方式
(2)使用 FragmentPagerAdapter ,FragmentPagerStateAdapter設定 setOffscreenPageLimit數,設定為底部Tab總數
(3)使用 FragmentPagerAdapter ,FragmentPagerStateAdapter進入到其他頁面或者點選Home鍵,返回到桌面。
其他注意的是:使用FragmentPagerAdapter 和FragmentPagerStateAdapter的區別在於FragmentPagerStateAdapter會在Fragment不可見的時候走 detach,而FragmentPagerAdapter不會。
當然我測試用的是FragmentPagerAdapter,我們先看一看正常滑動,Fragment生命週期是怎麼走的,先寫一個BaseLazyLoadFragment類繼承自Fragment.重寫我們剛才說的生命週期的方法,首頁等Fragment繼承BaseLazyLoadFragment列印生命週期(預設進來先載入首頁):
BaseLazyLoadFragment部分程式碼如下

image.png
Fragment程式碼如下

image.png
結果如下


image.png
可以看到進入到第一個Fragment的時候,也執行了下一個Fragment的生命週期,執行了不必要的操作。 那大家有沒有發現,如果那個Fragment的狀態為可見其setUserVisibleHint的值就為true,其餘Fragment的值為false ,那我們只需要判斷,如果setUserVisibleHint的值就為true即改Fragment為可見狀態,我們就執行耗時操作,其他Fragment為false,就不執行網路請求的操作唄。 那我們寫一個公共的方法,注意此方法執行,要放到onActivityCreate()之後,否則我請求回來的資料載體控制元件的Activity都沒有建立,所以我要定義幾個變數來檢視Fragment的狀態,我們之前也說了Fragement有首次可見,可見和不可見三種狀態,程式碼如下:
View rootView; /**當前Fragment是否首次可見,預設是首次可見**/ private boolean mIsFirstVisible = true; /**當前Fragment的View是否已經建立**/ private boolean isViewCreated = false; /**當前Fragment的可見狀態,一種當前可見,一種當前不可見**/ private boolean currentVisibleState = false; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { LogUtil.e(getClass().getSimpleName(),"-----> onCreateView"); if(rootView == null){ rootView = inflater.inflate(getLayoutId(), container, false); initView(rootView); } isViewCreated=true;//在onCreateView執行完畢,將isViewCreated改為true; return rootView; }
不同生命週期變數值的變更及涉及的相關程式碼:
- onStart
@Override public void onStart() { super.onStart(); LogUtil.e(getClass().getSimpleName(),"-----> onStart"); //isHidden()是Fragment是否處於隱藏狀態和isVisible()有區別 //getUserVisibleHint(),Fragement是否可見 if(!isHidden()&& getUserVisibleHint()){//如果Fragment沒有隱藏且可見 //執行分發的方法,三種結果對應自Fragment的三個回撥,對應的操作,Fragment首次載入,可見,不可見 disPatchFragment(true); } }
- onResume
@Override public void onResume() { super.onResume(); LogUtil.e(getClass().getSimpleName(),"-----> onResume"); if(!mIsFirstVisible){ //表示點選home鍵又返回操作,設定可見狀態為ture if(!isHidden()&& !getUserVisibleHint() && currentVisibleState){ disPatchFragment(true); } } }
- onPause
@Override public void onPause() { super.onPause(); //表示點選home鍵,原來可見的Fragment要走該方法,更改Fragment的狀態為不可見 if(!isHidden()&& getUserVisibleHint()){ disPatchFragment(false); } }
- onDestroyView
@Override public void onDestroyView() { super.onDestroyView(); LogUtil.e(getClass().getSimpleName(),"-----> onStart"); //當 View 被銷燬的時候我們需要重新設定 isViewCreated mIsFirstVisible 的狀態 isViewCreated = false; mIsFirstVisible = true; }
- Fragment不同狀態對應的回撥方法
/** * * @param visible Fragment當前是否可見,然後呼叫相關方法 */ publicvoiddisPatchFragment(boolean visible){ currentVisibleState=visible; if(visible){//Fragment可見 if(mIsFirstVisible){//可見又是第一次 mIsFirstVisible=false;//改變首次可見的狀態 onFragmentFirst(); }else{//可見但不是第一次 LogUtil.e(getClass().getSimpleName(),"可見"); } }else {//不可見 LogUtil.e(getClass().getSimpleName(),"不可見"); } }; //Fragemnet首次可見的方法 public void onFragmentFirst(){ LogUtil.e(getClass().getSimpleName(),"首次可見"); }; //Fragemnet可見的方法 public void onFragmentVisble(){//子Fragment呼叫次方法,執行可見操作 LogUtil.e(getClass().getSimpleName(),"可見"); }; //Fragemnet不可見的方法 public void onFragmentInVisible(){ LogUtil.e(getClass().getSimpleName(),"不可見"); };
最後來一個總的程式碼:
public abstract class BaseLazyLoadFragment extends android.support.v4.app.Fragment {
View rootView; /**當前Fragment是否首次可見,預設是首次可見**/ private boolean mIsFirstVisible = true; /**當前Fragment的View是否已經建立**/ private boolean isViewCreated = false; /**當前Fragment的可見狀態,一種當前可見,一種當前不可見**/ private boolean currentVisibleState = false; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { LogUtil.e(getClass().getSimpleName(),"-----> onCreateView"); if(rootView == null){ rootView = inflater.inflate(getLayoutId(), container, false); initView(rootView); } isViewCreated=true;//在onCreateView執行完畢,將isViewCreated改為true; return rootView; } @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); LogUtil.e(getClass().getSimpleName(),"----->"+isVisibleToUser); if (isViewCreated) { if (isVisibleToUser && !currentVisibleState) {//Fragment可見且狀態不是可見(從一個Fragment切換到另外一個Fragment,後一個設定狀態為可見) disPatchFragment(true); } else if (!isVisibleToUser && currentVisibleState) {//Fragment不可見且狀態是可見(從一個Fragment切換到另外一個Fragment,前一個更改狀態為不可見) disPatchFragment(false); } } } /**返回子Fragment的佈局id**/ public abstractint getLayoutId(); /**初始化View的方法**/ public abstractvoid initView(View rootView); @Override public void onStart() { super.onStart(); LogUtil.e(getClass().getSimpleName(),"-----> onStart"); //isHidden()是Fragment是否處於隱藏狀態和isVisible()有區別 //getUserVisibleHint(),Fragement是否可見 if(!isHidden()&& getUserVisibleHint()){//如果Fragment沒有隱藏且可見 //執行分發的方法,三種結果對應自Fragment的三個回撥,對應的操作,Fragment首次載入,可見,不可見 disPatchFragment(true); } } @Override public void onResume() { super.onResume(); LogUtil.e(getClass().getSimpleName(),"-----> onResume"); if(!mIsFirstVisible){ //表示點選home鍵又返回操作,設定可見狀態為ture if(!isHidden()&& !getUserVisibleHint() && currentVisibleState){ disPatchFragment(true); } } } @Override public void onPause() { super.onPause(); //表示點選home鍵,原來可見的Fragment要走該方法,更改Fragment的狀態為不可見 if(!isHidden()&& getUserVisibleHint()){ disPatchFragment(false); } } @Override public void onDestroyView() { super.onDestroyView(); LogUtil.e(getClass().getSimpleName(),"-----> onStart"); //當 View 被銷燬的時候我們需要重新設定 isViewCreated mIsFirstVisible 的狀態 isViewCreated = false; mIsFirstVisible = true; } /** * * @param visible Fragment當前是否可見,然後呼叫相關方法 */ publicvoiddisPatchFragment(boolean visible){ currentVisibleState=visible; if(visible){//Fragment可見 if(mIsFirstVisible){//可見又是第一次 mIsFirstVisible=false;//改變首次可見的狀態 onFragmentFirst(); }else{//可見但不是第一次 LogUtil.e(getClass().getSimpleName(),"可見"); } }else {//不可見 LogUtil.e(getClass().getSimpleName(),"不可見"); } }; //Fragemnet首次可見的方法 public void onFragmentFirst(){ LogUtil.e(getClass().getSimpleName(),"首次可見"); }; //Fragemnet可見的方法 public void onFragmentVisble(){//子Fragment呼叫次方法,執行可見操作 LogUtil.e(getClass().getSimpleName(),"可見"); }; //Fragemnet不可見的方法 public void onFragmentInVisible(){ LogUtil.e(getClass().getSimpleName(),"不可見"); };
}
我們的Fragment只需要繼承BaseLazyLoadFragment,然後對應呼叫首次可見方法,再次可見方法,不可見方法做相應的操作就可以了。
懶載入進階
我們上面說的是一層的ViewPager加Fragment,但大家也一定遇到過Fragemgt中又來了一層ViewPager+Fragment,如圖:

那這種的怎麼辦呢?之前的方法還管用不,別急,我們先看看其生命週期列印:
納尼,InvestFragment(投資)內部第一個Tab,Invest_MineFragment竟然執行了首次可見的方法,要知道此時的InvestFragmetn都沒有顯示。
針對此問題,我的解決方法是,先判斷父Fragment如果沒有顯示,那麼不觸發我們定義的方法,程式碼如下:
/** *判斷多層巢狀的父Fragment是否顯示 */ private boolean isParentFragmentInvisible() { //獲取父Fragment的狀態 Fragment parentFragment = getParentFragment(); if (parentFragment instanceof BaseLazyLoadFragment ) { BaseLazyLoadFragment fragment = (BaseLazyLoadFragment) parentFragment; //返回父Fragment的狀態 return fragment.getCurrentVisibleState(); }else { return false; } } private boolean getCurrentVisibleState() { return currentVisibleState; }
ok,我們在執行一下,列印如下:

image.png
感覺沒啥問題?
別急,還有剩餘部分列印資訊:

image.png
也就是我們還需要一個第一個子Fragment的狀態資訊:解決思路如下:
由於父Fragment的執行在子Fragment之前,所以,當我們在父 Fragment 分發完成自己的可見事件後,讓子 Fragment 再次呼叫自己的可見事件分發方法,這次我們上 isParentFragmentVsible() 返回 false ,可見狀態將會正確分發了,有點類似於父類完成後,又呼叫方法重新整理子類
。
/** * * @param visible Fragment當前是否可見,然後呼叫相關方法 */ publicvoiddisPatchFragment(boolean visible){ String aa =getClass().getSimpleName(); //如果父Fragment不可見,則不向下分發給子Fragment if(visible && isParentFragmentVsible())return; currentVisibleState=visible; if(visible){//Fragment可見 if(mIsFirstVisible){//可見又是第一次 mIsFirstVisible=false;//改變首次可見的狀態 onFragmentFirst(); }//可見但不是第一次 onFragmentVisble(); //可見狀態的時候內層 fragment 生命週期晚於外層 所以在 onFragmentResume 後分發 dispatchChildFragmentVisibleState(true); }else {//不可見 onFragmentInVisible(); dispatchChildFragmentVisibleState(false); } }; /** * 重新分發給子Fragment * @param visible */ private void dispatchChildFragmentVisibleState(boolean visible) { FragmentManager childFragmentManager = getChildFragmentManager(); @SuppressLint("RestrictedApi") List<Fragment> fragments = childFragmentManager.getFragments(); if(fragments != null){ if (!fragments.isEmpty()) { for (Fragment child : fragments) { if (child instanceof BaseLazyLoadFragment && !child.isHidden() && child.getUserVisibleHint()) { ((BaseLazyLoadFragment) child).disPatchFragment(visible); } } } } }
這樣就ok了,然後我們點選home鍵:

這又是什麼問題,Invest_YourFragment為什麼走了兩邊?
原因處在順序呼叫上,我剛才說了: 父 Fragment總是優先於子 Fragment,而對於不可見事件,內部的 Fragment 生命週期總是先於外層 Fragment。回到我們程式碼裡:父Fragment呼叫自身的 disPatchFragment方法分發了不可見事件,又會再次呼叫 dispatchChildFragmentVisibleState ,導致子 Fragment 再次呼叫自己的 disPatchFragment再次呼叫了一次 不可見事件onFragmentInVisible ,故產生了兩次。
解決辦法就是我們之前定義的變數:currentVisibleState,如果當前的 Fragment 要分發的狀態與 currentVisibleState 相同我們就沒有必要去做分發了。
程式碼及新增位置如下:
``

image.png
``
最後附上總程式碼,編寫Fragment時,只需要繼承該類,然後呼叫可見的方法就好了。
public abstract class BaseLazyLoadFragment extends Fragment { View rootView; /**當前Fragment是否首次可見,預設是首次可見**/ private boolean mIsFirstVisible = true; /**當前Fragment的View是否已經建立**/ private boolean isViewCreated = false; /**當前Fragment的可見狀態,一種當前可見,一種當前不可見**/ private boolean currentVisibleState = false; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { LogUtil.e(getClass().getSimpleName(),"-----> onCreateView"); if(rootView == null){ rootView = inflater.inflate(getLayoutId(), container, false); initView(rootView); } isViewCreated=true;//在onCreateView執行完畢,將isViewCreated改為true; return rootView; } @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if (isViewCreated) { if (isVisibleToUser && !currentVisibleState) {//Fragment可見且狀態不是可見(從一個Fragment切換到另外一個Fragment,後一個設定狀態為可見) disPatchFragment(true); } else if (!isVisibleToUser && currentVisibleState) {//Fragment不可見且狀態是可見(從一個Fragment切換到另外一個Fragment,前一個更改狀態為不可見) disPatchFragment(false); } } } /**返回子Fragment的佈局id**/ public abstractint getLayoutId(); /**初始化View的方法**/ public abstractvoid initView(View rootView); @Override public void onStart() { super.onStart(); //isHidden()是Fragment是否處於隱藏狀態和isVisible()有區別 //getUserVisibleHint(),Fragement是否可見 if(!isHidden()&& getUserVisibleHint()){//如果Fragment沒有隱藏且可見 //執行分發的方法,三種結果對應自Fragment的三個回撥,對應的操作,Fragment首次載入,可見,不可見 disPatchFragment(true); } } @Override public void onResume() { super.onResume(); if(!mIsFirstVisible){ //表示點選home鍵又返回操作,設定可見狀態為ture if(!isHidden()&& !getUserVisibleHint() && currentVisibleState){ disPatchFragment(true); } } } @Override public void onPause() { super.onPause(); //表示點選home鍵,原來可見的Fragment要走該方法,更改Fragment的狀態為不可見 if(!isHidden()&& getUserVisibleHint()){ disPatchFragment(false); } } @Override public void onDestroyView() { super.onDestroyView(); //當 View 被銷燬的時候我們需要重新設定 isViewCreated mIsFirstVisible 的狀態 isViewCreated = false; mIsFirstVisible = true; } /** * * @param visible Fragment當前是否可見,然後呼叫相關方法 */ publicvoiddisPatchFragment(boolean visible){ String aa =getClass().getSimpleName(); //如果父Fragment不可見,則不向下分發給子Fragment if(visible && isParentFragmentVsible())return; // 如果當前的 Fragment 要分發的狀態與 currentVisibleState 相同我們就沒有必要去做分發了。 if (currentVisibleState == visible) { return; } currentVisibleState=visible; if(visible){//Fragment可見 if(mIsFirstVisible){//可見又是第一次 mIsFirstVisible=false;//改變首次可見的狀態 onFragmentFirst(); }//可見但不是第一次 onFragmentVisble(); //可見狀態的時候內層 fragment 生命週期晚於外層 所以在 onFragmentResume 後分發 dispatchChildFragmentVisibleState(true); }else {//不可見 onFragmentInVisible(); dispatchChildFragmentVisibleState(false); } }; /** * 重新分發給子Fragment * @param visible */ private void dispatchChildFragmentVisibleState(boolean visible) { FragmentManager childFragmentManager = getChildFragmentManager(); @SuppressLint("RestrictedApi") List<Fragment> fragments = childFragmentManager.getFragments(); if(fragments != null){ if (!fragments.isEmpty()) { for (Fragment child : fragments) { if (child instanceof BaseLazyLoadFragment && !child.isHidden() && child.getUserVisibleHint()) { ((BaseLazyLoadFragment) child).disPatchFragment(visible); } } } } } //Fragemnet首次可見的方法 public void onFragmentFirst(){ LogUtil.e(getClass().getSimpleName(),"首次可見"); }; //Fragemnet可見的方法 public void onFragmentVisble(){//子Fragment呼叫次方法,執行可見操作 LogUtil.e(getClass().getSimpleName(),"可見"); }; //Fragemnet不可見的方法 public void onFragmentInVisible(){ LogUtil.e(getClass().getSimpleName(),"不可見"); }; /** *判斷多層巢狀的父Fragment是否顯示 */ private boolean isParentFragmentVsible() { BaseLazyLoadFragment fragment = (BaseLazyLoadFragment) getParentFragment(); return fragment != null && !fragment.getCurrentVisibleState(); } private boolean getCurrentVisibleState() { return currentVisibleState; } }
完畢!