关于Fragment的一些思考

2,555 阅读6分钟

概述

Fragment在开发中经常使用,它的出现大大解放了Activity,使得原来在Activity中书写的代码,可以放到Fragment中,并且方便复用。之前虽然经常使用Fragment但是并没有深究其内部是如何实现的,遇到使用上的一些问题也没有深究问题发生的根本原因,借此机会对Fragment进行深入研究下,涉及到的点如下

  • Fragment的生命周期是如何调用的
  • Fragment是如何存储与恢复的
  • Fragment重影以及在ViewPager中使用需要注意的地方
  • Fragment使用的一些技巧

Fragment源码版本为androidx:1.1.0

Fragment是什么

Fragment包含一个View对象,可以在Activity的布局中添加或删除,它还有生命周期、回退栈、销毁重建机制等,它的这些特性使它成为一个微型的Activity,同时相比Activity来说,

优点有:

  • 界面切换时不需要进程间通信,很快
  • 复用很方便

缺点有:

  • 使用复杂,生命周期比Activity多,Fragment中的View的和Fragment的生命周期不一样
  • 重建机制导致容易使用出错
  • 如果在转场动画过程中发生了数据渲染,那么就会产生动画卡顿现象,因为数据渲染造成了 UI 重新 layout

Fragment的生命周期

平常使用中发现,FragmentActivity的生命周期调用基本一致,不难猜想到,FragmentActivity持有,并且Activity在各个生命周期方法里调用Fragment相应的生命周期,查看FragmentActivity里代码如下:


//FragmentActivity.java

final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    mFragments.dispatchCreate(); //分发onCreate给Fragment
 }
 
 @Override
    protected void onStart() {
    mFragments.dispatchStart(); //分发onStart给Fragment
    }
    
    ... // 其他生命周期方法是类似的

ActivityFragment的相关的操作放在FragmentController中,在Activity生命周期调用时,通过FragmentController来给Fragment分发生命周期事件,我们通过以下常用的方式来深入探索,我们常用的方式如下:

getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.container,new FragmentTest())
                .commit();

这里引申出两个相关的类

  • FragmentManager
  • FragmentTransaction

FragmentManager

getSupportFragmentManager()得到一个FragmentManager对象,它是用来管理Fragment,包含所有添加到当前FragmentManager中的所有FragmentFragment的回退栈,调用Fragment生命周期等等,不过它是一个抽象类,它的实现是FragmentManagerImpl,关注它内部几个有用的成员变量

//通过addFragment添加或者从布局<fragment/>添加而来
final ArrayList<Fragment> mAdded = new ArrayList<>();
//fragment回退栈
ArrayList<BackStackRecord> mBackStack;

FragmentTransaction

getSupportFragmentManager() .beginTransaction()获取到的就是一个FragmentTransaction,它代表一个事务,表示要执行的一系列动作要么都完成,要么都不完成,在Fragment中使用的意义如下:

getSupportFragmentManager()
                .beginTransaction()
                .add()
                .add()
                .remove()
                .addToBackStack("")
                .commit();

我们是可以在一次提交中同时addremove多次,而这一系列操作就是一个事务,上面代码提交时加入了回退栈,此时点击手机上的返回键,将会执行相反的操作,既removeremoveadd这三个操作,这就提现了事务的特点,事务是指一组原子性的操作,这些操作是不可分割的整体,要么全完成,要么全不完成,完成后可以回滚到完成前的状态

FragmentTransaction也是一个抽象类的,它的实现是BackStackRecord,它就是我们在上面看到的回退栈所保存的元素。其中有一个关键的变量

ArrayList<Op> mOps = new ArrayList<>();

static final class Op {
        int mCmd;
        Fragment mFragment;
}

mOps这个列表保存的就是一次事物中的所有动作,mCmd表示我们对Fragment的操作,比如addremove等,当commit()之后在某个时间点,遍历mOps完成响应操作,这就是事务实现的机制。

提交事务我们上面使用的是commit,它还有一些相似的方法

  • commit(): 通过Handler来提交,并不是立即执行。如果在onStop或者onSaveInstanceState之后提交会抛出异常 throw new IllegalStateException( "Can not perform this action after onSaveInstanceState");
  • commitAllowingStateLoss(): 和commit()的不同之处在与onStop或者onSaveInstanceState之后提交不抛出异常可以正常执行
  • commitNow(): 相比与commit() 不同点在于是立即执行提交而不是通过Handler
  • commitNowAllowingStateLoss(): 类似

生命周期回调

