Android开发-ViewPager嵌套Fragment卡顿解决Viewpager滑动重复加载Fragment延迟加载

前言

Fragment大家肯定不会陌生的,几乎每个App里都有它的存在,作为Google在3.0以后引入的一个概念,极大的解决了Activity(或者说手机屏幕)的局限性,让Activity碎片化,正如它的原意 【分段】,【碎片】一样让一个屏幕中的activity展示多个页面成为了现实。

本篇文章主要讲的是在Viewpager和Fragment一起使用的时候出现的一些问题,如何解决。至于Fragment的使用总结可以参考Android中Fragment知识点终极整理

先配一张生命周期图,以便下面分析使用
这里写图片描述

问题分析

  • 问题一:当Viewpager和Fragment一起使用的时候,假如有四个Fragment,如果不做其它设置,当Fragment的逻辑复杂耗时的时候或者View结构复杂,在页面进行滑动的时候,可以感觉到明显的卡顿

  • 问题二:当Viewpager和Fragment一起使用的时候,假如有四个Fragment,如果不做其它设置,当你从第一个Fragment滑动到第三个Fragment的时候:
    1.如果使用的是FragmentPagerAdapter,那第一个Fragment会执行到onDestroyView,即Fragment的视图被销毁了,实例还存在,当再次滑动到第一个Fragment的时候,会再次从onCreateView回调重建View。
    2.如果使用的是FragmentStatePagerAdapter,那第一个Fragment会一直执行到onDetach,即视图销毁了,如果没有添加到回退栈,Fragment的实例也会被销毁,当再次滑动到第一个Fragment的时候,会再从onAttach开始回调。
    不管是第一种adapter还是第二种adapter,都对导致Fragment实例或者视图的重复加载。

显然问题一和问题二都不是我们想看到的,有人可能会想通过 ViewPager的 setOffscreenPageLimit 方法预加载四个Fragment,避免第一次 进到页面时候滑动卡顿和重复加载,但是这有一个比较大的问题,如果多个Fragment里有很多的网络请求,耗时操作,那这些操作在同一时间进行操作像View的初始化赋值等还是会出现卡顿问题。

总结上面所说:我们要解决的是View的重复加载以及当Fragment页面和逻辑复杂时ViewPager滑动卡顿

解决方案

我推荐的做法是通过封装一个Fragment使用延迟加载,当Fragment第一次可见时进行数据和View的相关操作以避免滑动卡顿;同时结合FragmentPagerAdapter,取消销毁视图,只创建一次View。如何封装,见如下代码

/**
 * @Description TODO(所有Fragment基类,延迟加载)
 * @author mango
 * @Date 2018/2/23 17:49
 */
public abstract class BaseFragment extends Fragment {

    private String TAG = BaseFragment.class.getSimpleName();

    private View mRoot;

    /**
     * 是否执行了lazyLoad方法
     */
    private boolean isLoaded;
    /**
     * 是否创建了View
     */
    private boolean isCreateView;

    /**
     * 当从另一个activity回到fragment所在的activity
     * 当fragment回调onResume方法的时候,可以通过这个变量判断fragment是否可见,来决定是否要刷新数据
     */
    public boolean isVisible;

    /*
    * 此方法在viewpager嵌套fragment时会回调
    * 查看FragmentPagerAdapter源码中instantiateItem和setPrimaryItem会调用此方法
    * 在所有生命周期方法前调用
    * 这个基类适用于在viewpager嵌套少量的fragment页面
    * 该方法是第一个回调,可以将数据放在这里处理(viewpager默认会预加载一个页面)
    * 只在fragment可见时加载数据,加快响应速度
    * */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (getUserVisibleHint()) {
            onVisible();
        } else {
            onInvisible();
        }
    }


    /*
    *  因为Fragment是缓存在内存中,所以可以保存mRoot ,防止view的重复加载 
    *  与FragmentPagerAdapter 中destroyItem方法取消调用父类的效果是一样的,可以任选一种做法
    *  推荐第二种
    * */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if(mRoot == null){
            mRoot = createView(inflater,container,savedInstanceState);
            isCreateView = true;
            initView(mRoot);
            initListener();
            onVisible();
        }
        return mRoot;
    }

    protected void onVisible() {

        isVisible = true;

        if(isLoaded){
            refreshLoad();
        }
        if (!isLoaded && isCreateView && getUserVisibleHint()) {
            isLoaded = true;
            lazyLoad();
        }
    }

    protected void onInvisible() {
        isVisible = false;
    }

    protected abstract View createView(LayoutInflater inflater,ViewGroup container,Bundle savedInstanceState);
    protected abstract void initView(View root);
    protected abstract void initListener();

    /**
     * fragment第一次可见的时候回调此方法
     */
    protected abstract void lazyLoad();

    /**
     * 在Fragment第一次可见加载以后,每次Fragment滑动可见的时候会回调这个方法,
     * 子类可以重写这个方法做数据刷新操作
     */
    protected void refreshLoad(){}

}

