飞道的博客

Fragment.setNextAnim(int) on a null object 解决方法及源码详解

478人阅读  评论(0)

前言

相信遇到这个问题的小伙伴,一定很无奈,要想知道这个问题的原因,并根治这个问题,需要研究fragment系列的大部分源码。网上很多文章,只是简单描述了这个问题如何出现(使用的方法很麻烦,下面我回介绍一种更容易去重现这个问题的方法),我在这里从源码的角度,详细分析一下,这个问题出现的原因,知道了原因你自然知道如何解决。当让我也会给出解决方法。

我的问题发生场景,可能和你的不太一样,我尽量从更普通使用方式的去分析这个问题。

分析这个问题的过程,按照我调试bug的过程,有点倒序的感觉。这样的陈述,可以更好的引导思路,可以知其所以然。

推荐一下我维护的fragment库,轻松实现单activity+多fragment 的框架,https://github.com/JantHsueh/Fragmentation

报错

先来看一下报错:

Attempt to invoke virtual method 'void androidx.fragment.app.Fragment.setNextAnim(int)' on a null object reference
        at androidx.fragment.app.BackStackRecord.executePopOps(BackStackRecord.java:831)
        at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManager.java:2622)
        at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManager.java:2411)
        at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManager.java:2366)
        at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:2273)
        at androidx.fragment.app.FragmentManagerImpl.executePendingTransactions(FragmentManager.java:814)
        at androidx.fragment.app.FragmentationMagician$4.run(FragmentationMagician.java:134)
        at androidx.fragment.app.FragmentationMagician.hookStateSaved(FragmentationMagician.java:193)
        at androidx.fragment.app.FragmentationMagician.executePendingTransactionsAllowingStateLoss(FragmentationMagician.java:131)
        at me.yokeyword.fragmentation.TransactionDelegate.safePopTo(TransactionDelegate.java:579)
        at me.yokeyword.fragmentation.TransactionDelegate.access$1100(TransactionDelegate.java:33)
        at me.yokeyword.fragmentation.TransactionDelegate$7.run(TransactionDelegate.java:213)
        at me.yokeyword.fragmentation.queue.ActionQueue.handleAction(ActionQueue.java:53)
        at me.yokeyword.fragmentation.queue.ActionQueue.enqueueAction(ActionQueue.java:45)
        at me.yokeyword.fragmentation.queue.ActionQueue.access$000(ActionQueue.java:17)
        at me.yokeyword.fragmentation.queue.ActionQueue$1.run(ActionQueue.java:37)
        at android.os.Handler.handleCallback(Handler.java:888)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:213)
        at android.app.ActivityThread.main(ActivityThread.java:8147)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)

错误信息,相信你已经看了N遍了,下面直接看是分析

重现步骤

为了方便后面文章的介绍,先规定一个“术语”,安卓app有一种特殊情况,就是 app运行在后台的时候,系统资源紧张的时候导致把app的资源全部回收(杀死app的进程),这时把app再从后台返回到前台时,app会重启。这种情况下文简称为:“内存重启”。(屏幕旋转等配置变化也会造成当前Activity重启,本质与“内存重启”类似)

模拟内存重启很简单,在开发者模式中

  1. 把“不保留活动” 选中
  2. 把“后台进行限制” 设置为不允许后台进程

这样你只要按home 键,就可以模拟内存重启

下面用更为普通的方式描述:

1、上面一系列操作的目的是,在fragment栈 回退栈mBackStack中同一个类有两个连续的name相同的BackStackRecord实例

两个BackStackRecord的实例B1、B2,分别对应Fragment的F1,F2 。为了与实际代码的顺序一致,我们把B2、F2 称为栈顶的数据

2、调用下面popBackStack函数对栈顶的BackStackRecord B2 进行弹出操作

FragmentManager.java 中的函数

    @Override
    public void popBackStack(@Nullable final String name, final int flags) {
        enqueueAction(new PopBackStackState(name, -1, flags), false);
    }

3、这时候再去更新Fragment F1的状态 ,就会出现这个错误。

由于我代码,可能和你不太一样,所以我的重新步骤可能和你也不尽相同。下面针对我的特殊情况,

1、未登录状态,打开app,登录后,到主界面
2、点击home键
3、打开app,到主界面,退出登录,再次登录,到主界面
4、点击home键
5、再次打开app,出现崩溃

源码分析

错误是在下面这个地方报出来的。
可以看到是fragment为null导致的,下面具体分许这个函数

上面截图的代码

BackStackRecord.java 中的函数

  • mOps 是当前的BackStackRecord实例所包含的操作,例如:remove,add等等
  • Op是一个具体的操作,所以一个mOps 中包含多个 Op

可以看到executePopOps 函数中,判断op.cmd的类型和执行的操作,正好相反。它指定了Fragment“原路回复”的起点。

为了能让fragment“原路返回”,比如当前的状态是 Fragment.STARTED 要转为Fragment.CREATED

