来自面试官的灵魂拷问:Fragment是如何进行事务管理的?

背景

本文基于androidx版本的Fragment

如果要是有面试官问你,Fragment是如何进行事务管理的?相信很多人可能都回答不出来,很多人可能觉得问题这个还不如问如何进行Fragment预/懒加载,生命周期,返回栈,状态保存等来的有水平。其实我觉得这个问题其实问的挺有深度,如果你想要了解Fragment的事务管理,需要对Fragment的源码进行充分了解,前面的那些问题都是小儿科了。

面试的时候问题有两种类型,一种是问典型,比如面试官问你HashMap,里面涉及到的方方面面非常多,可以进行大量扩展;还有一个就是直接问一定深度的问题,如果你能答上来,说明的掌握的到一定的程度,简单的问题就可以忽略了,答不上了我再问前面简单的问题。

要了解Fragment,首先需要了解它的生命周期,同时与Activity的生命周期的关联也是非常重要的。

Fragment的生命周期

来自面试官的灵魂拷问:Fragment是如何进行事务管理的?_第1张图片
image

Fragment和Activity的生命周期关联

来自面试官的灵魂拷问:Fragment是如何进行事务管理的?_第2张图片
image

PS:关于我

来自面试官的灵魂拷问:Fragment是如何进行事务管理的?_第3张图片

本人是一个拥有6年开发经验的帅气Android攻城狮,记得看完点赞,养成习惯,微信搜一搜「 程序猿养成中心 」关注这个喜欢写干货的程序员。

另外耗时两年整理收集的Android一线大厂面试完整考点PDF出炉,资料【完整版】已更新在我的【Github】,有面试需要的朋友们可以去参考参考,如果对你有帮助,可以点个Star哦!

地址:【https://github.com/733gh/xiongfan】

Fragment是依附于Activity,所以Fragment的生命周期和Activity的生命周期息息相关,在每个Activity的生命周期中最终都会调用FragmentManagerImpl.dispatchXXX()通知,然后调用到FragmentManagerImpl.dispatchStateChange(int nextState),Fragment有多个状态值来展示什么周期所处的状态

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 默认的状态是 INITIALIZING,也是代表当前的生命周期状态
int mState = INITIALIZING;

image.gif

注意本文用的是androidx版本的fragment,而非普通support包下面的fragment(有六种状态),最终都会调用到moveToState方法,然后进行这些状态的变化,同时调用fragment具体的生命周期方法。

Fragment是如何进行事务管理的?

先来看看Fragment中的事务(FragmentTransaction)的action

static final int OP_NULL = 0;
static final int OP_ADD = 1;
static final int OP_REPLACE = 2;
static final int OP_REMOVE = 3;
static final int OP_HIDE = 4;
static final int OP_SHOW = 5;
static final int OP_DETACH = 6;
static final int OP_ATTACH = 7;
static final int OP_SET_PRIMARY_NAV = 8;
static final int OP_UNSET_PRIMARY_NAV = 9;
static final int OP_SET_MAX_LIFECYCLE = 10;
image.gif

所有的事务操作被封装为一个Op类进行统一管理

static final class Op {
    int mCmd;
    Fragment mFragment;
    int mEnterAnim;
    int mExitAnim;
    int mPopEnterAnim;
    int mPopExitAnim;
    Lifecycle.State mOldMaxState;
    Lifecycle.State mCurrentMaxState;

    Op() {
    }

    Op(int cmd, Fragment fragment) {
        this.mCmd = cmd;
        this.mFragment = fragment;
        this.mOldMaxState = Lifecycle.State.RESUMED;
        this.mCurrentMaxState = Lifecycle.State.RESUMED;
    }

    Op(int cmd, @NonNull Fragment fragment, Lifecycle.State state) {
        this.mCmd = cmd;
        this.mFragment = fragment;
        this.mOldMaxState = fragment.mMaxState;
        this.mCurrentMaxState = state;
    }
}
image.gif

先来看一下事务添加的流程

来自面试官的灵魂拷问:Fragment是如何进行事务管理的?_第4张图片
image