具体作用已经注释很清楚了,子类继承这个就可以了,例如

/**
 * @Description TODO()
 * @author mango
 * @Date 2018/2/23 10:16
 */
public class FirstFragment extends BaseFragment{

    public String TAG = FirstFragment.class.getSimpleName();
    public TextView textView;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.e(TAG,"onAttach");
    }

    @Override
    protected View createView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        Log.e(TAG,"createView");
        View root = inflater.inflate(R.layout.frag_first,container,false);
        return root;
    }

    @Override
    protected void initView(View root) {
        textView = (TextView) root.findViewById(R.id.tv);
    }

    @Override
    protected void initListener() {

    }

    @Override
    protected void lazyLoad() {
        Log.e(TAG,"lazyLoad");
        textView.setText("这是第一个fragment");
        /**
         * 第一次加载的时候在这做数据和View的操作
         */
    }

    @Override
    protected void refreshLoad() {
        super.refreshLoad();
    }
}

再看看FragmentPagerAdapter

public class FragmentAdapter extends FragmentPagerAdapter {

    public FragmentAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return FragmentFactory.getInstance().getFragment(position);
    }

    @Override
    public int getCount() {
        return 4;
    }

    /**
     * 重写该方法,取消调用父类该方法
     * 可以避免在viewpager切换,fragment不可见时执行到onDestroyView,可见时又从onCreateView重新加载视图
     * 因为父类的destroyItem方法中会调用detach方法,将fragment与view分离,(detach()->onPause()->onStop()->onDestroyView())
     * 然后在instantiateItem方法中又调用attach方法,此方法里判断如果fragment与view分离了,
     * 那就重新执行onCreateView,再次将view与fragment绑定(attach()->onCreateView()->onActivityCreated()->onStart()->onResume())
     * @param container
     * @param position
     * @param object
     */
    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
//        super.destroyItem(container, position, object);
    }
}

方案总结

  • 通过取消调用FragmentPagerAdapter 的destroyItem方法避免视图的重复创建
  • 通过Fragment的setUserVisibleHint方法在Fragment可见时再加载数据,达到懒加载的效果;当Fragment可见时,该方法被回调,参数是true;不可见时,也会被回调,参数是false
    使用过ViewPager的同学一定知道setOffScreenPageLimit()方法,它默认的limit为1,也就是ViewPager会默认初始化下一个Fragment,也就是预加载;当页面可见时setUserVisibleHint方法值为true,预加载时Fragment中的该方法也会被回调,参数是false,所以需要注意

FragmentPagerAdapter分析

使用这种adapter时,滑动viewpager,不可见的fragment最多执行到onDestroyView, 即视图被销毁了,但fragment实例并没有被销毁,缓存在FragmentManager中,还是常驻内存的,即没有执行到onDestroy,并且保存了Fragment状态

具体表现就是该fragment里所实例化的对象依然还在(包括控件),仅仅只是把视图销毁了,fragment的状态依然由FragmentManager维护
;当再次可见时,仅从onCreateView开始回调,重新创建视图,像textview和edittext等的值依然保存并显示,但是像listview滑动的位置,scrollview滑动的位置等状态并没有保存

接下来从FragmentPagerAdapter源码分析下优化点

优化1

当创建某个position的页卡时,instantiateItem方法会被调用

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            mCurTransaction.attach(fragment);
        } else {
            fragment = getItem(position);
            mCurTransaction.add(container.getId(), fragment,
                    makeFragmentName(container.getId(), itemId));
        }
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }

重点看这句话 :Fragment fragment = mFragmentManager.findFragmentByTag(name);