状态是 Fragment.STARTED ,说明已经经过了Fragment.INITIALIZINGFragment.CREATEDFragment.STARTED每个状态相应的操作,为了可以无异常的返回,正常回调该调用的生命周期。转为状态Fragment.CREATED 需要“反”执行Fragment.STARTED,Fragment.ACTIVITY_CREATED的状态。这些操作是在函数moveToState中来实现的

    void executePopOps(boolean moveToState) {
        for (int opNum = mOps.size() - 1; opNum >= 0; opNum--) {
            final Op op = mOps.get(opNum);
            Fragment f = op.fragment;
            if (f != null) {
                f.setNextTransition(FragmentManagerImpl.reverseTransit(mTransition),
                        mTransitionStyle);
            }
            switch (op.cmd) {
                case OP_ADD:
                    f.setNextAnim(op.popExitAnim);
                    mManager.removeFragment(f);
                    break;
                case OP_REMOVE:
                    f.setNextAnim(op.popEnterAnim);
                    mManager.addFragment(f, false);
                    break;
                case OP_HIDE:
                    f.setNextAnim(op.popEnterAnim);
                    mManager.showFragment(f);
                    break;
                case OP_SHOW:
                    f.setNextAnim(op.popExitAnim);
                    mManager.hideFragment(f);
                    break;
                case OP_DETACH:
                    f.setNextAnim(op.popEnterAnim);
                    mManager.attachFragment(f);
                    break;
                case OP_ATTACH:
                    f.setNextAnim(op.popExitAnim);
                    mManager.detachFragment(f);
                    break;
                case OP_SET_PRIMARY_NAV:
                    mManager.setPrimaryNavigationFragment(null);
                    break;
                case OP_UNSET_PRIMARY_NAV:
                    mManager.setPrimaryNavigationFragment(f);
                    break;
                default:
                    throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
            }
            if (!mReorderingAllowed && op.cmd != OP_REMOVE && f != null) {
                mManager.moveFragmentToExpectedState(f);
            }
        }
        if (!mReorderingAllowed && moveToState) {
            mManager.moveToState(mManager.mCurState, true);
        }
    }

那么经过分析可以知道mOps 里的数据导致f为null,继续看看mOps是哪里来的

FragmentManager.java 中的函数,从上图可以看出records的数据已经是错的

    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);
            //根据是否是pop操作,来执行不同的操作
            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();
            }
        }
    }

我们继续往上追溯,发现是在records是在这里传入的removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop)

继续往上,看看变量mTmpRecords 是如何传入这个值的。

上面截图的函数是在,FragmentManager.java 中的函数

    public boolean execPendingActions() {
        //mTmpRecords 是在这里进行初始化
        ensureExecReady(true);

        boolean didSomething = false;
        //mTmpRecords 在函数generateOpsForPendingActions中进行赋值操作,list是址传递
        while (generateOpsForPendingActions(mTmpRecords, mTmpIsPop)) {
            mExecutingActions = true;
            try {
                removeRedundantOperationsAndExecute(mTmpRecords, mTmpIsPop);
            } finally {
                cleanupExec();
            }
            didSomething = true;
        }

        doPendingDeferredStart();
        burpActive();

        return didSomething;
    }

从函数名也能看出,这个是用来生成待执行的Ops

    private boolean generateOpsForPendingActions(ArrayList<BackStackRecord> records,
            ArrayList<Boolean> isPop) {
        boolean didSomething = false;
        synchronized (this) {
            if (mPendingActions == null || mPendingActions.size() == 0) {
                return false;
            }

            final int numActions = mPendingActions.size();
            for (int i = 0; i < numActions; i++) {
            //generateOps 是接口OpGenerator的函数,
            // 实现这个接口有两个类 1、BackStackRecord  2、PopBackStackState
                didSomething |= mPendingActions.get(i).generateOps(records, isPop);
            }
            mPendingActions.clear();
            mHost.getHandler().removeCallbacks(mExecCommit);
        }
        return didSomething;
    }

1、BackStackRecord 中的generateOps实现很简单,主要就是把自己的实例添加到records中,可以看到records的类型就是BackStackRecord

2、PopBackStackState 中的generateOps实现,才是导致这个问题的根本原因。

我们重新跑一下程序,看看这里的mTmpRecords 是怎么生成的。下面的过程是在上面过程的前面。

关于mActive 和 mAdd 的说明,可以看这个difference between mAdded & mActive in source code of support.FragmentManager

可以看到刚进去的时候records的个数是1,mBackStack的个数是3,执行的是LoginFragment cmd = 3的操作,也就是Remove的操作
当该PopBackStackState 函数执行完的时候,records的数量变为了3个,mBackStack的个数是2。其中mBackStack中名称为LoginFragment的,被添加到了records中。
这就是错误的根本原因,本来只想弹出一个LoginFragment,结果弹出了两个LoginFragment


继续往下执行,把两个LoginFragment的实例,设置动画结束后的状态


进到这个函数里面,设置动画执行完的回调,最终会执行到makeInactive()

函数makeInactive(),把Fragment置位初始状态,有个重要的参数 设置mIndex = -1

