Unable to start activity ComponentInfo{Activity}:java.lang.ArrayIndexOutOfBoundsException: length=1; index=1 完全分析与解决方案
Fragment中遇到了上述异常,在网上多方查找也没有找到关于抛出这个异常的具体原因与解决办法,于是决定自己去分析Fragment相关的API源码,功夫不负有心人,终于找到这个异常抛出的原因,以及几个可行的解决办法。
注1:本文中分析使用的源码版本是:
com.android.support:support-v4:24.0.0
注2:因为编写时显示器的宽度问题,所以文章中指出的代码的行号可能与最终发布时显示的行号不一致。
本文意图解决的问题
- 从源码角度分析Exception产生的原因
- 可行的解决方案
异常产生的必要条件
在分析原因之前,先简述异常产生的必要条件:
- 提交Fragment时使用的是
ft.commitAllowingStateLoss()
方法而不是ft.commit()
方法。 - Fragment必须要设置
Fragment.setRetainInstance(true)
,即不随配置的变化而销毁实例对象。 - Activity必须有状态的保存和状态的恢复(如从后台切换到前台,横竖屏幕切换等等)。
说明:对于上面第一点:从源码的实现来看,其保存状态的标志位mStateSaved
的修改没有做多线程同步考虑,所以如果Fragment状态的提交使用ft.commit()
方法并且是在非UI线程调用,也是有可能抛出ArrayIndexOutOfBoundsException
异常的,但是因为在实际中在使用中一般在在UI线程提交Fragment的状态的改变,并且该异常发生的条件很苛刻,所以将使用ft.commitAllowingStateLoss()
方法提交Fragment状态作为必要条件。
异常产生原因的源码分析
异常堆栈
以下是异常产生的完整堆栈:
07-18 22:16:38.993 8153-8153/com.example.androidfirsttest E/AndroidRuntime:
FATAL EXCEPTION: main
Process: com.example.androidfirsttest, PID: 8153
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.androidfirsttest/com.fragment.state.test.FragmentSaveRestoreActivity}: java.lang.ArrayIndexOutOfBoundsException: length=1; index=1
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2198)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2247)
at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3746)
at android.app.ActivityThread.access$900(ActivityThread.java:139)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1216)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5028)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:788)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:604)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.ArrayIndexOutOfBoundsException: length=1; index=1
at android.support.v4.app.FragmentManagerImpl.restoreAllState(FragmentManager.java:2018)
at android.support.v4.app.FragmentController.restoreAllState(FragmentController.java:158)
at android.support.v4.app.FragmentActivity.onCreate(FragmentActivity.java:324)
at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:85)
at com.fragment.state.test.FragmentSaveRestoreActivity.onCreate(FragmentSaveRestoreActivity.java:30)
at android.app.Activity.performCreate(Activity.java:5238)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1087)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2162)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2247)
at android.app.ActivityThread.handleRelaunchActivity(ActivityThread.java:3746)
at android.app.ActivityThread.access$900(ActivityThread.java:139)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1216)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5028)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:788)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:604)
at dalvik.system.NativeStart.main(Native Method)
通过异常堆栈可以发现,该异常是在Activity进行状态恢复的时候发生的,是在系统调用Activity的onCreate方法初始化Activity过程中恢复原有状态,这里的状态只关注恢复原有的Fragment, 即调用FragmentManagerImpl.restoreAllState
方法,恢复Fragment时导致的的数组下标越界访问异常,具体对应到源代码中2018行的代码:
//该行代码在FragmentMananger.java中FragmentManagerImpl.restoreAllState方法中
FragmentState fs = fms.mActive[f.mIndex];
至于为什么执行到这一行会抛出异常,到后面会有详细的分析,下面先从另一个更常见的异常开始分析。
既然有状态的恢复,那么必然是有状态的保存,如果是全新启动的一个Activity,自然不存在所谓的状态恢复了,这也证明了上面所写的必要条件中的第三点:
Activity必须有状态的保存和状态的恢复(如从后台切换到前台,横竖屏幕切换等等)的行为。
Fragment的状态改变的提交、状态保存、状态恢复
要想弄清楚异常产生的真正原因,那么必须要弄清楚以下几个问题:
- Fragment状态的改变的提交是如何实现的。
- 当需要保存Fragment的状态的时候,是如何保存的。
- 当需要恢复Fragment的状态的时候,是如何恢复的。
下面将依次从这几个方面进行分析。
Fragment的状态的改变是如何提交生效的
通过执行一系列如显示、隐藏、替代、添加等等的操作后改变Fragment的状态,其操作对应的方法如show、hide、replace、add等等。
一个典型的Fragment状态改变的提交,类似数据库中启用事务提交数据,包括以下过程:
- 开启提交的事务
- 确定要操作的Fragment(这个地方不做赘述)
- 实际要提交的操作(一次可以添加多个操作),如show, replace, add等
- 进行提交操作(commit/commitAllowingStateLoss)
注1:为了区分,把一个或多个操作的提交过程称为动作(action)。
注2:这里只分析Fragment的提交过程,不讨论状态修改提交后如何导致界面发生变化的过程。
下面的代码片是提交Fragment状态改变的常用代码:
private void showFragment(Fragment fragment, int containerId, String tag)
{
//mFinished 只有在调用finish()/finishAffinity()才有可能会被置为true,
//所以如横竖屏切换时isFinishing返回false
if(isFinishing() || isDestroyed()){
FragmentTransaction ft = fm.beginTransaction();
Fragment tempFragment = null;
if (tagName != null) {
tempFragment = fm.findFragmentByTag(tagName);
}
if (tempFragment != null) {
ft.show(tempFragment);
} else {
ft.replace(containerId, fragment, tagName);
//ft.add(containerId, fragment, tag);
}
ft.commit();
}
}
如果常常使用这样的类似代码,那么对下面这个异常一定不不会陌生了。
java.lang.RuntimeException: Unable to stop activity {com.example.androidfirsttest/com.fragment.state.test.FragmentSaveRestoreActivity}: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
...
正是为解决这个IllegalStateException
异常,我们常常将上面代码块中的最后一行改写为:
ft.commitAllowingStateLoss();
改成使用这个方法提交确实可以避免IllegalStateException
异常抛出,但是前面已经提到过过commitAllowingStateLoss()
方法是导致ArrayIndexOutOfBoundsException
异常的必要条件之一,下面就来看提交过程的具体源码分析和两个commit方法的不同之处。
分析之前先对几个核心类做下说明:
//FragmentManager类的具体实现类,管理Fragment的核心类,每个FragmentActivity都有一个唯FragmentManagerImpl实例
FragmentManagerImpl.java
//FragmentActivity中:
//FragmentController持有HostCallbacks的对象,而HostCallbacks持有FragmentManagerImpl的对象
//类似于代理的方式,使用FragmentController对Fragment进行操作,实际就是调用FragmentManagerImpl的相应方法
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
//HostCallbacks实现FragmentHostCallback抽象类,该类中:
final FragmentManagerImpl mFragmentManager = new FragmentManagerImpl();
//FragmentTransaction抽象类的具体实现类,使用事务的思想通过持有FragmentManagerImpl的实例对Fragment进行操作
BackStackRecord.java
开启Fragment提交的事务
@Override
public FragmentTransaction beginTransaction() {
return new BackStackRecord(this);
}
public BackStackRecord(FragmentManagerImpl manager) {
mManager = manager;
}
//注意其实现了Runnable接口
final class BackStackRecord extends FragmentTransaction implements
FragmentManager.BackStackEntry, Runnable
这个步骤很简单,就是实例化一个BackStackRecord
对象,该对象持有FragmentManagerImpl
实例,后面正是通过持有的这个实例对Fragment进行相应的操作。
Fragment要执行的操作
这里具体举例ft.show()
方法的源码,其他的与其类似。
public FragmentTransaction show(Fragment fragment) {
Op op = new Op();
op.cmd = OP_SHOW;
op.fragment = fragment;
addOp(op);
return this;
}
这里创建一个Op
对象,并对其cmd,fragment字段进行相应的赋值,那么Op
是一个什么样的对象呢?
static final class Op {
Op next;
Op prev;
int cmd;
Fragment fragment;
int enterAnim;
int exitAnim;
int popEnterAnim;
int popExitAnim;
ArrayList<Fragment> removed;
}
看到其类的定义,参考链表得数据结构模型,Op
就类似于链表中一个普通节点对象,串联上下两个节点, 同时分析addOp
方法就可以可以发现,这就是一个往链表的末尾插入一个节点的操作:
void addOp(Op op) {
if (mHead == null) {
mHead = mTail = op;
} else {
op.prev = mTail;
mTail.next = op;
mTail = op;
}
op.enterAnim = mEnterAnim;
op.exitAnim = mExitAnim;
op.popEnterAnim = mPopEnterAnim;
op.popExitAnim = mPopExitAnim;
mNumOp++;
}
这里也说明在执行commit之前可以添加多个操作,会依次添加到链表的末尾。
以上就一个Fragment的状态修改的动作,好了,现在我们action(动作)已经有了,现在要做的就是把动作提交生效了。
Fragment状态修改的提交
因为commit()
和commitAllowingStateLoss()
方法内部都是直接调用BackStackRecord.commitInternal(boolean)
方法,只是传入的参数不同:
public int commit() {
return commitInternal(false);
}
public int commitAllowingStateLoss() {
return commitInternal(true);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
//...省略一些无关的调试代码
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);
} else {
mIndex = -1;
}
mManager.enqueueAction(this, allowStateLoss);
return mIndex;
}
commitInternal(boolean)
方法传入的参数是true,表示允许状态丢失,如果使用commit()方法则传入的是false,至于这个标志位什么时候发挥作用后面马上就会看到。- 代码第18行,调用
FragmentManagerImpl.enqueueAction()
方法将这条新增动作添加到其待处理的动作队列中去。
看FragmentManagerImpl.enqueueAction()
方法:
public void enqueueAction(Runnable 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<Runnable>();
}
mPendingActions.add(action);
if (mPendingActions.size() == 1) { //此处长度等于1才提交是因为如果不为1时,动作队列中的任务正在执行
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
}
}
}
private void checkStateLoss() {
if (mStateSaved) {
throw new IllegalStateException(
"Can not perform this action after onSaveInstanceState");
}
if (mNoTransactionsBecause != null) {
throw new IllegalStateException(
"Can not perform this action inside of " + mNoTransactionsBecause);
}
}
- 代码块的2~4行,就是根据是否可以在Fragment状态保存后,是否允许提交新的Fragment的状态修改,如果为false(不允许修改),则就会进行判断Fragment的状态是否已经保存过了,看代码的21~24行,如果
mStateSaved
这个字段为true,就会抛出我们熟悉的IllegalStateException
,那么mStateSaved
这个字段什么时候会赋值为true呢?后面分析状态保存时会看到。 - 代码第9~12行,
mPendingActions
待处理的动作(实际就是Fragment的状态修改),代码中先将新动作添加到list中。 - 代码第13~16中,我们看到了使用
Handler.removeCallbacks
先移除原有的mExecCommit
,再使用Handler.post()
方法发送一条执行mExecCommit
这个Runnable的消息,从Handler的实现机制我们知道,这个提交事务的执行是个异步的过程。
注意上面的提交过程的所有代码,并没有要求在UI线程执行,所以在非UI线程也可以进行commit。
现在可以得出2个小结论:
- 当在系统已经保存了Fragment的状态后再使用
commit()
方法提交Fragment动作会抛出IllegalStateException
异常。- 使用
commit()
或者commitAllowStateLoss()
提交Fragment这是一个异步的过程,提交的动作并不会马上生效。
上面说到了提交是一个异步的过程,但是sdk也给我们提供了同步提交的方式:
- 在调用
commit()
或者commitAllowStateLoss()
后立即调用FragmentManagerImpl.executePendingTransactions()
方法,使用该方法会依次执行等待队列中所有未执行的动作。 - 使用
commitNow()
或者commitNowAllowingStateLoss()
方法替代以前的提交方法,这两个方法只要没有正在执行action,就会马上提交这次的action,api说明中也给出了这两个方法提交的效率更高。
但是使用上述两种提交方法,实际上是会增加ArrayIndexOutOfBoundsException
异常抛出的概率的!原因后面会讲到。
上面我们已经知道将动作添加到待完成action的队列后,会发送消息消息使用mExecCommit
这个Runnable异步处理,下面来看这个Runnable做了什么:
Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
/**
* Only call from main thread!
*/
public boolean execPendingActions() {
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");
}
boolean didSomething = false;
while (true) {
int numActions;
synchronized (this) {
if (mPendingActions == null || mPendingActions.size() == 0) {
break;
}
numActions = mPendingActions.size();
if (mTmpActions == null || mTmpActions.length < numActions) {
mTmpActions = new Runnable[numActions];
}
mPendingActions.toArray(mTmpActions);
mPendingActions.clear();
mHost.getHandler().removeCallbacks(mExecCommit);
}
mExecutingActions = true;
for (int i=0; i<numActions; i++) {
mTmpActions[i].run();
mTmpActions[i] = null;
}
mExecutingActions = false;
didSomething = true;
}
doPendingDeferredStart();
return didSomething;
}
这个Runnable内部很简单,只是调用了execPendingActions()
方法,其内部逻辑也很简单,复制mPendingActions
队列中的待处理的action,然后循环调用每个action的run方法,这里的action就是我们前面传入的BackStackRecord
对象,现在来看该对象的run方法:
//这个方法比较长,省略其中一些不相关的部分,这里只看执行添加动作
public void run() {
......
Op op = mHead;
while (op != null) {
int enterAnim = state != null ? 0 : op.enterAnim;
int exitAnim = state != null ? 0 : op.exitAnim;
switch (op.cmd) {
case OP_ADD: {
Fragment f = op.fragment;
f.mNextAnim = enterAnim;
mManager.addFragment(f, false);
} break;
......
}
op = op.next;
}
mManager.moveToState(mManager.mCurState, transition, transitionStyle, true);
if (mAddToBackStack) {
mManager.addBackStackState(this);
}
}
看上面代码中,从提交的动作的操作队列的头部开始,依次取出操作,进行相应处理,这里我们看一个比较简单的操作,add操作,其执行了FragmentManagerImpl.addFragment(Fragment, boolean)
方法:
public void addFragment(Fragment fragment, boolean moveToStateNow) {
if (mAdded == null) {
mAdded = new ArrayList<Fragment>();
}
if (DEBUG) Log.v(TAG, "add: " + fragment);
makeActive(fragment);
if (!fragment.mDetached) {
if (mAdded.contains(fragment)) {
throw new IllegalStateException("Fragment already added: " + fragment);
}
mAdded.add(fragment);
fragment.mAdded = true;
fragment.mRemoving = false;
if (fragment.mHasMenu && fragment.mMenuVisible) {
mNeedMenuInvalidate = true;
}
if (moveToStateNow) {
//该方法会将Fragment的状态的改变进行相应处理,最终可能让用户界面发生变化,不属于此文讨论范围
moveToState(fragment);
}
}
}
添加Fragment整体来说比较简单,如果fragment以前没有添加过,则将Fragment添加到mAdded
列表中 ,并调用moveToState(fragment)
对其状态进行相应处理。这里我们需要重点关注第6行调用的makeActive(fragment)
,它关系到我们后面的状态保存:
void makeActive(Fragment f) {
if (f.mIndex >= 0) {
return;
}
if (mAvailIndices == null || mAvailIndices.size() <= 0) {
if (mActive == null) {
mActive = new ArrayList<Fragment>();
}
f.setIndex(mActive.size(), mParent);
mActive.add(f);
} else {
f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
mActive.set(f.mIndex, f);
}
if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
}
//上面提到的mActive和mAdd等等都是一个ArrayList
ArrayList<Fragment> mActive;
ArrayList<Fragment> mAdded;
ArrayList<Integer> mAvailIndices;
ArrayList<BackStackRecord> mBackStack;
ArrayList<Fragment> mCreatedMenus;
这个方法主要做两件事,给新增加的Fragment分配一个下标(>=0,新的Fragment其默认下标是<0的),再将新增加的Fragment保存到mActive
列表中,这个列表维护每个Activity中活动的Fragment,它也是后面我们进行状态保存和恢复的关键。
这里我们有可以得出一个小结论:
commit()
和commitAllowingStateLoss()
方法的区别在于如果系统已经保存了Fragment的状态,前者就会抛出异常,后者还是会将Fragment添加到mActive
列表中。
现在,Fragment的完成提交过程分析完了,做个小结:
- 初始化一个新的action对象。
- 查找或新创建一个要执行新操作的Fragment。
- 添加一个或多个操作到action的操作链表中。
- 将包含了一个或多个关于Fragment操作的action添加到
FragmentManagerImpl
的等待处理action的队列中。 - 通过handler的异步机制从队列中有序的取出每个action进行处理,调用
action.run()
方法有序的从操作链表中取出每个操作,调用FragmentManagerImpl
的相应方法进行处理。 - 若Fragment是新添加的,则为其分配一个>=0的下标,并将其保存到
mActive
队列中。 - 进行相应的状态改变处理。
Fragment的状态保存
我们知道当Activity居于后台、低内存、横竖屏切换时,原有的Activity对象需要被回收, 此时会触发Activity对其状态进行保存,以便恢复的时候使用,这里的状态当然也包括Fragment的状态进行保存了,我们都知道,activity要保存状态时,会调用其
Activity.onSaveInstanceState(Bundle outState)
方法,但是事实上除了该方法之外,还会调用Activity.onRetainNonConfigurationInstance()
方法,而且前者一定是在Activity.onStop()
之前调用,并且一定比后者早,后者常常在Activity.onStop()
之后调用,正是因为执行了两次保存,才有可能发生ArrayIndexOutOfBoundsException
异常,那么两次保存有什么不同的地方呢?具体我们来看两个方法的源码。
注:这里只关注Fragment的状态的保存。
onSaveInstanceState(Bundle)
方法保存状态
/**
* Save all appropriate fragment state.
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Parcelable p = mFragments.saveAllState();
if (p != null) {
outState.putParcelable(FRAGMENTS_TAG, p);
}
if (mPendingFragmentActivityResults.size() > 0) {
outState.putInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG, mNextCandidateRequestIndex);
int[] requestCodes = new int[mPendingFragmentActivityResults.size()];
String[] fragmentWhos = new String[mPendingFragmentActivityResults.size()];
for (int i = 0; i < mPendingFragmentActivityResults.size(); i++) {
requestCodes[i] = mPendingFragmentActivityResults.keyAt(i);
fragmentWhos[i] = mPendingFragmentActivityResults.valueAt(i);
}
outState.putIntArray(ALLOCATED_REQUEST_INDICIES_TAG, requestCodes);
outState.putStringArray(REQUEST_FRAGMENT_WHO_TAG, fragmentWhos);
}
}
- 代码第7-10行,第7行获取所有的Fragment的状态并返回成一个可序列化的对象,接着在第9行保存起来,前面已经说过,调用
FragmentController
的操作Fragment的方法,实际就是调用FragmentManagerImpl
相对应的方法。
/**
* Saves the state for all Fragments.
*/
public Parcelable saveAllState() {
return mHost.mFragmentManager.saveAllState();
}
//方法比较长,删除了一些注释,和一些异常抛出的代码,不影响正常的流程
Parcelable saveAllState() {
execPendingActions();
if (HONEYCOMB) {
mStateSaved = true;
}
if (mActive == null || mActive.size() <= 0) {
return null;
}
// First collect all active fragments.
int N = mActive.size();
FragmentState[] active = new FragmentState[N];
boolean haveFragments = false;
for (int i=0; i<N; i++) {
Fragment f = mActive.get(i);
if (f != null) {
if (f.mIndex < 0) {
......
}
haveFragments = true;
FragmentState fs = new FragmentState(f);
active[i] = fs;
if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {
fs.mSavedFragmentState = saveFragmentBasicState(f);
if (f.mTarget != null) {
if (f.mTarget.mIndex < 0) {
......
}
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;
}
......
}
}
if (!haveFragments) {
if (DEBUG) Log.v(TAG, "saveAllState: no fragments!");
return null;
}
int[] added = null;
BackStackState[] backStack = null;
// Build list of currently added fragments.
if (mAdded != null) {
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) {
......
}
......
}
}
}
// Now save back stack.
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));
......
}
}
}
FragmentManagerState fms = new FragmentManagerState();
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
return fms;
}
方法看起来很长,但是逻辑其实比较简单:
- 首先将等待执行action的队列中的action全部执行完
- 如果API LEVEL >= 11,则将
mStateSaved
标志位置为true,表示已经保存状态了,如果是11以下呢,在调用onStop方法的时候会置为true。 - 再依次获取mActive中的Fragment的状态,mAdded中的Fragment的下标,存放在
mBackStack
中的动作(这些动作是是可以响应back键的)。 - 最后创建一个可序列化的
FragmentManagerState
对象,分别获取上面要保存的几个list,并返回给调用者。
小结:
调用
onSaveInstanceState
方法保存Fragment的状态实际就是做两件事,将Fragment 状态以保存的标志位设为true,并将活动中的(mActive列表)的Fragment的状态使用序列化保存起来。
onRetainNonConfigurationInstance()
方法保存Fragment的状态
看方法的名称我们就可以猜测,该方法用于保存一些在Activity因配置发生改变(如横竖屏切换),Activity销毁重建时,不销毁的实例对象,通过阅读源码和API说明也证实了这一点,同时API告诉我们调用Fragment.setRetainInstance(boolean)
方法来实现一个Fragment是否在Activity在重新创建时也进行销毁重新实例化。
注:在FragmentActivity中
重写了父类的onRetainNonConfigurationInstance()
方法,并且定义成了final,如果子类要想实现一些自己的对象持久化保存,则需要重写FragmentActivity.onRetainCustomNonConfigurationInstance()
方法。
下面看onRetainNonConfigurationInstance()
方法的源码:
/**
* Retain all appropriate fragment and loader state. You can NOT
* override this yourself! Use {@link #onRetainCustomNonConfigurationInstance()}
* if you want to retain your own state.
*/
/**
* 说明一下该方法的调用逻辑:
* 该方法被父类的retainNonConfigurationInstances()方法调用
* 而父类的方法最终是在ActivityThread.handleRelaunchActivity(ActivityClientRecord)中被调用
* /
@Override
public final Object onRetainNonConfigurationInstance() {
if (mStopped) {
doReallyStop(true);
}
Object custom = onRetainCustomNonConfigurationInstance();
FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
SimpleArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();
if (fragments == null && loaders == null && custom == null) {
return null;
}
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.fragments = fragments;
nci.loaders = loaders;
return nci;
}
这里我们只需要关注代码的第19行,等同于调用了FragmentManagerImpl.retainNonConfig()
方法:
/**
* Returns a nested tree of Fragments that have opted to retain their instance across
* configuration changes.
*/
public FragmentManagerNonConfig retainNestedNonConfig() {
return mHost.mFragmentManager.retainNonConfig();
}
FragmentManagerNonConfig retainNonConfig() {
ArrayList<Fragment> fragments = null;
ArrayList<FragmentManagerNonConfig> childFragments = null;
if (mActive != null) {
for (int i=0; i<mActive.size(); i++) {
Fragment f = mActive.get(i);
if (f != null) {
if (f.mRetainInstance) {
if (fragments == null) {
fragments = new ArrayList<Fragment>();
}
fragments.add(f);
f.mRetaining = true;
f.mTargetIndex = f.mTarget != null ? f.mTarget.mIndex : -1;
if (DEBUG) Log.v(TAG, "retainNonConfig: keeping retained " + f);
}
boolean addedChild = false;
if (f.mChildFragmentManager != null) {
FragmentManagerNonConfig child = f.mChildFragmentManager.retainNonConfig();
if (child != null) {
if (childFragments == null) {
childFragments = new ArrayList<FragmentManagerNonConfig>();
for (int j = 0; j < i; j++) {
childFragments.add(null);
}
}
childFragments.add(child);
addedChild = true;
}
}
if (childFragments != null && !addedChild) {
childFragments.add(null);
}
}
}
}
if (fragments == null && childFragments == null) {
return null;
}
return new FragmentManagerNonConfig(fragments, childFragments);
}
这个方法的逻辑也很简单,取出mActive列表中所有设置了Activity重新创建不销毁的Fragment,包括Fragment中的子Fragment,将其封装到FragmentManagerNonConfig
对象中返回。
这里我们只要关注第12-24行就可以了,在第13在对mActive中的Fragment进行遍历,在第16行当Fragment.mRetainInstance = true
时,就将其添加到要保存的列表中,其实这个字段正是我们前面提到的使用Fragment.setRetainInstance(boolean)
方法设置的字段,其默认值是为false的:
public void setRetainInstance(boolean retain) {
mRetainInstance = retain;
}
小结:
调用
onRetainNonConfigurationInstance()
方法保存的是mActive列表中Fragment不随Activity因重建而销毁的Fragment的状态。
到这里就有一个疑问了,onRetainNonConfigurationInstance()
方法的调用时机在onStop()
方法之后,此时如果在onSaveInstanceState(Bundle)
方法之后使用commitAllowingStateLoss()
方法提交一个Fragment,并且设置不随Activity的重建而销毁,并且在onRetainNonConfigurationInstance()
方法调用前已经添加到mActive队列中,那么在Activity重建后可能会发生什么?
Fragment的状态恢复
既然有Fragment的状态的保存,自然也有恢复的过程,当Activity因为某些情况重新创建时(reCreate)时,会进行Fragment的状态恢复,所以我们先来看FragmentActivity.onCreate()
方法:
/**
* Perform initialization of all fragments and loaders.
*/
@SuppressWarnings("deprecation")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mFragments.restoreLoaderNonConfig(nc.loaders);
}
if (savedInstanceState != null) {
Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
// Check if there are any pending onActivityResult calls to descendent Fragments.
if (savedInstanceState.containsKey(NEXT_CANDIDATE_REQUEST_INDEX_TAG)) {
mNextCandidateRequestIndex =
savedInstanceState.getInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG);
int[] requestCodes = savedInstanceState.getIntArray(ALLOCATED_REQUEST_INDICIES_TAG);
String[] fragmentWhos = savedInstanceState.getStringArray(REQUEST_FRAGMENT_WHO_TAG);
if (requestCodes == null || fragmentWhos == null ||
requestCodes.length != fragmentWhos.length) {
Log.w(TAG, "Invalid requestCode mapping in savedInstanceState.");
} else {
mPendingFragmentActivityResults = new SparseArrayCompat<>(requestCodes.length);
for (int i = 0; i < requestCodes.length; i++) {
mPendingFragmentActivityResults.put(requestCodes[i], fragmentWhos[i]);
}
}
}
}
if (mPendingFragmentActivityResults == null) {
mPendingFragmentActivityResults = new SparseArrayCompat<>();
mNextCandidateRequestIndex = 0;
}
mFragments.dispatchCreate();
}
/**
* @return Returns the object previously returned by
* {@link #onRetainNonConfigurationInstance()}.
*/
public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}
static final class NonConfigurationInstances {
Object custom;
FragmentManagerNonConfig fragments;
SimpleArrayMap<String, LoaderManager> loaders;
}
- 代码的第12-13调用
getLastNonConfigurationInstance()
获取保存的未销毁的实例的封装NonConfigurationInstances
对象,从API说明和源码都可以知道这个对象就是上面调用onRetainNonConfigurationInstance()
方法时返回的对象。 - 代码第17行,实际就是读取使用
onSaveInstanceState(Bundle)
方法保存的Fragment的状态。 - 代码第18行,等同于调用
FragmentManagerImpl.restoreAllState()
方法,注意看其传入的两个参数分别为第一次保存Fragment状态时的数据,第二次保存的Fragment的集合。
从异常的堆栈,我们知道crash的抛出就是在FragmentManagerImpl.restoreAllState()
方法中,现在我们来看看它的源码吧。
//源码方法很长,这里放上出错部分的源码
void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
// If there is no saved state at all, then there can not be
// any nonConfig fragments either, so that is that.
if (state == null) return;
FragmentManagerState fms = (FragmentManagerState)state;
if (fms.mActive == null) return;
List<FragmentManagerNonConfig> childNonConfigs = null;
// First re-attach any non-config instances we are retaining back
// to their saved state, so we don't try to instantiate them again.
if (nonConfig != null) {
List<Fragment> nonConfigFragments = nonConfig.getFragments();
childNonConfigs = nonConfig.getChildNonConfigs();
final int count = nonConfigFragments != null ? nonConfigFragments.size() : 0;
for (int i = 0; i < count; i++) {
Fragment f = nonConfigFragments.get(i);
if (DEBUG) Log.v(TAG, "restoreAllState: re-attaching retained " + f);
FragmentState fs = fms.mActive[f.mIndex];
fs.mInstance = f;
f.mSavedViewState = null;
f.mBackStackNesting = 0;
f.mInLayout = false;
f.mAdded = false;
f.mTarget = null;
if (fs.mSavedFragmentState != null) {
fs.mSavedFragmentState.setClassLoader(mHost.getContext().getClassLoader());
f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray(
FragmentManagerImpl.VIEW_STATE_TAG);
f.mSavedFragmentState = fs.mSavedFragmentState;
}
}
}
......
}
看代码的中的第11-12行注释,首先进行状态恢复的Fragment是没有随Activiy重建而销毁的Fragment。
- 代码第14行,获取未销毁的Fragment的实例列表。
- 代码第17行开始循环取出未销毁Fragment实例列表中的Fragment。
- 代码对20行,通过未销毁Fragment的下标从
onSaveInstanceState(Bundle)
方法保存的mActive列表中获取Fragment的状态(FragmentState
),但是我们已经知道异常的抛出就是在这一行,显然是因为我们的未销毁的Fragment的下标大于mActive的大小,从而导致访问越界,那么怎么会发生这种情况呢?
记得在上一节的末尾提出的那个疑问,那个场景不是完美的符合现在的这种异常现象吗?现在让步骤具体点:
- 假设正常情况下我只有一个Fragment A,mActive中只有一个Fragment,且其下标为0,
- 进行横竖屏切换,在系统调用
onStop()
方法调用前必然会调用onSaveInstanceState(Bundle)
方法保存Fragment的状态,注意此时mActive的大小为1,此时Activity能够进行正常恢复。 - 现在我对
onStop()
方法进行重写,在其内部进行一个新的Fragment B使用commitAllowingStateLoss()
提交,设置Fragment.setRetainInstance(true)
,并添加立刻执行提交(使用fm.executePendingTransactions()
方法),根据源码我们已经知道B会存放到mActive中去,且其下标会分配为1,mActive的大小变为2,注意此时onSaveInstanceState(Bundle)
方法早已经调用过了,所以第一次保存的数据中mActive的大小还是为1。 - 因为
onRetainNonConfigurationInstance()
方法在onStop()
方法后执行,所以B会被保存起来。 - 当Activity进行恢复时,根据B的下标去mActive中找它对应保存的状态,是必然会抛出
ArrayIndexOutOfBoundsException
异常的。
口说无凭来看一段具体的重现这个异常的代码吧:
//Activity
package com.fragment.state.test;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import com.example.androidfirsttest.R;
/**
* Created by sky on 2016/7/18.
*/
public class FragmentSaveRestoreActivity extends AppCompatActivity {
public static final String TAG = "FragmentSaveRestore";
private int containerId1 = R.id.containerId1;
private int containerId2 = R.id.containerId2;
private FragmentManager fm = getSupportFragmentManager();
/**
* {@inheritDoc}
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.fragment_container_layout);
showFragment(new SaveRestoreTestFragment(), containerId1, "firstFragment");
}
/**
* {@inheritDoc}
*/
@Override
protected void onSaveInstanceState(Bundle outState) {
// TODO Auto-generated method stub
super.onSaveInstanceState(outState);
}
/**
* {@inheritDoc}
*/
@Override
public Object onRetainCustomNonConfigurationInstance() {
// TODO Auto-generated method stub
Log.d(TAG, "onRetainCustomNonConfigurationInstance called");
return super.onRetainCustomNonConfigurationInstance();
}
/**
* {@inheritDoc}
*/
@Override
protected void onPause() {
// TODO Auto-generated method stub
Log.d(TAG, "onPause() called");
if (isFinishing()) {
Log.d(TAG, "onPause() called, activity is finishing");
} else {
Log.d(TAG, "onPause() called, but activity is not finishing");
}
super.onPause();
}
/**
* {@inheritDoc}
*/
@Override
protected void onStop() {
// TODO Auto-generated method stub
Log.d(TAG, "onStop() called");
if (isFinishing()) {
Log.d(TAG, "onStop() called activity is finishing");
} else {
Log.d(TAG, "onStop() called but activity is not finishing");
}
showFragment(new SaveRestoreTestFragment(), containerId2, "SecondFragment");
fm.executePendingTransactions();
super.onStop();
}
@Override
public void finish() {
super.finish();
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
private void showFragment(Fragment fragment, int containerId, String tagName) {
//mFinished 只有在调用finish()/finishAffinity()才有可能会被置为true,
//所以如横竖屏切换时isFinishing返回false
if(!isFinishing() || !isDestroyed()){
FragmentTransaction ft = fm.beginTransaction();
Fragment tempFragment = null;
if (tagName != null) {
tempFragment = fm.findFragmentByTag(tagName);
}
if (tempFragment != null) {
ft.show(tempFragment);
} else {
ft.replace(containerId, fragment, tagName);
// ft.add(containerId, fragment, tag);
}
ft.commitAllowingStateLoss();
}
}
}
//fragment
package com.fragment.state.test;/*
* 文 件 名: SaveRestoreTestFragment.java
*
*/
import com.example.androidfirsttest.R;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
*
*/
public class SaveRestoreTestFragment extends Fragment
{
/** <默认构造函数>
*/
public SaveRestoreTestFragment()
{
// TODO Auto-generated constructor stub
setRetainInstance(true);
}
/** {@inheritDoc} */
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
// TODO Auto-generated method stub
// super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.first_fragment_layout, container, false);
return view;
}
/** {@inheritDoc} */
@Override
public void onAttach(Context context)
{
// TODO Auto-generated method stub
Log.d(FragmentSaveRestoreActivity.TAG, "the fragment attach to activity");
super.onAttach(context);
}
/** {@inheritDoc} */
@Override
public void onDetach()
{
// TODO Auto-generated method stub
Log.d(FragmentSaveRestoreActivity.TAG, "the fragment detach from the activity");
super.onDetach();
}
}
需要特别说明的是,如果没有代码中的第95行,这个异常很难复现,这也不难理解,因为如果不立即执行的话而异步执行的话,很有可能在将fragment添加到mActive中去之前,onRetainNonConfigurationInstance()
方法已经被调用了。
注:以前所在公司使用了ViewPager + Fragment的方案,在所有出现的这一类型的异常中,ViewPager占了70%以上,分析ViewPager的源码发现,它提交Fragment使用的就是一下两行代码:
ft.commitAllowingStateLoss();
fm.executePendingTransactions();
ArrayIndexOutOfBoundsException
异常抛出原因
现在终于找到抛出ArrayIndexOutOfBoundsException
异常的原因了:
简单来说,就是在Activity已经调用了
onSaveInstanceState(Bundle)
方法保存了一次Fragment的状态后,在调用onRetainNonConfigurationInstance()
方法第二次保存状态之间,进行了一次或多次Fragment的动作的提交。
解决方案
下面总结了三种解决方式:
不持久化保存Fragment的实例
即设置
Fragment.setRetainInstance(false);
这种方式简单实用,但是在有些运用场景中,我们需要持久化持有Fragment的对象,这个时候我们就要考虑使用第二种方案了。
重写Activity的onSaveInstanceState
方法,设置状态保存标志位
package com.fragment.state.test;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
/**
* Created by sky on 2016/7/31.
*/
public class BaseActivity extends AppCompatActivity {
/**
* 自定义一个标志位,标记Activity的状态是否已经保存
*/
private boolean stateSaved = false;
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
stateSaved = true;
}
@Override
protected void onResume() {
stateSaved = false;
super.onResume();
}
/**
* 获取activity的保存状态,是否已经保存了Activity的状态。
*/
public boolean isStateSaved() {
return stateSaved;
}
/**
* 提交Fragment,在提交前,判断Activity是否已经保存状态了,如果已经保存了状态就提交
* */
public void commitFragment(Fragment fragment, int containerId, String tagName){
//mFinished 只有在调用finish()/finishAffinity()才有可能会被置为true,
//所以如横竖屏切换时isFinishing返回false
if(!isStateSaved() && !isFinishing()){
if (Build.VERSION.SDK_INT >= 17 && isDestroyed()) {
return;
}
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
Fragment tempFragment = null;
if (tagName != null) {
tempFragment = fm.findFragmentByTag(tagName);
}
if (tempFragment != null) {
ft.show(tempFragment);
} else {
ft.replace(containerId, fragment, tagName); //或者其他的操作
}
ft.commitAllowingStateLoss();
}
}
}
这种方法,归纳起来就是两点:
- 重写
onSaveInstanceState
方法,将stateSaved
标志位置为ture
,其他情况置为false
。 - 在进行Fragment的相关操作前,先 判断Activity是否已经保存了状态,即调用
isStateSaved()
方法,如果已经保存了状态,则不再进行相关操作,可以参看commitFragment
方法。
对于ViewPager
,重写FragmentStatePagerAdapter
的finishUpdate
方法
之所以要重写finishUpdate
方法,是因为ViewPager中Fragment的状态变化是由该方法执行的,以下是源码:
@Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitNowAllowingStateLoss();
mCurTransaction = null;
}
}
修改后的代码片段:
package com.fragment.state.test;
import android.os.Build;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.view.ViewGroup;
import java.lang.ref.WeakReference;
/**
* Created by sky on 2016/7/31.
*/
public class CustomFragmentAdapter extends FragmentStatePagerAdapter {
/**
* 以弱引用的方式持有其添加到的Activity的实例
*/
private WeakReference<BaseActivity> activity;
public CustomFragmentAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return null;
}
@Override
public int getCount() {
return 0;
}
/**
* 持有其依附到的Activity的弱引用
*/
public void setActivity(BaseActivity activity) {
this.activity = new WeakReference<>(activity);
}
/**
* 重写finishUpdate方法,只有在其持有的Activity不为空,且不在finish,destory,状态已经保存的状态时,才调用其父类方法
*/
@Override
public void finishUpdate(ViewGroup container) {
BaseActivity tempActivity = activity.get();
if (tempActivity != null && !tempActivity.isStateSaved() && !tempActivity.isFinishing()) {
if (Build.VERSION.SDK_INT >= 17 && tempActivity.isDestroyed()) {
return;
}
super.finishUpdate(container);
}
}
}
以上就是关于Unable to start activity ComponentInfo{Activity}:java.lang.ArrayIndexOutOfBoundsException: length=1; index=1
异常的全部内容,如果有不正确之处,还请指正。