欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

源码分析自定义ViewGroup中Fragment无法显示的问题

程序员文章站 2022-06-08 17:41:28
...

一、背景

昨天接到同学的一个问题:用了别人的自定义侧滑菜单控件,这个控件继承自ViewGroup,想通过左侧菜单里的列表,更改右侧界面显示的内容,内容通过Fragment来显示。问题来了:

  1. 点击列表时,replace一个新的Fragment,无法显示
  2. 在onCreate()中直接replace,可以显示

二、填坑过程

这里完全是个自嘲过程,可以直接跳过。

因为调试过几次后,觉得应该从源码找原因,简单地看了下,没找到突破口,又继续调试。最后原因还是通过源码找到的,但这个教训还是得记录下来。

他的代码看上去没有正常,找bug的思路有时也挺逗的,毫无保留地怀疑自己的一切逻辑,O(∩_∩)O哈哈~。主要从这几方面分析:

  1. 首先,看时候点击事件是否抵达。结果:正常
  2. 检查Fragment生命周期,是否启动,即是否调用了onCreateView()。结果:已启动
  3. 检查Fragment生命周期,是否因某个原因关闭了,即是否调用了onDestroyView()。结果:未关闭
  4. 也可以再去检查其它生命周期:onAttach()、onCreate()、onActivityCreated()、onStart()、onResume(),也肯定一样:正常
  5. 这里,就发现:Fragment正常创建出来了,但就是没显示出来
  6. 甚至怀疑其他地方动过手脚,就在自定义ViewGroup外面,创建一个类似的功能,一个button点击后,显示一个Fragment。结果:正常
  7. 说明就是自定义ViewGroup内部的问题。查看代码,并没有修改过contentView(用于显示Fragment的FrameLayout)的内容
  8. 难道没有重新绘制?在ViewGroup里添加一个TextView,button点击后,除了显示Fragment,同时把TextView的内容修改掉,使用计数器,这样显示的内容,每次都不一样。结果:TextView的内容正常修改了,但Fragment依旧没显示
  9. 中途,还有天马行空、不合逻辑地怀疑:构造方法少了一个三个参数的,毫无疑问,补上无果,否则早就报错了;宽、高都是以屏幕为标准,改成固定值,结果:还是无效。
  10. 同学说点击之后就没有,而里面重写了onInterceptTouchEvent()、onTouchEvent(),是否跟这有关。成功引导我去查了一遍,甚至把它两干掉。结果:无效
  11. 最后代码抽丝剥茧,只剩下三个构造、onMeasure()、onLayout()。依旧不行
  12. 不自定义ViewGroup的话,直接在xml布局中使用FrameLayout或LinearLayout,肯定可以。就把继承改成FrameLayout,onMeasure()和onLayout()去掉。结果:正常
  13. 继承FrameLayout,把原来的onMeasure()与onLayout()保留。结果:不显示
  14. 最后,把onMeasure()与onLayout()里,两个为了性能而加的开关给关掉。结果:正常

强迫症的我,最终还是把问题给解决了。但,原因不知道,也肯定难受,回到家又分析了一波Fragment的源码(自己看比较吃力,后来还是跟着后面的参考博客来分析的),主要查看replace()和commit()后做了哪些事情。

三、原因分析

3.1 问题复原

找到了问题,逆向分析原因。知道原因后,再来正向具体分析,就会一目了然。用一个简单的demo来复现:

/**
 * 自定义ViewGroup
 */
