前段時間在本身的練習項目中想用到懶加載機制,查看了大多數資料只介紹了在
View Pager
+Fragment
組合的狀況下實現的懶加載,可是如今大多數App更多的是Fragmentmanager
去管理主頁面多個Fragment
的顯示與隱藏,而後主界面的某個或多個Fragment
裏又嵌套了多個Fragment
+ViewPager
(詳細見下圖),對於這種狀況,適用於第一種的方式是不能直接解決第二種的狀況的,因此寫下這篇文章,記錄一下踩的幾個坑,但願對同像我同樣的初學者提供一種思考方式做爲參考(若是有錯誤或者不合適的地方,但願各位前輩能在評論區指出,很是感謝!)。git
懶加載也叫延遲加載,在APP中指的是每次只加載當前頁面,是一種很好的優化APP性能的一種方式。github
在咱們平時開發中,常用
ViewPager+Fragment
的組合來實現左右滑動的頁面設計(如上圖),可是ViewPger
有個預加載機制,默認會把ViewPager
當前位置的左右相鄰頁面預先初始化(俗稱預加載),即便設置setOffscreenPageLimit(0)
也無效果,也會預加載。經過點進源碼中發現,若是不主動設置setOffscreenPageLimit()
方法,mOffscreenPageLimit
默認值爲1,即便設置了0(小於1)的值了,可是還會按照mOffscreenPageLimit=limit=1
處理。bash
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();
}
複製代碼
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
。ide
將懶加載數據提取成一個方法,那麼這個方法該什麼時候調用呢?首先 setUserVisibleHint(boolean isVisibleToUser)
方法中是必須調用的,即當Fragment
由可見變爲不可見和不可見變爲可見時回調。 其次,很容易忽略的一點。對於第一個Fragment
,若是setUserVisibleHint(boolean isVisibleToUser )
方法在onCreateView()
以前調用的話,若是懶加載方法只在setUserVisibleHint(boolean isVisibleToUser )
中調用,那麼該Fragment
將只能在被主動切換一次以後才能加載數據,這確定是不可能的,所以,咱們須要在view建立完成以後,也進行一次調用。思來想去,在onActivityCreated()
方法中是最合適的。咱們在繼承的時候,在onViewCreated()
方法中進行一些初始化就好了,這樣不會引發衝突。佈局
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,對於這種由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;
}
}
複製代碼
這裏個人處理方式是,在lazyInitData()中多加了一段處理邏輯,以下:ui
/**
* 懶加載方法
*/
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下,實現懶加載。
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
的主題。