您好,登錄后才能下訂單哦!
前言
前段時間在自己的練習項目中想用到懶加載機制,查看了大多數資料只介紹了在 View Pager
+ Fragment
組合的情況下實現的懶加載,但是現在大多數App更多的是 Fragmentmanager
去管理主頁面多個 Fragment
的顯示與隱藏,然后主界面的某個或多個 Fragment
里又嵌套了多個 Fragment
+ ViewPager
(詳細見下圖 ),對于這種情況,適用于第一種的方式是不能直接解決第二種的情況的,所以寫下這篇文章,記錄一下踩的幾個坑,希望對同像我一樣的初學者提供一種思考方式作為參考(如果有錯誤或者不合適的地方,希望各位前輩能在評論區指出,非常感謝!)。
關于懶加載
1. 什么是懶加載?
懶加載也叫延遲加載,在APP中指的是每次只加載當前頁面,是一種很好的優化APP性能的一種方式。
2.為什么要用懶加載?
優化APP性能,提升用戶體驗 :如果用戶打開某頁面,就會去預加載其它的頁面時,數據集較小或者網絡性能較優時還好,但是如果數據集過大或者網絡性能不佳時,就會造成用戶等待的時間較長,APP界面產生明顯的滯頓感的情況,嚴重影響到用戶的體驗。
減少無效資源的加載,減少服務器的壓力,節省用戶流量 :如果用戶只想瀏覽或者經常瀏覽某個特定的頁面,如果使用預加載的方式,就會造成資源浪費,增加服務器的壓力等。
實現懶加載
1.ViewPager+Fragment情況
1.1遇到的問題
在我們平時開發中,經常使用 ViewPager+Fragment
的組合來實現左右滑動的頁面設計(如上圖),但是 ViewPger
有個 預加載 機制,默認會把 ViewPager
當前位置的左右相鄰頁面預先初始化(俗稱預加載),即使設置 setOffscreenPageLimit(0)
也無效果,也會預加載。通過點進源碼中發現,如果不主動設置 setOffscreenPageLimit()
方法, mOffscreenPageLimit
默認值為1,即使設置了0(小于1)的值了,但是還會按照 mOffscreenPageLimit=limit=1
處理。
private int mOffscreenPageLimit = 1;//即使不設置,默認值就為1 public int getOffscreenPageLimit() { return this.mOffscreenPageLimit; } public void setOffscreenPageLimit(int limit) { if (limit < 1) {//設置為0,還是會默認為1 Log.w("ViewPager", "Requested offscreen page limit " + limit + " too small; defaulting to " + 1); limit = 1; } if (limit != this.mOffscreenPageLimit) { this.mOffscreenPageLimit = limit; this.populate(); }
1.2 解決思路
Fragment
有一個非生命周期的 setUserVisibleHint(boolean isVisibleToUser)
回調方法, 當 ViewPager
嵌套 Fragment
時會起作用 ,如果切換 ViewPager
則該方法也會被調用,參數 isVisibleToUser
為 true
代表當前 Fragment
對用戶可見,否則不可見。 所以最簡單的思路: Fragment
可見時才去加載數據,不可見時就不讓它加載數據 。據我們創建抽象 BaseFragment
,對其進行封裝。首先我們引入 isVisibleToUser
變量,負責保存當前 Fragment
對用戶的可見狀態。 同時還有幾個值得注意的地方:
setUserVisibleHint(boolean isVisibleToUser)
方法的回調時機并沒有與 Fragment
的生命周期有確切的關聯,比如說,回調時機有可能在 onCreateView()
方法之后,也可能在 onCreateView()
方法之前。因此,必須引入一個標志位 isPrepareView
判斷view是否創建完成,不然,很容易會造成空指針異常。我們初始化該變量為 false
,在 onViewCreated()
中,也就是view創建完成后,將其賦值為 true
。
數據初始化只應該加載一次,因此,引入第二個標志位, isInitData
,初始為 false,
在數據加載完成之后,將其賦值為 true
,下次返回此頁面時不會再自動加載。至此,我們的懶加載方法考慮了所有條件。也就是當 isVisibleToUser
為 true
, isInitData
為 false
, isPrepareView
為 true
時,進行數據加載,并且加載后為了防止重復調用,將 isInitData
賦值為 true
。
將懶加載數據提取成一個方法,那么這個方法該何時調用呢?首先 setUserVisibleHint(boolean isVisibleToUser)
方法中是必須調用的,即當 Fragment
由可見變為不可見和不可見變為可見時回調。 其次,很容易忽略的一點。對于第一個 Fragment
,如果 setUserVisibleHint(boolean isVisibleToUser )
方法在 onCreateView()
之前調用的話,如果懶加載方法只在 setUserVisibleHint(boolean isVisibleToUser )
中調用,那么該 Fragment
將只能在被主動切換一次之后才能加載數據,這肯定是不可能的,因此,我們需要在view創建完成之后,也進行一次調用。思來想去,在 onActivityCreated()
方法中是最合適的。我們在繼承的時候,在 onViewCreated()
方法中進行一些初始化就行了,這樣不會引起沖突。
1.3 BaseFragment代碼實現
public abstract class BaseFragment extends Fragment { private Boolean isInitData = false; //標志位,判斷數據是否初始化 private Boolean isVisibleToUser = false; //標志位,判斷fragment是否可見 private Boolean isPrepareView = false; //標志位,判斷view已經加載完成 避免空指針操作 @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(getLayoutId(),container,false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); isPrepareView=true;//此時view已經加載完成,設置其為true } /** * 懶加載方法 */ public void lazyInitData(){ if(!isInitData && isVisibleToUser && isPrepareView){//如果數據還沒有被加載過,并且fragment已經可見,view已經加載完成 initData();//加載數據 isInitData=true;//是否已經加載數據標志重新賦值為true } } @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); this.isVisibleToUser=isVisibleToUser;//將fragment是否可見值賦給標志isVisibleToUser lazyInitData();//懶加載 } /** * fragment生命周期中onViewCreated之后的方法 在這里調用一次懶加載 避免第一次可見不加載數據 * @param savedInstanceState */ @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); lazyInitData();//懶加載 } /** * 由子類實現 * @return 返回子類的布局id */ abstract int getLayoutId(); /** * 加載數據的方法,由子類實現 */ abstract void initData(); }
2.Fragment+ViewPager+Fragment情況
2.1 遇到的問題
如圖2,對于這種由 Fragmentmanager
管理主頁面的多個 Fragment
的顯示與隱藏,在其中的某個 Fragment
中又嵌套了多個 Fragment
的情況( 如上圖 ),上面的方案是無法解決的,如果主頁面的 Fragment
直接繼承上面的 BaseFragment
,就會出現主頁的幾個 Fragment
都不會加載的現象,為什么會這樣呢,按道理說 Fragment
應該可見了,加載數據的判斷邏輯應該沒問題啊,而且上面那個demo也跑成功了。最終我發現,問題出在 setUserVisibleHint()
這個方法上,點進去它的源碼發現注釋中有這么一句話:
This may be used by the system to prioritize operations such as fragment lifecycle updates or loader ordering behavior.
也就是說這個可能被用來在一組有序的 Fragment
里 ,例如 Fragment
生命周期的更新。告訴我們這個方法被調用希望在一個pager里 ,因此 FragmentPagerAdapter
所以可以使用這個,而主頁面的幾個 Fragment
我們是通過 Fragmentmanager
管理的,所以 setUserVisibleHint()
是不會被調用,而我們設置的 isVisibleToUser=false
默認值一直不會變,那么 lazyInitData()
方法也就一直不會執行。
/** * 懶加載方法 */ public void lazyInitData(){ if(!isInitData && isVisibleToUser && isPrepareView){//因為isVisibleToUser一直都是false,所以iniData()是不會被執行的 initData();//加載數據 isInitData=true; } }
2.2 解決思路
這里我的處理方式是,在lazyInitData()中多加了一段處理邏輯,如下:
/** * 懶加載方法 */ public void lazyInitData(){ if(!isInitData && isVisibleToUser && isPrepareView){//如果數據還沒有被加載過,并且fragment已經可見,view已經加載完成 initData();//加載數據 isInitData=true;//是否已經加載數據標志重新賦值為true }else if (!isInitData && getParentFragment()==null && isPrepareView){ initData(); isInitData=true; } } /** * Fragment顯示隱藏監聽 * @param hidden */ @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); if (!hidden) { lazyInitData(); } }
對于主頁面的多個 Fragment
只會在第二個判斷邏輯處理(因為它的 isVisibleToUser
值一直等于 false
),對于嵌套的 Fragment
只會經過第一個處理邏輯(因為它的 getParentFragment()!=null
),然后通過 onHiddenChanged()
方法去加載 lazyInitData()
方法,這樣以來就能處理這種情況了。
但是這時候又會出現一個問題,如果一個APP里第一種,第二種情況并存的話,這段代碼又不適合第一種情況了,因為對于第一種的情況當判定 isVisibleToUser
為 false
時,雖然不走第一個處理邏輯,但是它的 getParentFragment()
一直是等于 null
的,那么它就會走第二個判斷邏輯,這樣又會預加載了。
對于這種情況,我的處理方式:給每個Fragment設置一個標志值,當是第一種情況時,設為true,第二種情況時,設置false,然后再分別處理相應的判斷邏輯。代碼如下:
/** * 懶加載方法 */ public void lazyInitData(){ if(setFragmentTarget()){ if(!isInitData && isVisibleToUser && isPrepareView){//如果數據還沒有被加載過,并且fragment已經可見,view已經加載完成 initData();//加載數據 isInitData=true;//是否已經加載數據標志重新賦值為true } }else { if(!isInitData && isVisibleToUser && isPrepareView){//如果數據還沒有被加載過,并且fragment已經可見,view已經加載完成 initData();//加載數據 isInitData=true;//是否已經加載數據標志重新賦值為true }else if (!isInitData && getParentFragment()==null && isPrepareView ){ initData(); isInitData=true; } } } /** * 設置Fragment target,由子類實現 */ abstract boolean setFragmentTarget();
經過這樣的處理之后,第一種情況和第二種情況,或兩者并存的情況下都能保證在繼承一個base下,實現懶加載。
2.3 BaseFragmentTwo最終代碼實現
public abstract class BaseFragmentTwo extends Fragment { private Boolean isInitData = false; //標志位,判斷數據是否初始化 private Boolean isVisibleToUser = false; //標志位,判斷fragment是否可見 private Boolean isPrepareView = false; //標志位,判斷view已經加載完成 避免空指針操作 @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(getLayoutId(),container,false); } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); isPrepareView=true;//此時view已經加載完成,設置其為true } /** * 懶加載方法 */ public void lazyInitData(){ if(setFragmentTarget()){ if(!isInitData && isVisibleToUser && isPrepareView){//如果數據還沒有被加載過,并且fragment已經可見,view已經加載完成 initData();//加載數據 isInitData=true;//是否已經加載數據標志重新賦值為true } }else { if(!isInitData && isVisibleToUser && isPrepareView){//如果數據還沒有被加載過,并且fragment已經可見,view已經加載完成 initData();//加載數據 isInitData=true;//是否已經加載數據標志重新賦值為true }else if (!isInitData && getParentFragment()==null && isPrepareView ){ initData(); isInitData=true; } } } @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); if (!hidden) { lazyInitData(); } } @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); this.isVisibleToUser=isVisibleToUser;//將fragment是否可見值賦給標志isVisibleToUser lazyInitData();//加載懶加載 } /** * fragment生命周期中onViewCreated之后的方法 在這里調用一次懶加載 避免第一次可見不加載數據 * @param savedInstanceState */ @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); lazyInitData(); } /** * 由子類實現 * @return 返回子類的布局id */ abstract int getLayoutId(); /** * 加載數據的方法,由子類實現 */ abstract void initData(); /** * 設置Fragment target,由子類實現 */ abstract boolean setFragmentTarget(); }
其它需要注意:
①給 viewpager
設置 adapter
時,一定要傳入 getChildFragmentManager()
,否則 getParentFragment()
將會一直等于 null
,這會影響 lazyInitData()
的判斷,導致懶加載出現混亂甚至無效的情況。
②demo中我使用的是 ViewPager+Tablayout
的組合方式,在使用 Tablayout
時一定要保證 styles.xml
中的主題應該使用 Theme.AppCompat.Light.NoActionBar
或者 Theme.AppCompat.Light
等 Theme.AppCompat.XXX
的主題。
項目地址
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。