public class TestViewGroup extends ViewGroup {
    private boolean isMeasured;
    public TestViewGroup(Context context) {
        super(context);
    }
    public TestViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public TestViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        logD("onMeasure: width.mode=%d, width.size=%d, height.mode=%d, height.size=%d",
                (widthMeasureSpec & 3 << 30) >> 30, widthMeasureSpec & 0x3FFF,
                (heightMeasureSpec & 3 << 30) >> 30, heightMeasureSpec & 0x3FFF
        );
        if (!isMeasured) {
            final int count = getChildCount();
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                int childW = child.getLayoutParams().width;
                int childWidthSpec = MeasureSpec.makeMeasureSpec(childW, MeasureSpec.getMode(widthMeasureSpec));
                child.measure(childWidthSpec, heightMeasureSpec);
            }
            isMeasured = true;
        }
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        logD("onLayout: changed=%b, l=%d, t=%d, r=%d, b=%d",
                changed, l, t, r, b);

        if (!changed) {
            // 全部水平摆放
            final int count = getChildCount();
            int wOffset = 0;
            int w, h;
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);
                w = child.getMeasuredWidth();
                h = child.getMeasuredHeight();
                child.layout(wOffset, 0, wOffset + w, h);
                wOffset += w;
            }
            isLayouted = true;
        }
    }
}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="18dp"
    tools:context=".MainActivity">

    <com.zjun.demo.gradationview.TestViewGroup
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_marginBottom="8dp"
        android:background="#c5c6c7">

        <TextView
            android:id="@+id/tv_hello"
            android:layout_width="80dp"
            android:layout_height="match_parent"
            android:text="hello"
            android:background="#cac9aa"/>

        <FrameLayout
            android:id="@+id/fl_content"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:background="#dbbfb2"/>

    </com.zjun.demo.gradationview.TestViewGroup>

    <Button
        android:id="@+id/btn_replace"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="40dp"
        android:text="replace"
        android:textAllCaps="false"
        android:onClick="onClick"
        />

</LinearLayout>

MainActivity.java

// 省略其它的代码
public void onClick(View view) {
    switch (view.getId()) {
        case R.id.btn_replace:
            getSupportFragmentManager().beginTransaction().replace(R.id.fl_content, new TestFragment()).commit();
            break;
        default: break;
    }
}

UI效果:
源码分析自定义ViewGroup中Fragment无法显示的问题

点击按钮replace后,后面那块区域没有任何反应。通过log日志,可以看到每次点击,都会有日志输出(当前手机系统是4.4.2,而另一个6.0的并没有打印,应该是对onMeasure和onLayout做了优化处理):
源码分析自定义ViewGroup中Fragment无法显示的问题

3.2 源码分析

我们都知道,只有布局变动的情况下,才会重新测量。Fragment的replace()会引起布局的变动吗?不用想就知道肯定会(当时肯定想了),不然Fragment里的界面怎么能展示出来。现在就从源码找找
getSupportFragmentManager().beginTransaction().replace(R.id.fl_content, new TestFragment()).commit();
这句代码,具体做了哪些事情,从而让界面变化的。这里仅做快速简单分析,具体参考底下的博客。另外给这作者点个赞,首次看到源码上面带上了类名的博客,这样便于查看与理解,学习了。

以下源码版本:27.1.1

3.2.1 getSupportFragmentManager()

getSupportFragmentManager()得到的是什么?

// FragmentActvity:
public FragmentManager getSupportFragmentManager() {
    return mFragments.getSupportFragmentManager();
}

// FragmentController:
public FragmentManager getSupportFragmentManager() {
    return mHost.getFragmentManagerImpl();
}

// FragmentHostCallback:
FragmentManagerImpl getFragmentManagerImpl() {
    return mFragmentManager;
}

// FragmentHostCallback:
final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl();

// 类FragmentManagerImpl在FragmentManager.java文件内,但不是内部类,而是同级:
final class FragmentManagerImpl extends FragmentManager implements LayoutInflater.Factory2

结论:getSupportFragmentManager()得到的是一个FragmentManagerImpl对象

3.2.2 beginTransaction()

再看beginTransaction()又是怎么开启事务的:

// FragmentManager:这是抽象类里的抽象方法:
public abstract FragmentTransaction beginTransaction();

// FragmentManagerImpl:由这个实现类来实现此方法:
@Override
public FragmentTransaction beginTransaction() {
    return new BackStackRecord(this);
}

// BackStackRecord:
final class BackStackRecord extends FragmentTransaction implements
        FragmentManager.BackStackEntry, FragmentManagerImpl.OpGenerator{
    public BackStackRecord(FragmentManagerImpl manager) {
        mManager = manager;
    }
}

所以,beginTransaction()是获取到了一个BackStackRecord对象

3.2.3 replace()

replace()是如何把我们xml里的R.id.fl_content替换成fragment的?

// BackStackRecord:以下代码都是在这个类中,一些非核心的代码都将省略
@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment) {
    return replace(containerViewId, fragment, null);
}

@Override
public FragmentTransaction replace(int containerViewId, Fragment fragment, String tag) {
    // 注意这个操作命令:OP_REPLACE
    doAddOp(containerViewId, fragment, tag, OP_REPLACE);
    return this;
}

/**
 * 添加操作前的准备工作
 */
