ViewPager懶加載極致優化

目錄介紹

  • 01.ViewPager簡單介紹
  • 02.ViewPager弊端分析
  • 03.ViewPager預加載
  • 04.ViewPager部分源碼
  • 05.懶加載出現問題
  • 06.如何實現預加載機制
  • 07.懶加載配合狀態管理器

好消息

  • 博客筆記大彙總【16年3月到至今】,包括Java基礎及深刻知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug彙總,固然也在工做之餘收集了大量的面試題,長期更新維護而且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連接地址:github.com/yangchong21…
  • 若是以爲好,能夠star一下,謝謝!固然也歡迎提出建議,萬事起於忽微,量變引發質變!

01.ViewPager簡單介紹

  • ViewPager使用一個鍵對象來關聯每一頁,而不是管理View。這個鍵用於追蹤和惟一標識在adapter中獨立位置中的一頁。調用方法startUpdate(ViewGroup)代表ViewPager中的內容須要更改。
  • 經過調用一次或屢次調用instantiateItem(ViewGroup, int)來構造頁面視圖。
  • 調用destroyItem(ViewGroup, int, Object)來取消ViewPager關聯的頁面視圖。
  • 最後,當一次更新(添加和/或移除)完成以後將會調用finishUpdate(ViewGroup)來通知adapter, 提交關聯和/或取消關聯的操做。這三個方法就是用於ViewPager使用回調的方式來通知PagerAdapter來管理其中的頁面。
  • 一個很是簡單的方式就是使用每頁視圖做爲key來關聯它們本身,在方法instantiateItem(ViewGroup, int)中建立和添加它們到ViewGroup以後,返回該頁視圖。與之相匹配的方法destroyItem(ViewGroup, int, Object)實現從ViewGroup中移除視圖。固然必須在isViewFromObject(View, Object)中這樣實現:return view == object;.
  • PagerAdapter支持數據改變時刷新界面,數據改變必須在主線程中調用,並在數據改變完成後調用方法notifyDataSetChanged(), 和AdapterView中派生自BaseAdapter類似。一次數據的改變可能關聯着頁面的添加、移除、或改變位置。ViewPager將根據adapter中實現getItemPosition(Object)方法返回的結果,來判斷是否保留當前已經構造的活動頁面(即重用,而不徹底自行構造)。

02.ViewPager弊端分析

  • 普通的viewpager若是你不使用setoffscreenpagelimit(int limit)這個方法去設置默認加載數的話是會默認加載頁面的左右兩頁的,也就是說當你進入viewpager第一頁的時候第二頁和第一頁是會被一塊兒加載的,這樣同時加載就會形成一些問題,試想咱們若是設置了setoffscreenpagelimit爲3的話,那麼進入viewpager之後就會同時加載4個fragment,像咱們平時的項目中在這些fragment中通常都是會發送網絡請求的,也就是說咱們有4個fragment同時發送網絡請求去獲取數據,這樣的結果顯而易見給用戶的體驗是很差的(如:浪費用戶流量,形成卡頓等等)。
  • 懶加載的實現弊端
    • 概念:當須要時才加載,加載以後一直保持該對象。
    • 而關於Fragment實現的PagerAdapter都沒有徹底保存其引用和狀態。FragmentPageAdapter須要重建視圖,FragmentStatePageAdapter使用狀態恢復,View都被銷燬,可是恢復的方式不一樣,而一般咱們想獲得的結果是,Fragment一旦被加載,其視圖也不會被銷燬,即不會再從新走一遍生命週期。並且ViewPager爲了實現滑動效果,都是預加載左右兩側的頁面。
    • 咱們一般想要實現的兩種效果:不提供滑動,須要時才構造,而且只走一遍生命週期,避免在Fragment中作過多的狀態保存和恢復。

