网站开发种类如何建立一个网站
前言
在使用Fragment的时候,在内存重启的后,很容易出现一些难以预期的bug,下面将继续一边分析源码,一边看看这个bug是怎么产生的。
这个报错的原因,可能和你的情况并不尽然相同。但是你可以通过对源码的理解,来加深对fragment的认识,从而能更优雅的解决问题。
推荐一下我维护的fragment库,轻松实现单activity+多fragment 的框架,https://github.com/JantHsueh/Fragmentation
报错
通俗的讲,就是在要使用Fragment 的 mHost 变量的时候,这个 变量为空。导致下面的报错
2019-12-30 09:39:55.755 28136-28136/com.icisoo.xw.staging E/MessageQueue-JNI: Exception in MessageQueue callback: handleReceiveCallback
2019-12-30 09:39:55.761 28136-28136/com.icisoo.xw.staging E/MessageQueue-JNI: java.lang.IllegalStateException: Fragment has not been attached yet.at androidx.fragment.app.Fragment.instantiateChildFragmentManager(Fragment.java:2383)at androidx.fragment.app.Fragment.getChildFragmentManager(Fragment.java:845)at me.yokeyword.fragmentation.debug.DebugStackDelegate.getChildFragmentRecords(DebugStackDelegate.java:183)at me.yokeyword.fragmentation.debug.DebugStackDelegate.addDebugFragmentRecord(DebugStackDelegate.java:211)at me.yokeyword.fragmentation.debug.DebugStackDelegate.getFragmentRecords(DebugStackDelegate.java:153)at me.yokeyword.fragmentation.debug.DebugStackDelegate.showFragmentStackHierarchyView(DebugStackDelegate.java:101)at me.yokeyword.fragmentation.debug.DebugStackDelegate$1.onClick(DebugStackDelegate.java:67)at android.view.View.performClick(View.java:6597)at me.yokeyword.fragmentation.debug.DebugStackDelegate$StackViewTouchListener.onTouch(DebugStackDelegate.java:257)at android.view.View.dispatchTouchEvent(View.java:12509)at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3030)at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2719)at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:440)at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)at android.app.Activity.dispatchTouchEvent(Activity.java:3400)at me.yokeyword.fragmentation.SupportActivity.dispatchTouchEvent(SupportActivity.java:59)at androidx.appcompat.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:69)at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:398)at android.view.View.dispatchPointerEvent(View.java:12752)at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5106)at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4909)at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4585)at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4642)at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4479)at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4445)at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4453)at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4426)at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7092)at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7061)at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7022)at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7195)at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:186)at android.os.MessageQueue.nativePollOnce(Native Method)at android.os.MessageQueue.next(MessageQueue.java:326)at android.os.Looper.loop(Looper.java:160)at android.app.ActivityThread.main(ActivityThread.java:6718)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)at
重现步骤
为了方便描述,下面SplashFragment 实例 简称S,LoginFragment 实例简称 L。第一次打开APP,通过replace(S)
启动S后,会add(L)
,hide(S)
如何模拟内存重启,查看这篇博客
1、第一次打开app,有 S1,L1
2、点击home键(模拟内存重启,系统会保存S1,L1),
3、打开app,有S2,L2,S1,L1 (S1,L1 是系统自动恢复的)
4、点击home键(模拟内存重启,系统会保存S2,L2,S1,L1)
5、打开app,有S3,L3,S2,L2,S1,L1(S2,L2,S1,L1 是系统自动恢复的)
这时候S1和L1的mHost 为空,导致上面这个错误
当然这样的启动过程,是有问题的,同学们也很容易想出解决办法。我们通过源码来深入分析一下,为什么会出现这个问题。
源码分析
通过追溯源码,发现在第二次内存重启后,出现S1 L1 的mHost 为空的问题。
到当前断点看下更深的函数调用
这里就是重现步骤中的第五步,其中 1和 2 的实例的mHost 为空,如下图(图中只展示1的变量情况):
通过不断调试,发现在重现步骤的第五步,系统恢复S2,L2,S1,L1,
这时候S2,L2,S1,L1 中的mHost 值都为null,图中只展示S2 的mHost数据
那么S2,L2 的mHost 是在哪里赋值的呢?为什么S1,L1的mHost 没有被赋值
在onCreate()函数执行的时候,调用了 dispatchStateChange(Fragment.CREATED);
最终调用下图中的moveToState() 来把mAdded 中的fragment 移动到指定的生命周期状态
在moveFragmentToExpectedState
中判断要置为哪种初始状态,在下图中的moveToState
中的函数设置了mHost的值。也就是说在初始化中,只有在mAdded中的Fragment 才会设置mHost。在恢复数据时,S1,L1 没有添加到mAdded,也就是说上次保存数据的时候,S1,L1没有被保存在mAdded中
来看看上一次保存fragment 实例时(重现步骤第3步)是什么样的
mActive 中有S2,L2,S1,L1 ,mAdded 中有S2,L2
下面是上图中的 saveAllState()的源码
Parcelable saveAllState() {// Make sure all pending operations have now been executed to get// our state update-to-date.//在保存状态前,保证所以的Transactions都已经执行,动画都已经结束(因为动画结束也会影响fragment的状态,例如animateRemoveFragment(),)。//在onSaveInstance后,不能提交事务.共同保证了,最终保存fragment状态的将不再改变forcePostponedTransactions();endAnimatingAwayFragments();execPendingActions();mStateSaved = true;mSavedNonConfig = null;if (mActive == null || mActive.size() <= 0) {return null;}// First collect all active fragments.int N = mActive.size();//FragmentState一个序列化的用来保存fragment的类FragmentState[] active = new FragmentState[N];boolean haveFragments = false;//遍历mActive中的每一个 fragmentfor (int i=0; i<N; i++) {Fragment f = mActive.valueAt(i);if (f != null) {if (f.mIndex < 0) {throwException(new IllegalStateException("Failure saving state: active " + f+ " has cleared index: " + f.mIndex));}haveFragments = true;//把fragment 转为FragmentState FragmentState fs = new FragmentState(f);//存储在数组active中,这个数组最终被保存active[i] = fs;if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {fs.mSavedFragmentState = saveFragmentBasicState(f);if (f.mTarget != null) {if (f.mTarget.mIndex < 0) {throwException(new IllegalStateException("Failure saving state: " + f+ " has target not in fragment manager: " + f.mTarget));}if (fs.mSavedFragmentState == null) {fs.mSavedFragmentState = new Bundle();}putFragment(fs.mSavedFragmentState,FragmentManagerImpl.TARGET_STATE_TAG, f.mTarget);if (f.mTargetRequestCode != 0) {fs.mSavedFragmentState.putInt(FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG,f.mTargetRequestCode);}}} else {fs.mSavedFragmentState = f.mSavedFragmentState;}}}int[] added = null;BackStackState[] backStack = null;// Build list of currently added fragments.//因为mActive 是mAdded的超级,所以这里保存mAdded,就只保存了索引值N = mAdded.size();if (N > 0) {added = new int[N];for (int i = 0; i < N; i++) {added[i] = mAdded.get(i).mIndex;if (added[i] < 0) {throwException(new IllegalStateException("Failure saving state: active " + mAdded.get(i)+ " has cleared index: " + added[i]));}if (DEBUG) {Log.v(TAG, "saveAllState: adding fragment #" + i+ ": " + mAdded.get(i));}}}// Now save back stack.//把mBackStack信息保存在BackStackState中if (mBackStack != null) {N = mBackStack.size();if (N > 0) {backStack = new BackStackState[N];for (int i=0; i<N; i++) {backStack[i] = new BackStackState(mBackStack.get(i));if (DEBUG) Log.v(TAG, "saveAllState: adding back stack #" + i+ ": " + mBackStack.get(i));}}}//保存active、added、backStack的序列化类FragmentManagerState fms = new FragmentManagerState();fms.mActive = active;fms.mAdded = added;fms.mBackStack = backStack;if (mPrimaryNav != null) {fms.mPrimaryNavActiveIndex = mPrimaryNav.mIndex;}fms.mNextFragmentIndex = mNextFragmentIndex;saveNonConfig();return fms;}
保存数据搞明白了,恢复数据就很简单,它的逆操作。这里不再详细展开。
那么问题来了,为什么保存的时候,mAdded中只有两个本次添加的fragment呢,系统恢复的fragment为什么没在mAdded中呢?
此时我有两种思路:
1、到上上次保存数据看看。可能上上次保存数据时候,mAdded中没有S1,L1,导致上次恢复数据的时候,没有S1,L1。这种可能应该不大,毕竟是系统的源码,不可能出现这个错误吧。
2、在上次恢复数据后(对应重新步骤3),有恢复S1,L1到mAdded中,但是由于某种操作,给删除了。
经过调试发现,是第二种问题,那么下面来详细描述一下,是什么导致mAdded中的S1,L1的被删除
在上次恢复数据后(对应重新步骤3),S1,L1的mHost 也是被赋值过的(与上面分析的S2,L2是一样的过程),只是后来mAdded中的S1,L1被删除了
经过不断调试发现,事务中执行了S1,L1的remove()操作,原来是S2使用replace() ,系统优化Ops 导致 S1,L1被remove,下面通过源码分析一下,这个remove行为是如何产生的
打开APP时,业务代码会先使用replace()来启动S(这里指S2),fragment在处理这个事务的时候,在函数expandPos()对操作进行优化,包括replace
下图可以看到replace S2,命令 最终边转变为,remove L1 ,S1,add S2,所以
这里顺便插个题外话,调试代码中op是mOps中的第 0 索引的值,但是在代码运行过程中,mOps 第0索引的值,发生变化,op也发生了变化。于是出现下图中,debugger中的局部变量op和代码中对应的op的变量内容不一样
下面是上图的代码,下面进行详细分析
// 对mOps操作 进行预处理,也算是一种对op操作优化的操作Fragment expandOps(ArrayList<Fragment> added, Fragment oldPrimaryNav) {for (int opNum = 0; opNum < mOps.size(); opNum++) {// 遍历mOps 中所有的操作。例如:add,show,hide,replace 等这些事务中的操作,都会保存在mOps final Op op = mOps.get(opNum);switch (op.cmd) {case OP_ADD:case OP_ATTACH:added.add(op.fragment);break;case OP_REMOVE:case OP_DETACH: {added.remove(op.fragment);if (op.fragment == oldPrimaryNav) {mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, op.fragment));opNum++;oldPrimaryNav = null;}}break;case OP_REPLACE: {//获取该操作对应的fragment,例如:add(f1),这里的op.fragment 就是f1final Fragment f = op.fragment;//获取f的容器idfinal int containerId = f.mContainerId;//是否已经加入到added,这个list中保存所有存活的fragment(没有remove和detached ),mActive包含mAddedboolean alreadyAdded = false;//遍历added中的fragmentfor (int i = added.size() - 1; i >= 0; i--) {final Fragment old = added.get(i);//如果是针对同一容器进行replaceif (old.mContainerId == containerId) {if (old == f) {//例如:replace(f1),f1已经在madded中,也就是说f1之前被添加过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;}//删除相同容器id的fragmentfinal 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 list中删除这个具有相同容器id的fragmentadded.remove(old);//操作的数量++opNum++;}}}if (alreadyAdded) {//如果已经添加过,删除replace操作//这里删除的是replace的操作,因为mOps.add 都是添加在opNum位置,把replace往后挤了一位,//所以opNum++ 后,opNum 就是replace的索引位置mOps.remove(opNum);opNum--;} else {//如果没有添加过,把replace改为addop.cmd = OP_ADD;//添加到added中added.add(f);}}break;case OP_SET_PRIMARY_NAV: {// It's ok if this is null, that means we will restore to no active// primary navigation fragment on a pop.mOps.add(opNum, new Op(OP_UNSET_PRIMARY_NAV, oldPrimaryNav));opNum++;// Will be set by the OP_SET_PRIMARY_NAV we inserted before when runoldPrimaryNav = op.fragment;}break;}}return oldPrimaryNav;}
这里可以看到,最后removeFragment函数中,调用了mAdder.remove() 把L1 和S1删除了,(下图只展示了L1被删除的断点)
所以在此时保存数据时,是没有L1和S1,也就是说在恢复数据时,mAdder中也没有L1和S1,也就不会给mHost赋值,导致上面这个错误。
解决方法:
第一种:
初次启动App,加载fragment使用add,而不是用replace,避免退出 已经恢复的其他fragment
第二种:
启动App时,判断是否已经有目标fragment的实例,如果已经有该实例,则不创建新的