美文网首页
一次FragmentTabHost切换Tab崩溃分析之旅

一次FragmentTabHost切换Tab崩溃分析之旅

作者: boboyuwu | 来源:发表于2017-09-01 14:20 被阅读448次

    最近在写项目时,由于使用了FragmentTabHost这个控件导致我每次点击第二个Tab按钮的时候就崩溃。
    然后开始搜stackoverflow,上面说原因在于MainActivity中使用了FragmentManager,MainActivty中的Fragment又嵌套了 viewpager+fragment这种模式所以嵌套的viewpager中不能再传FragmentManager,要传递getChildFragmentManager,兴冲冲的改过来后发现还是崩溃,最后分析源码才发现问题原因。

    而我们看看异常崩溃栈信息

    111.png

    问题就出现在初始化这里.来分析一下
    当我们点击第二个tab时候,我们看下FragmentTabHost的执行流程,首先会回掉这个方法

        @Override
        public void onTabChanged(String tabId) {
            if (mAttached) {
                final FragmentTransaction ft = doTabChanged(tabId, null);
                if (ft != null) {
                    ft.commit();
                }
            }
            if (mOnTabChangeListener != null) {
                mOnTabChangeListener.onTabChanged(tabId);
            }
        }
    

    FragmentTabHost自己实现了这个方法监听Tab点击,如果点击Tab改变情况下,就会调用FragmentTransaction 的commit方法提交事务,commit这个方法在FragmentTransaction 中是个抽象方法,那么我们就看看具体实现,找到getSupportFragmentManger()拿到的具体类一直点击最终会发现得到的是一个FragmentManagerImpl这个对象,它是FragmentManager一个内部类

    QQ截图20170901134823.png

    看下它拿到的FragmentTransaction是什么

        @Override
        public FragmentTransaction beginTransaction() {
            return new BackStackRecord(this);
        }
    
    123213.png

    ok,是这个类,好我们可以点进去查一下commit方法具体实现了

        @Override
        public int commit() {
            return commitInternal(false);
        }
    

    继续跟进

        int commitInternal(boolean allowStateLoss) {
            if (mCommitted) throw new IllegalStateException("commit already called");
            if (FragmentManagerImpl.DEBUG) {
                Log.v(TAG, "Commit: " + this);
                LogWriter logw = new LogWriter(TAG);
                PrintWriter pw = new PrintWriter(logw);
                dump("  ", null, pw, null);
                pw.close();
            }
            mCommitted = true;
            if (mAddToBackStack) {
                mIndex = mManager.allocBackStackIndex(this);
            } else {
                mIndex = -1;
            }
            mManager.enqueueAction(this, allowStateLoss);
            return mIndex;
        }
    

    注意这行
    mManager.enqueueAction(this, allowStateLoss);
    最后会调用FragmentManager的enqueueAction方法

    好我们看看FragmentManager中的enqueueAction方法怎么实现的

        /**
         * Adds an action to the queue of pending actions.
         *
         * @param action the action to add
         * @param allowStateLoss whether to allow loss of state information
         * @throws IllegalStateException if the activity has been destroyed
         */
        public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
            if (!allowStateLoss) {
                checkStateLoss();
            }
            synchronized (this) {
                if (mDestroyed || mHost == null) {
                    throw new IllegalStateException("Activity has been destroyed");
                }
                if (mPendingActions == null) {
                    mPendingActions = new ArrayList<>();
                }
                mPendingActions.add(action);
                scheduleCommit();
            }
        }
    

    最后一行可以看到调用了scheduleCommit方法

        /**
         * Schedules the execution when one hasn't been scheduled already. This should happen
         * the first time {@link #enqueueAction(OpGenerator, boolean)} is called or when
         * a postponed transaction has been started with
         * {@link Fragment#startPostponedEnterTransition()}
         */
        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);
                }
            }
        }
    

    在这里通过Handler post发送了一个消息,看看mExecCommit

        Runnable mExecCommit = new Runnable() {
            @Override
            public void run() {
                execPendingActions();
            }
        };
    
      /**
         * Only call from main thread!
         */
        public boolean execPendingActions() {
            ensureExecReady(true);
    
            boolean didSomething = false;
            while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
                mExecutingActions = true;
                try {
                    optimizeAndExecuteOps(mTmpRecords, mTmpIsPop);
                } finally {
                    cleanupExec();
                }
                didSomething = true;
            }
    
            doPendingDeferredStart();
    
            return didSomething;
        }
    

    分析了这么长现在回到了我们上面打印异常栈信息的开始,好继续分析,第一行就执行了ensureExecReady(true)这个方法,它是干什么用的呢?点进去看看

    
     /**
         * Broken out from exec*, this prepares for gathering and executing operations.
         *
         * @param allowStateLoss true if state loss should be ignored or false if it should be
         *                       checked.
         */
        private void ensureExecReady(boolean allowStateLoss) {
            if (mExecutingActions) {
                throw new IllegalStateException("FragmentManager is already executing transactions");
            }
    
            if (Looper.myLooper() != mHost.getHandler().getLooper()) {
                throw new IllegalStateException("Must be called from main thread of fragment host");
            }
    
            if (!allowStateLoss) {
                checkStateLoss();
            }
    
            if (mTmpRecords == null) {
                mTmpRecords = new ArrayList<>();
                mTmpIsPop = new ArrayList<>();
            }
            mExecutingActions = true;
            try {
                executePostponedTransaction(null, null);
            } finally {
                mExecutingActions = false;
            }
        }
    

    每次新提交的事务都会调用到execPendingActions()这个方法,在同一个FragmentManager中,如果第一个commit事务没有执行完毕,就又提交一个新事务那么就会判断mExecutingActions 这个变量,mExecutingActions 为true代表还有未处理完毕的事务,那么下个事务提交时mExecutingActions 为true就会抛出传说中的"FragmentManager is already executing transactions"异常

    那我们第一次点击commit这个值应该是false,好执行完这个方法会走下面optimizeAndExecuteOps方法,由于后面源码都比较长就截取片段了.

    接着会走这个方法

      if (startIndex != recordNum) {
                        executeOpsTogether(records, isRecordPop, startIndex, recordNum);
                    }
    

    next

            if (!allowOptimization) {
                FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, endIndex,
                        false);
            }
    

    next就会调用FragmentTransition的startTransitions方法

                if (isPop) {
                    calculatePopFragments(record, transitioningFragments, isOptimized);
                } else {
                    calculateFragments(record, transitioningFragments, isOptimized);
                }
    

    然后会走else
    接着走这个方法,会走manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);

       addToFirstInLastOut(transaction, op, transitioningFragments, false, isOptimized);
    
                if (fragment.mState < Fragment.CREATED && manager.mCurState >= Fragment.CREATED
                        && !transaction.mAllowOptimization) {
                    manager.makeActive(fragment);
                    manager.moveToState(fragment, Fragment.CREATED, 0, 0, false);
                }
    

    会走if然后就会调用这个Fragment的onCreate

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

    接着以此调用onViewCreate等Fragment生命周期方法

    f.onViewCreated(f.mView, f.mSavedFragmentState);
    

    我在BaseFragment中使用了这个库

    mLoadingAndRetryManager = new LoadingAndRetryManager(mActivity.get(), mOnLoadingAndRetryListener);
    

    由于BaseFragment使用并初始化了LoadingAndRetryManager这个控件,在new它的时候初始化执行这个方法


    2222.png

    addView,当最终add依附到最顶层的ViewGroup之后就会调用,dispatchAttachedToWindow方法,然后会调用FragmentTabHost的onAttachedToWindow方法,看看它的实现

     @Override
        protected void onAttachedToWindow() {
            super.onAttachedToWindow();
    
            final String currentTag = getCurrentTabTag();
    
            // Go through all tabs and make sure their fragments match
            // the correct state.
            FragmentTransaction ft = null;
            for (int i = 0, count = mTabs.size(); i < count; i++) {
                final TabInfo tab = mTabs.get(i);
                tab.fragment = mFragmentManager.findFragmentByTag(tab.tag);
                if (tab.fragment != null && !tab.fragment.isDetached()) {
                    if (tab.tag.equals(currentTag)) {
                        // The fragment for this tab is already there and
                        // active, and it is what we really want to have
                        // as the current tab.  Nothing to do.
                        mLastTab = tab;
                    } else {
                        // This fragment was restored in the active state,
                        // but is not the current tab.  Deactivate it.
                        if (ft == null) {
                            ft = mFragmentManager.beginTransaction();
                        }
                        ft.detach(tab.fragment);
                    }
                }
            }
    
            // We are now ready to go.  Make sure we are switched to the
            // correct tab.
            mAttached = true;
            ft = doTabChanged(currentTag, ft);
            if (ft != null) {
                ft.commit();
                mFragmentManager.executePendingTransactions();
            }
        }
    

    最终会调用 mFragmentManager.executePendingTransactions(); 这在异常栈信息上面也可以看到
    而这个方法实现在FragmentManagerImpl

        @Override
        public boolean executePendingTransactions() {
            boolean updates = execPendingActions();
            forcePostponedTransactions();
            return updates;
        }
    

    它会调用execPendingActions()这个方法。

    我们分析下问题的原因所在

    当我们点击第二个Tab按钮的时候,会调用Commit进行事务提交然后调用到execPendingActions()这个方法,这个方法在执行 optimizeAndExecuteOps(mTmpRecords, mTmpIsPop);方法之前会将mExecutingActions赋值为true,接着会调用一系列方法后走第二个Fragment的onCreate()方法,然后在创建LoadingAndRetryManager对象时会移除添加activity的contentview这样会触发onAttachToWindow这个方法,最终会调用
    mFragmentManager.executePendingTransactions();这一句,然后又会调用execPendingActions()这个方法, 在ensureExecReady方法中会判断mExecutingActions标记值,前面刚被设置为true,所以这里崩溃了

    这里有个小疑惑,我将创建LoadingAndRetryManager对象这句放在Fragment的onCreate()中会出现上述崩溃问题,而放在onViewCreate()就好了为什么呢?

    我们接着分析.
    其实原因就在这个方法里

     private void executeOpsTogether(ArrayList<BackStackRecord> records,
                ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
            final boolean allowOptimization = records.get(startIndex).mAllowOptimization;
            boolean addToBackStack = false;
            if (mTmpAddedFragments == null) {
                mTmpAddedFragments = new ArrayList<>();
            } else {
                mTmpAddedFragments.clear();
            }
            if (mAdded != null) {
                mTmpAddedFragments.addAll(mAdded);
            }
            Fragment oldPrimaryNav = getPrimaryNavigationFragment();
            for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
                final BackStackRecord record = records.get(recordNum);
                final boolean isPop = isRecordPop.get(recordNum);
                if (!isPop) {
                    oldPrimaryNav = record.expandOps(mTmpAddedFragments, oldPrimaryNav);
                } else {
                    oldPrimaryNav = record.trackAddedFragmentsInPop(mTmpAddedFragments, oldPrimaryNav);
                }
                addToBackStack = addToBackStack || record.mAddToBackStack;
            }
            mTmpAddedFragments.clear();
    
            if (!allowOptimization) {
                FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, endIndex,
                        false);
            }
            executeOps(records, isRecordPop, startIndex, endIndex);
    
            int postponeIndex = endIndex;
            if (allowOptimization) {
                ArraySet<Fragment> addedFragments = new ArraySet<>();
                addAddedFragments(addedFragments);
                postponeIndex = postponePostponableTransactions(records, isRecordPop,
                        startIndex, endIndex, addedFragments);
                makeRemovedFragmentsInvisible(addedFragments);
            }
    
            if (postponeIndex != startIndex && allowOptimization) {
                // need to run something now
                FragmentTransition.startTransitions(this, records, isRecordPop, startIndex,
                        postponeIndex, true);
                moveToState(mCurState, true);
            }
    
            for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
                final BackStackRecord record = records.get(recordNum);
                final boolean isPop = isRecordPop.get(recordNum);
                if (isPop && record.mIndex >= 0) {
                    freeBackStackIndex(record.mIndex);
                    record.mIndex = -1;
                }
                record.runOnCommitRunnables();
            }
            if (addToBackStack) {
                reportBackStackChanged();
            }
        }
    

    它在FragmentManager中,主要处理Fragment的状态,我们注意这段代码

            if (!allowOptimization) {
                FragmentTransition.startTransitions(this, records, isRecordPop, startIndex, endIndex,
                        false);
            }
            executeOps(records, isRecordPop, startIndex, endIndex);
    

    if中方法会启动事物,最终会调用Fragment的onCreate等系列生命周期方法,上面已经分析到,下面那行方法是做什么的呢

        /**
         * Run the operations in the BackStackRecords, either to push or pop.
         *
         * @param records The list of records whose operations should be run.
         * @param isRecordPop The direction that these records are being run.
         * @param startIndex The index of the first entry in records to run.
         * @param endIndex One past the index of the final entry in records to run.
         */
        private static void executeOps(ArrayList<BackStackRecord> records,
                ArrayList<Boolean> isRecordPop, int startIndex, int endIndex) {
            for (int i = startIndex; i < endIndex; i++) {
                final BackStackRecord record = records.get(i);
                final boolean isPop = isRecordPop.get(i);
                if (isPop) {
                    record.bumpBackStackNesting(-1);
                    // Only execute the add operations at the end of
                    // all transactions.
                    boolean moveToState = i == (endIndex - 1);
                    record.executePopOps(moveToState);
                } else {
                    record.bumpBackStackNesting(1);
                    record.executeOps();
                }
            }
        }
    
    

    注意最后一段 record.executeOps(); 当我们调用fragment的detach方法后只会把这个detach命令和fragment对象存储到这个Op对象里,调用这个方法后才会真正的从集合列表中移除fragment并且把fragment的mDetached变量设置为true。

    上面调用FragmentTransition.startTransitions后会调用到onCreate然后会创建LoadingAndRetryManager对象然后走onAttachToWindow

            for (int i = 0, count = mTabs.size(); i < count; i++) {
                final TabInfo tab = mTabs.get(i);
                tab.fragment = mFragmentManager.findFragmentByTag(tab.tag);
                if (tab.fragment != null && !tab.fragment.isDetached()) {
                    if (tab.tag.equals(currentTag)) {
                        // The fragment for this tab is already there and
                        // active, and it is what we really want to have
                        // as the current tab.  Nothing to do.
                        mLastTab = tab;
                    } else {
                        // This fragment was restored in the active state,
                        // but is not the current tab.  Deactivate it.
                        if (ft == null) {
                            ft = mFragmentManager.beginTransaction();
                        }
                        ft.detach(tab.fragment);
                    }
                }
            }
    

    此时fragment的mDetached值还是false,所以会走if,而第一个fragment的tab标签肯定不等于点击的第二个标签,所以又会走到else里,这样ft就被赋值了,然后走if又提交了事务导致上面分析的崩溃。

    而创建LoadingAndRetryManager对象放到onViewCreate里的话,在executeOpsTogether 方法中if执行后executeOps(records, isRecordPop, startIndex, endIndex)就会执行,这样第一个Fragment由于detach了mDetached就会赋值true,然后在onAttachedToWindow中循环tabs时候第一个tab就不走if,第二个tab的mDetached是false就会走if,并且由于第二个tab就是我们点击的当前tab所以里面也会走if,最终ft并不会被赋值,所以也不会走最后一行的if,事务就不会被提交二次。

    相关文章

      网友评论

          本文标题:一次FragmentTabHost切换Tab崩溃分析之旅

          本文链接:https://www.haomeiwen.com/subject/aoohjxtx.html