03.ViewPager預加載

  • ViewPager的預加載機制。那麼,咱們可不能夠設置ViewPager的預加載爲0,不就解決問題了嗎?也就是代碼這樣操做:
    vp.setOffscreenPageLimit(0);
    複製代碼
  • 而後看一下源碼
    • 即便你設置爲0,那麼仍是會在裏面判斷後設爲默認值1。因此這個方法是行不通的。
    public void setOffscreenPageLimit(int limit) {
        if (limit < 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();
        }
    
    }
    複製代碼
  • ViewPager默認狀況下的加載,當切換到當前頁面時,會默認預加載左右兩側的佈局到ViewPager中,儘管兩側的View並不可見的,咱們稱這種狀況叫預加載;因爲ViewPager對offscreenPageLimit設置了限制,頁面的預加載是不可避免……
  • 初始化緩存(mOffscreenPageLimit == 1)
    • 當初始化時,當前顯示頁面是第0頁;mOffscreenPageLimit爲1,因此預加載頁面爲第1頁,再日後的頁面就不須要加載了(這裏的2, 3, 4頁)
    • image
  • 中間頁面緩存(mOffscreenPageLimit == 1)
    • 當向右滑動到第2頁時,左右分別須要緩存一頁,第0頁就須要銷燬掉,第3頁須要預加載,第4頁不須要加載
    • image

04.ViewPager部分源碼

  • ViewPager.setAdapter方法
    • 銷燬舊的Adapter數據,用新的Adaper更新UI
    • 清除舊的Adapter,對已加載的item調用destroyItem,
    • 將自身滾動到初始位置this.scrollTo(0, 0)
    • 設置PagerObserver: mAdapter.setViewPagerObserver(mObserver);
    • 調用populate()方法計算並初始化View(這個方法後面會詳細介紹)
    • 若是設置了OnAdapterChangeListener,進行回調
  • ViewPager.populate(int newCurrentItem)
    • 該方法是ViewPager很是重要的方法,主要根據參數newCurrentItem和mOffscreenPageLimit計算出須要初始化的頁面和須要銷燬頁面,而後經過調用Adapter的instantiateItem和destroyItem兩個方法初始化新頁面和銷燬不須要的頁面!
    • 根據newCurrentItem和mOffscreenPageLimit計算要加載的page頁面,計算出startPos和endPos
    • 根據startPos和endPos初始化頁面ItemInfo,先從緩存裏面獲取,若是沒有就調用addNewItem方法,實際調用mAdapter.instantiateItem
    • 將不須要的ItemInfo移除: mItems.remove(itemIndex),並調用mAdapter.destroyItem方法
    • 設置LayoutParams參數(包括position和widthFactor),根據position排序待繪製的View列表:mDrawingOrderedChildren,重寫了getChildDrawingOrder方法
    • 最後一步獲取當前顯示View的焦點:currView.requestFocus(View.FOCUS_FORWARD)
  • ViewPager.dataSetChanged()
    • 當調用Adapter的notifyDataSetChanged時,會觸發這個方法,該方法會從新計算當前頁面的position,
    • 移除須要銷燬的頁面的ItemInfo對象,而後再調用populate方法刷新頁面
    • 循環mItems(每一個page對應的ItemInfo對象),調用int newPos = mAdapter.getItemPosition方法
    • 當newPos等於PagerAdapter.POSITION_UNCHANGED表示當前頁面不須要更新,不用銷燬,當newPos等於PagerAdapter.POSITION_NONE時,須要更新,移除item,調用mAdapter.destroyItem
    • 循環完成後,最後計算出顯示頁面的newCurrItem,調用setCurrentItemInternal(newCurrItem, false, true)方法更新UI(實際調用populate方法從新計算頁面信息)
  • ViewPager.scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected)
    • 滑動到指定頁面,內部會觸發OnPageChangeListener
  • ViewPager.calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)
    • 這個方法主要用於計算每一個頁面對應ItemInfo的offset變量,這個變量用於記錄當前view在全部緩存View中(包含當前顯示頁)的索引,用於佈局的時候計算該View應該放在哪一個位置
    • 在populate方法中更新完頁面數據後,會調用該方法計算全部頁面的offset