private void doAddOp(int containerViewId, Fragment fragment, String tag, int opcmd) {
    if (containerViewId != 0) {
        ...
        // 同一个Fragment,不能同时添加到两个不一样的containerViewId中
        if (fragment.mFragmentId != 0 && fragment.mFragmentId != containerViewId) {
            throw new IllegalStateException("Can't change container ID of fragment "
                    + fragment + ": was " + fragment.mFragmentId
                    + " now " + containerViewId);
        }
        fragment.mContainerId = fragment.mFragmentId = containerViewId;
    }

    addOp(new Op(opcmd, fragment));
}

/**
 * 添加操作
 */
void addOp(Op op) {
    mOps.add(op);
    op.enterAnim = mEnterAnim;
    op.exitAnim = mExitAnim;
    op.popEnterAnim = mPopEnterAnim;
    op.popExitAnim = mPopExitAnim;
}

// mOps是个ArrayList集合
ArrayList<Op> mOps = new ArrayList<>()

/**
 * Op是一个静态内部类,用于存放待执行的操作
 */
static final class Op {
    int cmd;
    Fragment fragment;
    // 进出动画资源id
    int enterAnim;
    int exitAnim;
    // 再次进出的动画资源id。popEnterAnim的解释:An animation or animator resource ID used for the enter animation on the view of the fragment being readded or reattached caused by
    int popEnterAnim;
    int popExitAnim;

    Op() {
    }

    Op(int cmd, Fragment fragment) {
        this.cmd = cmd;
        this.fragment = fragment;
    }
}

OK,到这,可以看到replace()就是添加了一个要待执行的替换操作Op,里面保存了操作命令、要替换的Fragment、和进出动画的信息。自定义进出动画方式:setCustomAnimations()

replace就这样没了?没错,好戏在后头呢

3.2.4 commit()

// BackStateRecord:以下代码都在此类中
@Override
public int commit() {
    return commitInternal(false);
}

int commitInternal(boolean allowStateLoss) {
    mManager.enqueueAction(this, allowStateLoss);
}
/**
 * FragmentManagerImpl:以下代码都在此类中
 * 把当前对象加入准备执行的队列
 */
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
    ...
    scheduleCommit();
}

/**
 * 第一个重点来了:调度执行,通过Handler来执行
 */
private void scheduleCommit() {
    synchronized (this) {
        boolean postponeReady =
                mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
        boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
        if (postponeReady || pendingReady) {
            mHost.getHandler().removeCallbacks(mExecCommit);
            mHost.getHandler().post(mExecCommit);
        }
    }
}

/**
 * Hanlder.post()里面只能是Runnable
 */
Runnable mExecCommit = new Runnable() {
    @Override
    public void run() {
        execPendingActions();
    }
};

/**
 * 注意:这里开始跟参考博客不一样
 * Only call from main thread!
 */
public boolean execPendingActions() {
    ...
    removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
    ...
    doPendingDeferredStart();
}

先分析removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);

/**
 * FragmentManagerImpl:以下代码都在此类中
 * 移除mTmpRecords(ArrayList<BackStackRecord>)里多余的操作,并执行这些操作
 */
private void removeRedundantOperationsAndExecute(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop){
    executeOpsTogether(records, isRecordPop, startIndex, recordNum);
}

/**
 * 一起执行操作
 */
private void executeOpsTogether(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
    ...
    record.expandOps(mTmpAddedFragments, oldPrimaryNav);
}

/**
 * BackStackRecord: 
 * 第二个重点:这里是OP_REPLACE命令唯一被处理的地方,但没看懂,个人觉得在这里主要保证了replace的唯一性
 */
Fragment expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav) {
            switch (op.cmd) {
                case OP_ADD:
                case OP_ATTACH:
                case OP_REMOVE:
                case OP_DETACH: 
                ...
                break;
                case OP_REPLACE: {
                    final Fragment f = op.fragment;
                    final int containerId = f.mContainerId;
                    boolean alreadyAdded = false;
                    for (int i = added.size() - 1; i >= 0; i--) {
                        final Fragment old = added.get(i);
                        if (old.mContainerId == containerId) {
                            if (old == f) {
                                alreadyAdded = true;
                            } else {
                                // This is duplicated from above since we only make
                                // a single pass for expanding ops. Unset any outgoing primary nav.
                                if (old == oldPrimaryNav) {
                                    mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, old));
                                    opNum++;
                                    oldPrimaryNav = null;
                                }
                                final Op removeOp = new Op(OP_REMOVE, old);
                                removeOp.enterAnim = op.enterAnim;
                                removeOp.popEnterAnim = op.popEnterAnim;
                                removeOp.exitAnim = op.exitAnim;
                                removeOp.popExitAnim = op.popExitAnim;
                                mOps.add(opNum, removeOp);
                                added.remove(old);
                                opNum++;
                            }
                        }
                    }
                    if (alreadyAdded) {
                        mOps.remove(opNum);
                        opNum--;
                    } else {
                        op.cmd = OP_ADD;
                        added.add(f);
                    }
                }
                break;
                case OP_SET_PRIMARY_NAV: 
                break;
            }
        }
    }

