Android在多種設計下實現懶加載機制

前言


前段時間在本身的練習項目中想用到懶加載機制,查看了大多數資料只介紹了在View Pager+Fragment組合的狀況下實現的懶加載,可是如今大多數App更多的是Fragmentmanager去管理主頁面多個Fragment的顯示與隱藏,而後主界面的某個或多個Fragment裏又嵌套了多個Fragment+ViewPager(詳細見下圖),對於這種狀況,適用於第一種的方式是不能直接解決第二種的狀況的,因此寫下這篇文章,記錄一下踩的幾個坑,但願對同像我同樣的初學者提供一種思考方式做爲參考(若是有錯誤或者不合適的地方,但願各位前輩能在評論區指出,很是感謝!)。git

關於懶加載


1. 什麼是懶加載?

懶加載也叫延遲加載,在APP中指的是每次只加載當前頁面,是一種很好的優化APP性能的一種方式。github

2.爲何要用懶加載?

  • 優化APP性能,提高用戶體驗:若是用戶打開某頁面,就會去預加載其它的頁面時,數據集較小或者網絡性能較優時還好,可是若是數據集過大或者網絡性能不佳時,就會形成用戶等待的時間較長,APP界面產生明顯的滯頓感的狀況,嚴重影響到用戶的體驗。
  • 減小無效資源的加載,減小服務器的壓力,節省用戶流量:若是用戶只想瀏覽或者常常瀏覽某個特定的頁面,若是使用預加載的方式,就會形成資源浪費,增長服務器的壓力等。

實現懶加載


1.ViewPager+Fragment狀況

1.1遇到的問題

在咱們平時開發中,常用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();
        }
複製代碼

1.2 解決思路

Fragment 有一個非生命週期的setUserVisibleHint(boolean isVisibleToUser)回調方法,ViewPager 嵌套 Fragment 時會起做用,若是切換 ViewPager 則該方法也會被調用,參數isVisibleToUsertrue表明當前 Fragment 對用戶可見,不然不可見。因此最簡單的思路:Fragment可見時纔去加載數據,不可見時就不讓它加載數據。據咱們建立抽象BaseFragment,對其進行封裝。首先咱們引入isVisibleToUser變量,負責保存當前Fragment對用戶的可見狀態。同時還有幾個值得注意的地方:服務器

  • setUserVisibleHint(boolean isVisibleToUser)方法的回調時機並無與Fragment的生命週期有確切的關聯,好比說,回調時機有可能在onCreateView()方法以後,也可能在onCreateView()方法以前。所以,必須引入一個標誌位isPrepareView判斷view是否建立完成,否則,很容易會形成空指針異常。咱們初始化該變量爲false,在onViewCreated()中,也就是view建立完成後,將其賦值爲true網絡

  • 數據初始化只應該加載一次,所以,引入第二個標誌位,isInitData,初始爲false,在數據加載完成以後,將其賦值爲true,下次返回此頁面時不會再自動加載。至此,咱們的懶加載方法考慮了全部條件。也就是當isVisibleToUsertrueisInitDatafalseisPrepareViewtrue時,進行數據加載,而且加載後爲了防止重複調用,將isInitData賦值爲trueide

  • 將懶加載數據提取成一個方法,那麼這個方法該什麼時候調用呢?首先 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()中多加了一段處理邏輯,以下: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裏第一種,第二種狀況並存的話,這段代碼又不適合第一種狀況了,由於對於第一種的狀況當斷定isVisibleToUserfalse時,雖然不走第一個處理邏輯,可是它的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.LightTheme.AppCompat.XXX的主題。

項目地址


項目地址

相關文章
相關標籤/搜索