05.懶加載出現問題

  • 發現Fragment中有一個setUserVisibleHint(boolean isVisibleToUser)方法,這個方法就是告訴用戶,UI對用戶是否可見,能夠作懶加載初始化操做。
    • 由於ViewPager會加載好多Fragment,爲了節省內容等會在Fragment不可見的某個時候調用onDestroyView()將用戶界面銷燬掉可是Fragment的實例還在,因此可能第一次加載沒有問題,可是再次回到第一個Fragment再去加載的時候就會出現UI對用戶可見可是視圖尚未初始化。
  • 懶加載須要處理的幾個問題
    • 預加載,雖然沒有顯示在界面上,可是當前頁面的上一頁和下一頁的Fragment已經執行了一個Fragment可以顯示在界面上的全部生命週期方法,可是咱們想在跳轉到該頁時才真正構造數據視圖和請求數據。那麼咱們可使用一個佔位視圖,那麼能夠想到使用ViewStub,當真正跳轉到該頁時,執行ViewStub.inflate()方法,加載真正的數據視圖和請求數據。
  • 視圖保存
    • 當某一頁超出可視範圍和預加載範圍,那麼它將會被銷燬,FragmentStatePagerAdapter銷燬整個Fragment, 咱們能夠本身保存該Fragment,或使用FragmentPagerAdapter讓FragmentTransition來保留Fragment的引用。雖然這樣,可是它的週期方法已經走完,那麼咱們只能手動的保存Fragment根View的引用,當再次從新進入新的聲明週期方法時,返回原來的View
  • 是否已經被用戶所看到
    • 其實自己而言,FragmentManager並無提供爲Fragment被用戶所看到的回調方法,而是在FragmentPagerAdapter和FragmentStatePagerAdapter中,調用了Fragment.setUserVisibleHint(boolean)來代表Fragment是否已經被做爲primaryFragment. 因此這個方法能夠被認爲是一個回調方法。

06.如何實現預加載機制

  • 主要的方法是Fragment中的setUserVisibleHint(),此方法會在onCreateView()以前執行,當viewPager中fragment改變可見狀態時也會調用,當fragment 從可見到不見,或者從不可見切換到可見,都會調用此方法,使用getUserVisibleHint() 能夠返回fragment是否可見狀態。
  • 在BaseLazyFragment中須要在onActivityCreated()及setUserVisibleHint()方法中都調了一次lazyLoad() 方法。若是僅僅在setUserVisibleHint()調用lazyLoad(),當默認首頁首先加載時會致使viewPager的首頁第一次展現時沒有數據顯示,切換一下才會有數據。由於首頁fragment的setUserVisible()在onActivityCreated() 以前調用,此時isPrepared爲false 致使首頁fragment 沒能調用onLazyLoad()方法加載數據。
    /**
     * <pre>
     *     @author yangchong
     *     blog  : https://github.com/yangchong211
     *     time  : 2017/7/22
     *     desc  : 懶加載
     *     revise: 懶加載時機:onCreateView()方法執行完畢 + setUserVisibleHint()方法返回true
     * </pre>
     */
    public abstract class BaseLazyFragment extends BaseFragment {
    
        /*
         * 預加載頁面回調的生命週期流程:
         * setUserVisibleHint() -->onAttach() --> onCreate()-->onCreateView()-->
         *              onActivityCreate() --> onStart() --> onResume()
         */
    
        /**
         * 懶加載過
         */
        protected boolean isLazyLoaded = false;
        /**
         * Fragment的View加載完畢的標記
         */
        private boolean isPrepared = false;
    
        /**
         * 第一步,改變isPrepared標記
         * 當onViewCreated()方法執行時,代表View已經加載完畢,此時改變isPrepared標記爲true,並調用lazyLoad()方法
         */
        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            isPrepared = true;
            //只有Fragment onCreateView好了
            //另外這裏調用一次lazyLoad()
            lazyLoad();
        }
    
    
        /**
         * 第二步
         * 此方法會在onCreateView()以前執行
         * 當viewPager中fragment改變可見狀態時也會調用
         * 當fragment 從可見到不見,或者從不可見切換到可見,都會調用此方法
         * true表示當前頁面可見,false表示不可見
         */
        @Override
        public void setUserVisibleHint(boolean isVisibleToUser) {
            super.setUserVisibleHint(isVisibleToUser);
            LogUtil.d("setUserVisibleHint---"+isVisibleToUser);
            //只有當fragment可見時,才進行加載數據
            if (isVisibleToUser){
                lazyLoad();
            }
        }
    
        /**
         * 調用懶加載
         * 第三步:在lazyLoad()方法中進行雙重標記判斷,經過後便可進行數據加載
         */
        private void lazyLoad() {
            if (getUserVisibleHint() && isPrepared && !isLazyLoaded) {
                showFirstLoading();
                onLazyLoad();
                isLazyLoaded = true;
            } else {
                //當視圖已經對用戶不可見而且加載過數據,若是須要在切換到其餘頁面時中止加載數據,能夠覆寫此方法
                if (isLazyLoaded) {
                    stopLoad();
                }
            }
        }
    
        /**
         * 視圖銷燬的時候講Fragment是否初始化的狀態變爲false
         */
        @Override
        public void onDestroyView() {
            super.onDestroyView();
            isLazyLoaded = false;
            isPrepared = false;
        }
    
        /**
         * 第一次可見時,操做該方法,能夠用於showLoading操做,注意這個是全局加載loading
         */
        protected void showFirstLoading() {
            LogUtil.i("第一次可見時show全局loading");
        }
    
        /**
         * 中止加載
         * 當視圖已經對用戶不可見而且加載過數據,可是沒有加載完,而只是加載loading。
         * 若是須要在切換到其餘頁面時中止加載數據,能夠覆寫此方法。
         * 存在問題,如何中止加載網絡
         */
        protected void stopLoad(){
    
        }
    
        /**
         * 第四步:定義抽象方法onLazyLoad(),具體加載數據的工做,交給子類去完成
         */
        @UiThread
        protected abstract void onLazyLoad();
    }
    複製代碼
  • onLazyLoad()加載數據條件
    • getUserVisibleHint()會返回是否可見狀態,這是fragment實現懶加載的關鍵,只有fragment 可見纔會調用onLazyLoad() 加載數據。
    • isPrepared參數在系統調用onActivityCreated時設置爲true,這時onCreateView方法已調用完畢(通常咱們在這方法裏執行findviewbyid等方法),確保 onLazyLoad()方法不會報空指針異常。
    • isLazyLoaded確保ViewPager來回切換時BaseFragment的initData方法不會被重複調用,onLazyLoad在該Fragment的整個生命週期只調用一次,第一次調用onLazyLoad()方法後立刻執行 isLazyLoaded = true。
    • 而後再繼承這個BaseLazyFragment實現onLazyLoad() 方法就行。他會自動控制當fragment 展示出來時,纔會加載數據
  • 還有幾個細節須要優化一下
    • 當視圖已經對用戶不可見而且加載過數據,若是須要在切換到其餘頁面時中止加載數據,能夠覆寫此方法,也就是stopLoad
    • 視圖銷燬的時候講Fragment是否初始化的狀態變爲false,這個也須要處理一下
    • 第一次可見時,定義一個showFirstLoading方法,操做該方法,能夠用於Loading加載操做,注意這個是全局加載loading,和下拉刷新數據或者局部刷新的loading不同的。可能有些開發app,沒有將loading分的這麼細。