FragmentTransaction 只是对事务的记录,最终执行还是调用FragmentManager的相关方法来进行addremoveshowhide等操作。以add为例,commit()之后,通过一系列方法后,调用FragmentManagerexecuteOps()Fragment放入上面提到的mAdded列表中,然后调用到moveToState()来处理Fragment相关的生命周期,在介绍moveToState()之前先看下

    static final int INITIALIZING = 0;     // Not yet created.
    static final int CREATED = 1;          // Created.
    static final int ACTIVITY_CREATED = 2; // Fully created, not started.
    static final int STARTED = 3;          // Created and started, not resumed.
    static final int RESUMED = 4;          // Created started and resumed.

这些是标记Fragment当前的状态,这里有一个令人疑惑的点就是,为什么状态只到resume,而答案就在moveToState中,

void moveToState(Fragment f, int newState, int transit, int transitionStyle,
                     boolean keepActive) {
     // newState 是FragmentManager的状态,是跟随Activity的
    if (f.mState <= newState) { 
        switch (f.mState) {
            case Fragment.INITIALIZING:
                if (newState > Fragment.INITIALIZING) {
                    f.performAttach();
                    if (!f.mIsCreated) {
                        f.performCreate(f.mSavedFragmentState); 
                    }
                }

            case Fragment.CREATED:
                if (newState > Fragment.CREATED) {
                    ViewGroup container = null;
                    f.mContainer = container;
                    f.performCreateView(f.performGetLayoutInflater(
                            f.mSavedFragmentState), container, f.mSavedFragmentState);
                    if (f.mView != null) {
                        if (container != null) {
                            container.addView(f.mView);
                        }

                        f.onViewCreated(f.mView, f.mSavedFragmentState);
                    }
                    f.performActivityCreated(f.mSavedFragmentState);
                    if (f.mView != null) {
                        f.restoreViewState(f.mSavedFragmentState);
                    }
                }

            case Fragment.ACTIVITY_CREATED:
                if (newState > Fragment.ACTIVITY_CREATED) {
                    f.performStart();
                }

            case Fragment.STARTED:
                if (newState > Fragment.STARTED) {
                    f.performResume();
                }
        }
    }
}

根据Fragment当前的状态与newState比较,来判断应该到达什么生命状态,注意上述case中是没有break的,所以Fragment会在到达它的目标状态过程中,会调用相应的生命周期方法,最终达到目标状态。我们也看到在onCreateView调用之后,就把该方法创建的View添加到了包含Fragment的容器中。以上只是到onResume状态,还有一部分代码没贴出来,来看一下

		// newState 是FragmentManager的状态,是跟随Activity的
        else if (f.mState > newState) {  
            switch (f.mState) {
                case Fragment.RESUMED:
                    if (newState < Fragment.RESUMED) {
                        f.performPause();
                    }
                case Fragment.STARTED:
                    if (newState < Fragment.STARTED) {
                        f.performStop();
                    }
 
                case Fragment.ACTIVITY_CREATED:
                    if (newState < Fragment.ACTIVITY_CREATED) {
                        if (f.mView != null) {
                            if (mHost.onShouldSaveFragmentState(f) && f.mSavedViewState == null) {
                                saveFragmentViewState(f);
                            }
                        }
                        f.performDestroyView();
                        if (f.mView != null && f.mContainer != null) {
                            if (f.getParentFragment() == null || !f.getParentFragment().mRemoving) {

                                f.mContainer.removeView(f.mView);
                            }
                        }
                        f.mContainer = null;
                        f.mView = null;
                        f.mViewLifecycleOwner = null;
                        f.mViewLifecycleOwnerLiveData.setValue(null);
                        f.mInnerView = null;
                        f.mInLayout = false;
                    }
                case Fragment.CREATED:
                    if (newState < Fragment.CREATED) {
                        if (f.getAnimatingAway() != null || f.getAnimator() != null) {
                            newState = Fragment.CREATED;
                        } else {
                            boolean beingRemoved = f.mRemoving && !f.isInBackStack();
                            f.performDestroy();
                        } else{
                            f.mState = Fragment.INITIALIZING;
                        }

                        f.performDetach();
                    }
            }
        }

当处于Fragment.RESUMED时,调用的是pause,其他状态也是类似,看到DestroyView()之后,Fragment中所持有的View被置为null了。

这里还有一个点就是,Fragment作为其他Fragment的容器时,父Fragment会将生命周期分发给子Fragment。上述只是简单的介绍一些我认为的关键点,commit()之后的完成流程是: 上图出自【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析

