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…

相关文章
相关标签/搜索