07.懶加載配合狀態管理器

  • 什麼是狀態管理器?
    • 通常在須要用戶等待的場景,顯示一個Loading動畫可讓用戶知道App正在加載數據,而不是程序卡死,從而給用戶較好的使用體驗。
    • 當加載的數據爲空時顯示一個數據爲空的視圖、在數據加載失敗時顯示加載失敗對應的UI並支持點擊重試會比白屏的用戶體驗更好一些。
    • 加載中、加載失敗、空數據的UI風格,通常來講在App內的全部頁面中須要保持一致,也就是須要作到全局統一。
  • 如何下降偶性和入侵性
    • 讓View狀態的切換和Activity完全分離開,必須把這些狀態View都封裝到一個管理類中,而後暴露出幾個方法來實現View之間的切換。 在不一樣的項目中能夠須要的View也不同,因此考慮把管理類設計成builder模式來自由的添加須要的狀態View。
    • 那麼如何下降耦合性,讓代碼入侵性低。方便維護和修改,且移植性強呢?大概具有這樣的條件……
      • 能夠運用在activity或者fragment中
      • 不須要在佈局中添加LoadingView,而是統一管理不一樣狀態視圖,同時暴露對外設置自定義狀態視圖方法,方便UI特定頁面定製
      • 支持設置自定義不一樣狀態視圖,即便在BaseActivity統一處理狀態視圖管理,也支持單個頁面定製
      • 在加載視圖的時候像異常和空頁面可否用ViewStub代替,這樣減小繪製,只有等到出現異常和空頁面時,纔將視圖給inflate出來
      • 當頁面出現網絡異常頁面,空頁面等,頁面會有交互事件,這時候能夠設置點擊設置網絡或者點擊從新加載等等
  • 那麼具體怎麼操做呢?
    • 能夠自由切換內容,空數據,異常錯誤,加載,網絡錯誤等5種狀態。父類BaseFragment直接暴露5中狀態,方便子類統一管理狀態切換,這裏fragment的封裝和activity差很少。
    /**
     * <pre>
     *     @author yangchong
     *     blog  : https://github.com/yangchong211
     *     time  : 2017/7/20
     *     desc  : fragment的父類
     *     revise: 注意,該類具備懶加載
     * </pre>
     */
    public abstract class BaseStateFragment extends BaseLazyFragment {
    
        protected StateLayoutManager statusLayoutManager;
        private View view;
    
        @Nullable
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                                 @Nullable Bundle savedInstanceState) {
            if(view==null){
                view = inflater.inflate(R.layout.base_state_view, container , false);
                initStatusLayout();
                initBaseView(view);
            }
            return view;
        }
    
        @Override
        public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            initView(view);
            initListener();
        }
    
        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
        }
    
        /**
         * 獲取到子佈局
         * @param view              view
         */
        private void initBaseView(View view) {
            LinearLayout llStateView = view.findViewById(R.id.ll_state_view);
            llStateView.addView(statusLayoutManager.getRootLayout());
        }
    
    
        /**
         * 初始化狀態管理器相關操做
         */
        protected abstract void initStatusLayout();
    
        /**
         * 初始化View的代碼寫在這個方法中
         * @param view              view
         */
        public abstract void initView(View view);
    
        /**
         * 初始化監聽器的代碼寫在這個方法中
         */
        public abstract void initListener();
    
        /**
         * 第一次可見狀態時,showLoading操做,注意下拉刷新操做時不要用該全局loading
         */
        @Override
        protected void showFirstLoading() {
            super.showFirstLoading();
            showLoading();
        }
    
        /*protected void initStatusLayout() {
            statusLayoutManager = StateLayoutManager.newBuilder(activity)
                    .contentView(R.layout.common_fragment_list)
                    .emptyDataView(R.layout.view_custom_empty_data)
                    .errorView(R.layout.view_custom_data_error)
                    .loadingView(R.layout.view_custom_loading_data)
                    .netWorkErrorView(R.layout.view_custom_network_error)
                    .build();
        }*/
    
    
        /*---------------------------------下面是狀態切換方法-----------------------------------------*/
    
    
        /**
         * 加載成功
         */
        protected void showContent() {
            if (statusLayoutManager!=null){
                statusLayoutManager.showContent();
            }
        }
    
        /**
         * 加載無數據
         */
        protected void showEmptyData() {
            if (statusLayoutManager!=null){
                statusLayoutManager.showEmptyData();
            }
        }
    
        /**
         * 加載異常
         */
        protected void showError() {
            if (statusLayoutManager!=null){
                statusLayoutManager.showError();
            }
        }
    
        /**
         * 加載網絡異常
         */
        protected void showNetWorkError() {
            if (statusLayoutManager!=null){
                statusLayoutManager.showNetWorkError();
            }
        }
    
        /**
         * 加載loading
         */
        protected void showLoading() {
            if (statusLayoutManager!=null){
                statusLayoutManager.showLoading();
            }
        }
    }
    
    //如何切換狀態呢?
    showContent();
    showEmptyData();
    showError();
    showLoading();
    showNetWorkError();
    
    //或者這樣操做也能夠
    statusLayoutManager.showLoading();
    statusLayoutManager.showContent();
    複製代碼
  • 狀態管理器的設計思路
    • StateFrameLayout是繼承FrameLayout自定義佈局,主要是存放不一樣的視圖,以及隱藏和展現視圖操做
    • StateLayoutManager是狀態管理器,主要是讓開發者設置不一樣狀態視圖的view,以及切換視圖狀態操做
      • 幾種異常狀態要用ViewStub,由於在界面狀態切換中loading和內容View都是一直須要加載顯示的,可是其餘的3個只有在沒數據或者網絡異常的狀況下才會加載顯示,因此用ViewStub來加載他們能夠提升性能。
    • OnRetryListener,爲接口,主要是重試做用。好比加載失敗了,點擊視圖須要從新刷新接口,則能夠用到這個。開發者也能夠本身設置點擊事件
    • 關於狀態視圖切換方案,目前市場有多種作法,具體能夠看個人這篇博客:juejin.im/post/5d2f01…

其餘介紹

01.關於博客彙總連接

02.關於個人博客

項目地址:github.com/yangchong21…

相關文章
相關標籤/搜索