上面对Fragment的生命周期只是进行了简单的分析,更深的理解还是需要去阅读源码才行

Fragment是如何存储与恢复的

我们知道当App处于后台被意外杀死时不仅Activity会重建,原来添加的Fragment也会恢复。首先来看一下Fragment保存的时机

	FragmentActivity.java

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        markFragmentsCreated();
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
    }

FragmentActivityonSaveInstanceState()方法中,看到Parcelable p = mFragments.saveAllState();在这里保存了Fragment的状态,代码如下

    @Nullable
    public Parcelable saveAllState() {
        return mHost.mFragmentManager.saveAllState();
    }

最终调用FragmentManager.saveAllState(),我们进入这个方法里看一下

Parcelable saveAllState() {
            //  1.将未执行完成的事务和动画执行完毕
            forcePostponedTransactions();
            endAnimatingAwayFragments();
            execPendingActions();

            mStateSaved = true;

            if (mActive.isEmpty()) {
                return null;
            }

            // 2.保存mActive中的Fragment
            int size = mActive.size();
            ArrayList<FragmentState> active = new ArrayList<>(size);
            boolean haveFragments = false;
            for (Fragment f : mActive.values()) {
            if (f != null) {

                haveFragments = true;

                FragmentState fs = new FragmentState(f);
                active.add(fs);
            }
        }

            ArrayList<String> added = null;
            BackStackState[] backStack = null;

            //3. 保存 mAdded中的Fragment
            size = mAdded.size();
            if (size > 0) {
                added = new ArrayList<>(size);
                for (Fragment f : mAdded) {
                    added.add(f.mWho);
                }
            }

            //4. 保存 mBackStack中的数据
            if (mBackStack != null) {
                size = mBackStack.size();
                if (size > 0) {
                    backStack = new BackStackState[size];
                    for (int i = 0; i < size; i++) {
                        backStack[i] = new BackStackState(mBackStack.get(i));
                    }
                }
            }
			
            FragmentManagerState fms = new FragmentManagerState();
            fms.mActive = active;
            fms.mAdded = added;
            fms.mBackStack = backStack;
            if (mPrimaryNav != null) {
                fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
            }
            fms.mNextFragmentIndex = mNextFragmentIndex;
            return fms;
        }

上面的代码经过了一些简化,不过逻辑很简单,就是将现有的数据保存到FragmentManagerState中,步骤如下

  • 保存mActive中的Fragment
  • 保存 mAdded中的Fragment
  • 保存 mBackStack中的数据
  • 最后将数据保存到一个FragmentManagerState对象中, mAddedmBackStack上面提到过,分别是添加到FragmentManager中的Fragment和返回栈中Fragment,而mActive这个列表尚未搞明白代表的意义是什么,不过也不影响我们理解保存的逻辑。

FragmentManagerState最终保存在哪里了呢,返回去看FragmentActivity.onSaveInstanceState()

@Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
     Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
    }

可以看到,所有的Fragment是保存到了一个Bundle对象中,这个Bundle对象通过进程间通信保存到了由AMS管理的ActivityRecord中 的Bundle icicle;

Fragment恢复

恢复的入口在FragmentActivit.onCreate(Bundle savedInstanceState)

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);

        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreSaveState(p);

    	}
    }

恢复最终还是由FragmentManager来处理的,详细代码在void restoreSaveState(Parcelable state)中,其中的逻辑也很简单。

不过有一点需要注意的是,Fragment 恢复是真的和重建前使用的是同一个对象吗? 有两种情况:

  • 设置了RetainInstance,并且进程还在: 设置了RetainInstance,Fragment会被保存到nonConfig里,重建时会直接拿出这个已有的Fragment,不会重新创建。

  • 没有设置RetainInstance,或者进程已经被kill掉了: 这种情况下,没有可以直接获得的Fragment,会通过反射创建出一个新的Fragment,这个fragment肯定和之前那个不是同一个对象。

摘自 每日一问 | Fragment 是如何被存储与恢复的? 有更新

使用Fragment需要注意的地方

  1. 重影问题

    该问题就是当app发生重建时(横竖屏切换或者因内存不足被杀后恢复),会发现原来的fragment上,又盖了一层Fragment,发生重影,代码如下

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    	getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.container, new FragmentTest())
                .commit();
    }
    

    正常情况下这样使用是没问题的,但是当发生重建时,根据上面对Fragment恢复的分析得知,系统已经帮我们做好了Fragment的恢复,而上述代码又在onCreate()中添加了Fragment,所以会导致Fragment添加两遍的问题,知道了原因我们就可以改正,如下

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    	if(savedInstanceState == null) {
            getSupportFragmentManager()
                    .beginTransaction()
                    .add(R.id.container, new FragmentTest())
                    .commit();
        }
    }
    
  2. 在ViewPager中使用