再来看doPendingDeferredStart();

/**
 * FragmengManager:以下代码都在此类中
 */
void doPendingDeferredStart() {
    startPendingDeferredFragments();
}

void startPendingDeferredFragments() {
    performPendingDeferredStart(f);
}

public void performPendingDeferredStart(Fragment f) {
    moveToState(f, mCurState, 0, 0, false);
}

/**
 * 第三个核心:这里代码筛选过,但也比较多,主要为了说明:
 *     - 这里的case,跟生命周期匹配,有顺序的
 *     - 所有的case都没有break,所以会fall through向下继续执行
 *     - 不同的状态进来,执行的起始点也就不一样
 *     - 里面能看到我们经常使用到的生命周期方法的调用
 * 回到核心,我们的R.id.fl_content,在这里转换成ViewGroup container,然后Fragment的布局通过performCreateView()填充成View后,再通过container.addView()进去了
 */
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
                 boolean keepActive) {
    switch (f.mState) {
        case Fragment.INITIALIZING:
            if (newState > Fragment.INITIALIZING) {

                dispatchOnFragmentPreAttached(f, mHost.getContext(), false);
                f.onAttach(mHost.getContext());

                dispatchOnFragmentAttached(f, mHost.getContext(), false);

                if (!f.mIsCreated) {
                    dispatchOnFragmentPreCreated(f, f.mSavedFragmentState, false);
                    f.performCreate(f.mSavedFragmentState);
                    dispatchOnFragmentCreated(f, f.mSavedFragmentState, false);
                } else {
                    f.restoreChildFragmentState(f.mSavedFragmentState);
                    f.mState = Fragment.CREATED;
                }

            }
            // fall through
        case Fragment.CREATED:

            if (newState > Fragment.CREATED) {
                if (!f.mFromLayout) {
                    ViewGroup container = null;
                    if (f.mContainerId != 0) {
                        if (f.mContainerId == View.NO_ID) {
                            throwException(new IllegalArgumentException(
                                    "Cannot create fragment "
                                            + f
                                            + " for a container view with no id"));
                        }
                        container = (ViewGroup) mContainer.onFindViewById(f.mContainerId);
                        if (container == null && !f.mRestored) {
                            String resName;
                            try {
                                resName = f.getResources().getResourceName(f.mContainerId);
                            } catch (Resources.NotFoundException e) {
                                resName = "unknown";
                            }
                            throwException(new IllegalArgumentException(
                                    "No view found for id 0x"
                                            + Integer.toHexString(f.mContainerId) + " ("
                                            + resName
                                            + ") for fragment " + f));
                        }
                    }
                    f.mContainer = container;
                    f.mView = f.performCreateView(f.performGetLayoutInflater(
                            f.mSavedFragmentState), container, f.mSavedFragmentState);
                    if (f.mView != null) {
                        f.mInnerView = f.mView;
                        f.mView.setSaveFromParentEnabled(false);
                        if (container != null) {
                            container.addView(f.mView);
                        }
                        if (f.mHidden) {
                            f.mView.setVisibility(View.GONE);
                        }
                        f.onViewCreated(f.mView, f.mSavedFragmentState);
                        dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
                                false);
                        // Only animate the view if it is visible. This is done after
                        // dispatchOnFragmentViewCreated in case visibility is changed
                        f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
                                && f.mContainer != null;
                    } else {
                        f.mInnerView = null;
                    }
                }

                f.performActivityCreated(f.mSavedFragmentState);
                dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
                if (f.mView != null) {
                    f.restoreViewState(f.mSavedFragmentState);
                }
                f.mSavedFragmentState = null;
            }
            // fall through
        case Fragment.ACTIVITY_CREATED:
            if (newState > Fragment.ACTIVITY_CREATED) {
                f.mState = Fragment.STOPPED;
            }
            // fall through
        case Fragment.STOPPED:
            if (newState > Fragment.STOPPED) {
                if (DEBUG) Log.v(TAG, "moveto STARTED: " + f);
                f.performStart();
                dispatchOnFragmentStarted(f, false);
            }
            // fall through
        case Fragment.STARTED:
            if (newState > Fragment.STARTED) {
                if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f);
                f.performResume();
                dispatchOnFragmentResumed(f, false);
                f.mSavedFragmentState = null;
                f.mSavedViewState = null;
            }
    }
}

