前言
相信遇到这个问题的小伙伴,一定很无奈,要想知道这个问题的原因,并根治这个问题,需要研究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重启,本质与“内存重启”类似)
模拟内存重启很简单,在开发者模式中
- 把“不保留活动” 选中
- 把“后台进行限制” 设置为不允许后台进程
这样你只要按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.INITIALIZING
,Fragment.CREATED
, Fragment.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) {
// POP_BACK_STACK_INCLUSIVE 是否弹出自己
//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