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;
}
}