首先是通过FragmentTransaction的add/replace/show/hide/remove/attach等操作,将这些操作(Op)添加到ArrayList mOps = new ArrayList<>()中,后面进行commit的时候,FragmentTransaction是个抽象类,真正的实现类是BackStackRecord,然后进行commit,判断用户是否添加到回退栈中,然后将操作交给FragmentManagerImpl,进行事务的入队,开始处理事务集合,最终进行到moveToState方法,执行Fragment的生命周期的方法。

来自面试官的灵魂拷问:Fragment是如何进行事务管理的?_第5张图片
image
ArrayList mBackStackIndices;
ArrayList mAvailBackStackIndices;  //可用的回退栈的索引
image.gif

这里要说一下这两个数组,只有加入到回退栈,才会进行管理。回退栈的添加需要手动调用具体方法。

一个是管理回退栈中的所有事务,一个是用于记录索引的。如上图所示,比如mBackStackIndices数组中有5个BackStackRecord,当你移除掉1,3两个(同时把这两个位置置为null),然后就会把索引添加到mAvailBackStackIndices数组(第一个和第二个位置上),相当于在把两个位置记录下来,下次再添加到返回栈中,通过判断mAvailBackStackIndices中的内容就可以判断出mBackStackIndices中有哪些位置是空的,直接在空的位置中放入新添加BackStackRecord,提高数组的复用效率,之前的版本使用链表来管理的,但是效率上来讲还是不如这种方式来得高。

事务,一组操作全部要做,要么全部不干,流程绑在一起用,保证连续的操作是原子性的。

事务(FragmentTransaction)的四个commit的区别

commit() commitAllowingStateLoss() commitNow() commitNowAllowingStateLoss()

commit并不是立即执行的,它会被发送到主线程的任务队列当中,当主线程准备好执行它的时候执行。

//BackStackRecord.java
//commit
@Override
public int commit() {
    return commitInternal(false);
}
//commitInternal
int commitInternal(boolean allowStateLoss) {
        ···
        mCommitted = true;
        if (mAddToBackStack) {
            mIndex = mManager.allocBackStackIndex(this);
        } else {
            mIndex = -1;
        }
        mManager.enqueueAction(this, allowStateLoss);
        return mIndex;
}
image.gif

然后是FragmentImpl中的enqueueAction方法

public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
    if (!allowStateLoss) {
        checkStateLoss();
    }
    synchronized (this) {
        if (mDestroyed || mHost == null) {
            if (allowStateLoss) {
                // This FragmentManager isn't attached, so drop the entire transaction.
                return;
            }
            throw new IllegalStateException("Activity has been destroyed");
        }
        if (mPendingActions == null) {
            mPendingActions = new ArrayList<>();
        }
        mPendingActions.add(action);
        scheduleCommit();
    }
}
image.gif

scheduleCommit方法

void scheduleCommit() {
    synchronized (this) {
        boolean postponeReady =
                mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
        boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
        //通过handler发送
        if (postponeReady || pendingReady) {
            mHost.getHandler().removeCallbacks(mExecCommit);
            mHost.getHandler().post(mExecCommit);
            updateOnBackPressedCallbackEnabled();
        }
    }
}
image.gif

popBackStack()也是这样,发送到主线程的任务队列中,也就是说他们都是异步的。

commitNow是同步的,保证立即执行,但是不会加入到回退栈当中,因为可能会扰乱回退栈中内容的顺序,还有另外两个方法的详细内容可以看这篇文章。

@Override
public void commitNow() {
    disallowAddToBackStack();
    mManager.execSingleAction(this, false);
}

 public void execSingleAction(OpGenerator action, boolean allowStateLoss) {
        ···
        ensureExecReady(allowStateLoss);
        if (action.generateOps(mTmpRecords, mTmpIsPop)) {
            mExecutingActions = true;
            try {
                removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
            } finally {
                cleanupExec();
            }
        }
        updateOnBackPressedCallbackEnabled();
        doPendingDeferredStart();
        burpActive();
    }
image.gif

然后是ensureExecReady方法