// Fragment: 这里performCreateView调用的是生命周期里的onCreateView(),其它performXXX也一样
View performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
    // ...
    return onCreateView(inflater, container, savedInstanceState);
}

commit()终于告一段落。

3.2.5 流程复盘

回顾一下整个流程:

FragmentActivity.getSupportFragmentManager(): 
    <- FragmentController.getSupportFragmentManager()
    <- FragmentHostCallback.getFragmentManagerImpl()
    <- new FragmentManagerImpl()
FragmentManager.beginTranscation()
    <- new BackStackRecord(this):this 指 FragmentManagerImpl 对象

FragmentTranscation.replace(): 在实现类BackStackRecord里,把OP_REPLACE命令放入到待执行的操作集合中

BackStackRecord.commit(): 把提交操作交给 FragmentManagerImpl,然后通过 Handler 来 post Runnable 对象 mExecCommit,从这可以猜测 replace Fragment 可以在子线程中执行,经测试,没问题:

new Thread(new Runnable() {
    @Override
    public void run() {
        getSupportFragmentManager().beginTransaction().replace(R.id.fl_content, new TestFragment()).commit();
    }
}).start();

mExecCommit里主要做了两件事:

  • 把replace命令,在BackStackRecord中切换到下一步命令,核心方法:expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav)
  • 在FragmentManager中,根据生命周期,一步一步切换Fragment的当前状态,同时调用Fragment对应的生命周期。核心方法:void moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)

插曲:
中途分析moveToState(Fragment f, int newState, int transit, int transitionStyle, boolean keepActive)
f.performCreate(f.mSavedFragmentState);的时候,进入了生命周期的循环处理机制,可把我给绕晕了,后来跳过了。感兴趣的小伙伴可以去研究以下,大概包括了这几个类:LifecycleRegistry、Lifecycle、LifecycleOwner、LifecycleObserver、ObserverWithState、GenericLifecycleObserver及其实现类

3.2.6 addView(View)

addView(View)什么时候触发测量的呢?

// ViewGroup:
public void addView(View child, int index, LayoutParams params) {
    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

3.2.7 requestLayout()

replace()也就是把Fragment的界面添加到container容器中,那直接让container.requestLayout()测量,是否有效呢?
还是看下源码:

// View:
public void requestLayout() {

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null && !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

public boolean isLayoutRequested() {
    return (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
}

可以看到,container的requestLayout(),肯定是让其父控件去测量,一级一级传上去,直到Activity的根布局DecorView。然后再由根布局一级一级向下测量onMeasure(),和布局onLayout()

而TestViewGroup里已经关闭了测量,所以即使调用container.requestLayout()也无效

3.2.8 解决方法

知道原因后,解决办法就很多了:

最直接地解决方法就是去掉isMeasured,onLayout()里的changed判断也去掉

如果硬要避免重复测量与布局,提高性能的话,那可以在请求重新测量的时候,把测量标记和布局标记复位:

private boolean isMeasured;
private boolean isLayouted;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    if (!isMeasured) {
        ...
        isMeasured = true;
    }
    setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}

@Override
public void requestLayout() {
    isMeasured = false;
    isLayouted = false;
    super.requestLayout();
}


@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (!isLayouted) {
        ...
        isLayouted = true;
    }
}

再假设一种情况:如果已经封装好了,那就用万能的发射修改isMeasured;同时重写onLayout(),强制给给super的changed传true

四、总结

  1. 找问题还要从源码入手,才能找到根源,才能柳暗花明
  2. 需要多看源码,才能更快地抓住核心点
  3. 语言组织能力亟待提高,写了好三个晚上才搞定

五、参考

《通过源码解析 Fragment 启动过程》