这意味着FragmentPagerAdapter会先从FragmentManager中的【ArrayList< Fragment> mActive】这个List缓存中去查找,如果没有就会通过getItem方法获取一个Fragment,该方法是需要我们重写的;一旦我们主界面tab的所有Fragment都被缓存到FragmentManager中,就不会再走getItem方法了

看到平时有很多人的写法是在Activity里维护一个List,然后在adapter里的getItem方法根据position从list获取Fragment;其实这种做法是多余的,会占用多余内存

优化2

上面说了重写destroyItem方法并取消调用父类的该方法可以避免重复创建视图,看看源码

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        mCurTransaction.detach((Fragment)object);
    }

重点看mCurTransaction.detach((Fragment)object)方法,会调用FragmentTransaction的detach方法,将fragment与view分离,此时Fragment会执行到onDestroyView();当再次执行instantiateItem方法时,因为Fragment实例被缓存,会调用attach方法,此方法里判断如果fragment与view分离了,那就重新执行onCreateView,再次将view与fragment绑定

所以我们只需要取消调用父类方法即可避免重复创建视图

使用场景

这种adapter消耗一定的内存,仅适合包含少量的Fragment页面,例如首页的tab,引导页等页面


大量Fragment问题

有朋友问到这种问题,我也觉得需要解决

比如新闻类的APP,页面甚至会有十几个Fragment存在,这种情况下显然是不能使用FragmentPagerAdapter了,它会缓存所有的Fragment实例;这时候就要使用FragmentStatePagerAdapter了

FragmentStatePagerAdapter

使用这种adapter,当滑动viewpager时,不可见的fragment会执行到onDestroy和onDetach,即视图被销毁了,被移除的Fragment没有添加到回退栈,那这个Fragment实例将会被销毁,不在由FragmentManager维护,仅保存Fragment状态(包括 View 状态和成员变量数据状态);当再次可见时,会从onAttach方法从新走一遍,再次创建该Fragment实例;FragmentStatePagerAdapter 内存占用较小,所以适合大量动态页面,比如我们常见的新闻列表类应用

FragmentStatePagerAdapter默认会预加载下一个Fragment,也就是会缓存这个Fragment,默认预加载数量是setOffScreenPageLimit()方法的limit决定的,limit默认是1,这时最多缓存3个Fragment;超过limit值,超出的Fragment会被回收,实例被销毁,也就是与当前可见Fragment间隔limit个Fragment会被销毁;如果Fragment没有被缓存,就会通过getItem()方法获取Fragment

接下来从源码看看

@Override
    public Object instantiateItem(ViewGroup container, int position) {
       
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);

        mCurTransaction.remove(fragment);
    }

先看instantiateItem方法的

 if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

比如有ABCDEF五个Fragment,预加载一个(limit=1),当AFragment可见时,预加载BFragment,最后mFragments里缓存AB
这样你从A划到B时,从mFragments取出B,同时预加载C,最后mFragments里缓存ABC
当你从B滑到A,从mFragments取出A;同时C与A间隔了一个Fragment,C会被销毁,从mFragments中剔除,这点从destroyItem方法的mFragments.set(position, null)可知

所以可以通过设置合适的limit值,来达到合适的内存利用;同时为了避免滑动卡顿,仍然要采用上述延迟加载的方法,即Fragment可见时才加载数据(比如网络请求,View数据的初始化等复杂操作)

注意:这种使用大量Fragment的情况下,千万不要在Activity里使用List保存初始化好的Fragment,然后在getItem方法里通过position获取,很浪费内存;通过上面的分析可以知道,getItem方法只会在没有缓存的Fragment可用时才会被调用,不会每次滑动ViewPager时就调用;这样操作后内存中最多只会缓存(2*limit+1)个Fragment,其它的会被销毁,不会浪费内存,除非需要销毁的Fragment出现了内存泄漏

public class SecondActivity extends AppCompatActivity {

    public static final String[] TITLE = new String[] { "NBA", "欧冠", "西甲", "英超", "世界杯", "CBA", "电竞", "中超", "NBA", "欧冠", "西甲", "英超", "世界杯", "CBA", "电竞", "中超"};

}
public class FragmentStateAdapter extends FragmentStatePagerAdapter {


    public FragmentStateAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return BatchFragment.newInstance(SecondActivity.TITLE[position&(SecondActivity.TITLE.length-1)]);
    }

    @Override
    public int getCount() {
        return SecondActivity.TITLE.length;
    }

}

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页