private void ensureExecReady(boolean allowStateLoss) {
    ···
    mExecutingActions = true;
    try {
        executePostponedTransaction(null, null);
    } finally {
        mExecutingActions = false;
    }
}

image.gif

最终执行到

public void completeTransaction() {
    final boolean canceled;
    canceled = mNumPostponed > 0;
    FragmentManagerImpl manager = mRecord.mManager;
    final int numAdded = manager.mAdded.size();
    for (int i = 0; i < numAdded; i++) {
        final Fragment fragment = manager.mAdded.get(i);
        fragment.setOnStartEnterTransitionListener(null);
        if (canceled && fragment.isPostponed()) {
            fragment.startPostponedEnterTransition();
        }
    }
    mRecord.mManager.completeExecute(mRecord, mIsBack, !canceled, true);
}
image.gif

Fragment状态的保存和恢复

状态保存

当Activity在后台被回收或者App的进程处于Sleep状态等特殊情况时候会调用ActivityThread.callActivityOnSaveInstanceState => Instrumentation.callActivityOnSaveInstanceState => Activity.performSaveInstanceState => Activity.onSaveInstanceState => FragmentActivity.onSaveInstanceState 来保存数据,最终将数据保存到ActivityClientRecord的成员变量state中。

状态恢复

当Activity恢复的时候创建一个新的Activity,执行FragmentActivity.onCreate,然后执行FragmentController.restore=>FragmentManagerImpl.restoreSaveState恢复数据,然后FragmentController.dispatchonCreate =>FragmentManagerImpl.dispatchCreate`重走Fragment的生命周期。

fragment的预加载和懒加载

Fragment预加载

主流的APP中首页底部一般有四个按钮,点击底部的不同界面,分别显示不同的界面(Fragment),这个时候通常使用ViewPager+多个Fragment。如果你想要预先加载多个Fragment,通常使用以下方法进行Fragment的预加载

viewPager.setOffscreenPageLimit(int count);
image.gif

预加载viewpager.setOffscreenPageLimit(),这个设置的值有两层含义: 一是 ViewPager 会预加载几页; 二是 ViewPager 会缓存 2n+1 页(n为设置的值),但是如果你不想与预加载数据呢,设置值为0如何?通过查看源码,如果传入的值小于1,那么ViewPager就会把预加载数量设置成默认值,而默认值就是1,所以说就算你传入了0,ViewPager还是会预先加载一个界面

Fragment懒加载

所谓的懒加载,就是结合生命周期判断fragment是否为可见状态,根据可见状态判断是否加载数据。

普通的Fragment(非AndroidX)

使用setUserVisibleHint或者onHiddenChanged进行辅助判断,show和hide的时候不会调用生命周期的方法,而是会调用onHiddenChanged,详细的设置可以参考该文章

androidx懒加载

androidx中setUserVisibleHint已经被废弃,推荐我们使用FragmentTransaction的setMaxLifecycle方法,而且androidx对Fragment中的生命周期进行了大规模的重构,已经和之前的版本有很大不同了,先来看下面这张图

来自面试官的灵魂拷问:Fragment是如何进行事务管理的?_第6张图片
image

图中展示了Fragment状态间切换会执行生命周期以及Lifecycle.State对应的Fragment状态,setMaxLifecycle方法要求传入的状态至少为CREATED

/**
 * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is
 * already above the received state, it will be forced down to the correct state.
 *
 * 

The fragment provided must currently be added to the FragmentManager to have it's * Lifecycle state capped, or previously added as part of this transaction. The * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise * an {@link IllegalArgumentException} will be thrown.

* * @param fragment the fragment to have it's state capped. * @param state the ceiling state for the fragment. * @return the same FragmentTransaction instance */ @NonNull public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, @NonNull Lifecycle.State state) { addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state)); return this; }
image.gif

下面来看看懒加载的方案,我们可以发现FragmentPagerAdapter的构造方法过时了

public TabFragmentPagerAdapter(FragmentManager fm, List list) {
    super(fm); //该方法过时了
    this.mlist = list;
}
image.gif

可以看到内部又调用了两个参数放到方法,我们来看一下这个新的构造方法

public FragmentPagerAdapter(@NonNull FragmentManager fm,
        @Behavior int behavior) {
    mFragmentManager = fm;
    mBehavior = behavior;
}
image.gif

多了一个int类型的参数

@Deprecated
public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;

public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;
image.gif

一个参数的构造方法默认传入BEHAVIOR_SET_USER_VISIBLE_HINT,如果你使用两个参数的构造方法,传入的是BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,会调用setMaxLifecycle()方法将上一个Fragment的状态设置为STARTED,将当前要显示的Fragment的状态设置为RESUMED;如果设置的值为BEHAVIOR_SET_USER_VISIBLE_HINT,最终调用到的还是setUserVisibleHint()方法,懒加载方法参考第一种。

调用两个参数的构造方法后,在Fragment变为可见时都会调用onResume方法,我们可以用这一点来实现懒加载

  • 将Fragment加载数据的逻辑放到onResume()方法中,这样就保证了Fragment可见时才会加载数据。
  • 声明一个变量标记是否是首次执行onResume()方法,因为每次Fragment由不可见变为可见都会执行onResume()方法,需要防止数据的重复加载。此外,如果我们使用的是FragmentPagerAdapter,切换导致Fragment被销毁时是不会执行onDestory()和onDetach()方法的,只会执行到onDestroyView()方法,因此在onDestroyView()方法中我们还需要将这个变量重置,否则当Fragment再次可见时就不会重新加载数据了。

Fragment之间传递数据

从 Fragment 1.3.0-alpha04 开始,每个 FragmentManager 都会实现 FragmentResultOwner。这意味着 FragmentManager 可以充当 Fragment 结果的集中存储区。此更改通过设置 Fragment 结果并监听这些结果,而不要求 Fragment 直接引用彼此,让单独的 Fragment 相互通信。具体的实现可以查看这里。

在父级 Fragment 和子级 Fragment 之间传递结果,如需将结果从子级 Fragment 传递到父级 Fragment,父级 Fragment 在调用 setFragmentResultListener() 时应使用 getChildFragmentManager() 而不是 getParentFragmentManager()。同时也可以通过ChildFragment => ParentFragment进行传递。

通过共享ViewModel的形式也可以进行数据传递,或者通过EventBus,LiveDataBus等都可以。

FragmentPagerAdapter, FragmentStatePagerAdapter 的区别

FragmentPagerAdapter 中,即使fragment不可见了,他的视图可能会 destory(执行 onViewDestory,是否执行与setOffscreenPageLimit 方法设置的值有关),但是他的实例仍然会存在于内存中。当较多的fragment时, 会占用较大的内存。

FragmentSatePagerAdapter 中,当fragment不可见时,可能会将fragment的实例也销毁(执行 onDestory,是否执行与setOffscreenPageLimit 方法设置的值有关)。所以内存开销会小些, 适合多fragment的情形。

上面我们讲过setOffscreenPageLimit 方法设置的默认值是1。这个设置的值有两层含义:一是 ViewPager 会预加载几页;二是 ViewPager 会缓存 2n+1 页(n为设置的值)。

明白了setOffscreenPageLimit 方法的含义后就明白了在 ViewPager 中Fragment的生命周期了:在 FragmentPagerAdapter 中 setOffscreenPageLimit 的值影响的是 onViewDestory 方法。当缓存的 fragment 超过 setOffscreenPageLimit 设置的值后,那些 fragment 的onViewDestory 方法会回调;在 FragmentStatePagerAdapter 中,当缓存的 fragment 超过 setOffscreenPageLimit 设置的值后。那些 fragment 的onDestory 方法会回调。

总结起来就是:ViewPager 中的 fragment 是否执行 onViewDestory 或者 onDestory 与 setOffscreenPageLimit 方法设置的值有关。

androidx的中一些新特性

FragmentContainerView、FragmentFactory,可以参考这篇文章,还有Google建议使用这些Fragment的新特性

你可能感兴趣的:(来自面试官的灵魂拷问:Fragment是如何进行事务管理的?)