先来看一段常用代码


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view_pager);
        viewPager = findViewById(R.id.viewPager);
        List<BaseFragment> list = new ArrayList<>();
        list.add(new FragmentOne());
        list.add(new FragmentTwo());
        list.add(new FragmentThree());
        viewPager.setAdapter(new ViewPagerAdapter(getSupportFragmentManager(), list));
    }

    private class ViewPagerAdapter extends FragmentPagerAdapter {
        private List<BaseFragment> list;
        public ViewPagerAdapter(FragmentManager fm, List<BaseFragment> list) {
            super(fm);
            this.list = list;
        }
        @Override
        public Fragment getItem(int position) {
            return list.get(position);
        }
        @Override
        public int getCount() {
            return list.size();
        }
    }

上述代码看起来没问题,但是当app发生重建时,list中的Fragment可能ViewPagerAdapter中的Fragment不是同一个,这里的可能是指

  • ViewPager加载过的Fragment : 肯定与list中的不一样
  • ViewPager没加载过的Fragment: 由于会调用getItem(),所以与list中的一样

原因如下:

@Override
    public Object instantiateItem(@NonNull 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));
        }
        ...
        return fragment;
    }

通过源码可以看到,FragmentPagerAdapter查找Fragment时先从FragmentManager获取,如果找不到才会调用getItem(position),而重建时FragmentManager会恢复之前添加的Fragment,对于重建的这些Fragment由于已经存在,所以不要调用getItem(position),自然也就和onCreate()中重新添加到列表list中的Fragment不一样。

对于FragmentStatePagerAdapter来说,只要明白了跟FragmentPagerAdapter的区别,就可以相同的思路去理解。

  1. ViewPager更新问题

当调用FragmentPagerAdapter.notifyDataSetChanged()后,发现ViewPager并没有改变,看源码发现最终调用如下代码:

void dataSetChanged() {
	for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            final int newPos = mAdapter.getItemPosition(ii.object);

			// 1.关键点
            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            if (newPos == PagerAdapter.POSITION_NONE) {
                mItems.remove(i);
                i--;
                // 2.关键点
                mAdapter.destroyItem(this, ii.position, ii.object);
            }
        }
        requestLayout();
}

上述有两句关键代码,第一句newPos == PagerAdapter.POSITION_UNCHANGED直接跳过循环,不处理,而PagerAdapter如果不重写getItemPosition()的话,默认就是POSITION_UNCHANGED,所以更新必须是重getItemPosition()如下

 @Override
        public int getItemPosition(Object object) {
           return POSITION_NONE;
        }

上述第二句是mAdapter.destroyItem(this, ii.position, ii.object);对于FragmentStatePagerAdapter来说,destroy之后FragmentManager会remove掉Fragment,当需要该位置的Fragment时,调用getItem()重新获取,这种情况下ViewPager没有缓存的Fragment更新是可以的,但是FragmentPagerAdapterdestroy之后只是将Fragment detach掉,Fragment实例并没有被移除,所以在数据变化时候需要清空掉FragmentManager管理的Fragment,才能达到更新的效果。

Fragment使用的一些技巧

  • 使用一个没有界面的Fragment可以获取到生命周期相关的数据,例子有LifeCycleGlide
  • 使用Fragment来动态获取权限

总结

  1. Fragment的生命周期由自己和Activity的状态来共同决定
  2. FragmentActivityonSaveInstanceState中保存起来,是保存到了AMS中,在应用因配置改变或内存低被杀死而重建时会恢复
  3. Fragment常见的问题,大多与重建机制有关,只要搞懂了重建机制就能处理好

参考

  1. 当Fragment遇上ViewPager
  2. 每日一问 Activity 都重建了,你 Fragment凭什么活着?
  3. 每日一问 | Fragment 是如何被存储与恢复的? 有更新
  4. 每日一问 | Activity与Fragment的那些事,“用起来没问题,我都要走了,你崩溃了?”
  5. 【背上Jetpack之Fragment】从源码角度看 Fragment 生命周期 AndroidX Fragment1.2.2源码分析
  6. 【背上Jetpack之Fragment】你真的会用Fragment吗?Fragment常见问题以及androidx下Fragment的使用新姿势
  7. 使用Fragment优雅地申请运行时权限
  8. 每日一问 ViewPager 这个流传广泛的写法,其实是有问题的!