下面从源码来分析一下,为什么会把连续的两个name为LoginFragment都弹出

   // 从回退栈中弹出BackStackRecord,根据name或者id,来选择弹出哪些BackStackRecord。
   // BackStackRecord就是一系列的Fragment操作,比如add、remove等等,
   // BackStackRecord加入到回退栈,就会保存在mBackStack 中
    boolean popBackStackState(ArrayList<BackStackRecord> records, ArrayList<Boolean> isRecordPop,
            String name, int id, int flags) {
         //如果mBackStack  为空,没有可以弹出的  直接返回, 
        if (mBackStack == null) {
            return false;
        }
        
        if (name == null && id < 0 && (flags & POP_BACK_STACK_INCLUSIVE) == 0) {
            //name 为空 并且 id 小于0,没有弹出不包括自己,就只弹出数组的最后一个BackStackRecord
            int last = mBackStack.size() - 1;
            if (last < 0) {
                return false;
            }
            records.add(mBackStack.remove(last));
            isRecordPop.add(true);
        } else {
           //根据name 或者id来弹出BackStackRecord,POP_BACK_STACK_INCLUSIVE 是否弹出自己
            int index = -1;
            if (name != null || id >= 0) {
                // If a name or ID is specified, look for that place in
                // the stack.
                index = mBackStack.size()-1;
                //index 为mBackStack中的索引,如果小于0就退出循环
                while (index >= 0) {
                    BackStackRecord bss = mBackStack.get(index);
                    if (name != null && name.equals(bss.getName())) {
                       //根据name,找到了最顶上的BackStackRecord ,索引位置为index
                        break;
                    }
                    if (id >= 0 && id == bss.mIndex) {
                        //根据id,找到了最顶上的BackStackRecord ,索引位置为index
                        break;
                    }
                    index--;
                }
                if (index < 0) {
                    //如果上面没有找到,那么index 小于0 退出上面的while循环,这里进行返回
                    return false;
                }
                if ((flags&POP_BACK_STACK_INCLUSIVE) != 0) {
                  //需要退出指定的
                    index--;
                    // Consume all following entries that match.
                    //根据name 或id,匹配BackStackRecord  直到找到不匹配的内容或到达堆栈底部为止。 否则,将删除直到但不包括该条目的所有BackStackRecord 。
                    //例如: A B A  -> 弹出name = A -> A B  
                    //      A B C C  -> 弹出name = C -> A B
                    while (index >= 0) {
                        BackStackRecord bss = mBackStack.get(index);
                        if ((name != null && name.equals(bss.getName()))
                                || (id >= 0 && id == bss.mIndex)) {
                            index--;
                            continue;
                        }
                        break;
                    }
                }
            }
            if (index == mBackStack.size()-1) {
               //
                return false;
            }
            for (int i = mBackStack.size() - 1; i > index; i--) {
               //根据index值,把需要弹出的BackStackRecord,添加到records,并从mBackStack中移除
                records.add(mBackStack.remove(i));
                isRecordPop.add(true);
            }
        }
        return true;
    }

重点是这里,假如有4个BackStackRecord 的实例
A B C1 C2 -> 弹出name = C -> A B
两个C 的实例C1 C2,如果根据name 执行弹出C ,那么C1,C2 都会被弹出(可能你只想弹出C2,你也并不知道此时有两个连续的C实例),BackStackRecord 里的操作相对应的fragment都会进行“反”操作,也就是上面看到的fragment为空。

如果再次使用C1 op 里面的fragment,就会出现这个错误。下面我们通过代码具体分析。

那么问题来了,为什么连续退出两个LoginFragment的实例,会导致这个崩溃呢?

这里到了上面步骤的第6步,点击home键,执行saveAllState()把Fragment的信息存储起来,因为这个LoginFragment 已经被置位初始状态,保存在mBackStack 里的值,也就是初始状态的。

BackStackState 是用于存储BackStackRecord的类,BackStackRecord中的op,op 里面有6个 参数值,在BackStackState 中用mOps 保存每个op 的参数值,所以的大小为mOps = new int[numOps * 6];

再次打开app,执行restoreAllState()恢复Fragment 的数据

关于saveAllState()restoreAllState()的源码,我在这篇博客中,有分析

业务代码继续往下执行,执行pop SplashFragment的代码,弹出SplashFragment上面所有的backstackRecord

这段代码的逻辑和上面一样,只不过这里按照预期的执行,把mBackStack中的backstackRecord全部弹出,放在局部变量records 中,也就是最上面的mTempRecords,所以导致后面执行,出现空异常。

解决方法

知道了原因,那么解决方法很明了

第一种:

最好在内存重启的时候,判断一下,当前已经恢复的fragment有哪些。不要重复创建这些(严谨的说是这个,因为如果是ABABAB 并不会有问题,但是AAA或者ABB就可能会出问题)fragment,避免连续的name相同的回退栈。

第二种:

在调用pop操作的时候,确保知道你回弹出哪些BackStackRecord,并且后续的使用不涉及到已经弹出的BackStackRecord


转载:https://blog.csdn.net/xx326664162/article/details/103733622
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场