getSupportFragmentManager()引起java.lang.IllegalStateException异常:在onSaveInstanceState之后无法执行此操作

5
我正在努力弄清楚为什么:
getSupportFragmentManager().beginTransaction().commit();

失败,出现了

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

在一个非常基本的FragmentActivity类中。

这是我的使用案例(这将是一些伪代码,而不是完整的示例,抱歉): 我有一个带有内部AsyncTask类的FragmentActivity。大致上是这样的:

public class HelloWorld extends FragmentActivity {
    showFragment(Fragment fragment, String name) {
        getSupportFragmentManager().beginTransaction().replace(R.id.fragmentContainer, fragment, name).commit();
    }

    private class SlowFragmentShow extends AsyncTask<Context, String, Void> {
        protected Void doInBackground(Context... contexts) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                /* meh */
            }
        }

        protected void onPostExecute(Void nothing) {
            showFragment(new MyFragment(), "myFragment");
        }
    }
}

基本上,在启动应用程序10秒钟后,它会显示另一个片段。听起来很简单,对吧?这似乎也很好用,直到我决定旋转手机为止。当我这样做时,应用程序在调用“getSupportFragmentManager()...”时会崩溃,并出现“无法执行此操作...”。

2个回答

10

经过大量调试,发现当调用SlowFragmentShow.onPostExecute()时,它调用了showFragment(),后者又调用了getSupportFragmentManager(),我得到的FragmentManager处于非法状态(所以可以说我得到的异常是正确的)。 我仍然不确定为什么getSupportFragmentManager()会返回一个处于这种悬置状态的对象,但它确实这样做了,而我需要以某种方式访问“正确”的FragmentManager。 为了简化问题,我将FragmentManager作为静态变量存储在我的HelloWorld FragmentActivity中,并在HelloWorld.onStart()被调用时更新:

public class HelloWorld extends FragmentActivity {
    private static FragmentManager fragmentManager;

    public void onStart() {
        fragmentManager = getSupportFragmentManager();
        /* more code here */
    }

    showFragment(Fragment fragment, String name) {
        fragmentManager.beginTransaction().replace(R.id.fragmentContainer, fragment, name).commit();
    }

    private class SlowFragmentShow extends AsyncTask<Context, String, Void> {
        protected Void doInBackground(Context... contexts) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                /* meh */
            }
        }

        protected void onPostExecute(Void nothing) {
            showFragment(new MyFragment(), "myFragment");
        }
    }
}

好的,那就差不多解决了。现在我可以任意旋转手机,异步任务完成后仍然会显示片段。

回想起来,这似乎真的有些“噢,当然了!”但 Android 背后的设计决策感觉相当“外星”和不寻常。我似乎总是要在几乎一半的代码周围加上 try-catch(Exception),以防止出现非致命错误(例如无法更新文本字段)而导致崩溃,并且有很多静态变量需要在 onStart() 上更新,因为这似乎是您可以引用 Android 对象而它们不处于 IllegalState 状态的唯一明智方式。


如果您有更好的策略让AsyncTask访问(Support)FragmentManager而不需要我所采用的静态变量,请告诉我。 - Vidar Wahlberg
3
在创建你的碎片时调用 setRetainInstance(true) 方法。然后在 onPostExecute() 方法中调用 getSupportFragmentManager() 方法以获取 SupportFragmentManager 对象。然而,在英语中,“很多”意味着不止一个情况,这就是为什么我建议你提出一个新的 StackOverflow 问题,并提供示例,这样人们可以帮助你找到更好的解决策略。 - CommonsWare
我应该在哪里调用setRetainInstance(true)?FragmentActivity中没有这样的方法。 - Vidar Wahlberg
如果HelloWorld是一个Fragment而不是FragmentActivity,那可能会起作用。您可以将SlowFragmentShow视为异步任务,用于加载数据,而HelloWorld则显示一些内容,让用户知道应用正在初始化。我认为将此异步任务连接到活动而不是片段并没有意义,因为它是用于初始化应用程序/活动的任务,而不是片段。至于使用“很多”静态变量:https://dev59.com/Qeo6XIcBkEYKwwoYOR0q#14695397 - Vidar Wahlberg
我觉得将这个异步任务连接到一个Fragment是没有意义的 - 模型片段模式对此仍然有效。如果您愿意,可以使用静态的AsyncTask内部类和onRetainNonConfigurationInstance(),但这已经被弃用,以引导人们使用保留的片段:https://dev59.com/t2865IYBdhLWcg3wa980#3821998。关于您的其他答案,请勿将`View`放入静态数据成员中,因为这会引入内存泄漏。 - CommonsWare
显示剩余2条评论

0

@VidarWahlberg,你的回答很好,但是假设你有一个FragmentActivity,在其中运行任务,并打开新的FragmentActivity,在异步完成之前或者你的应用程序在后台运行(例如:你点击了主页)。我建议你创建一个BaseActivity,在其中扩展FragmentActivity,并且所有的活动都应该扩展它。

什么很重要?

BaseActivity中,你需要有static FragmentManager fm;,正如@VidarWahlberg在他的回答中提到的那样,并在子活动中使用它来显示DialogFragment

有什么区别?

不要在onStart()方法中初始化fm = getSupportFragmentManager();,而是需要在onResume()方法中进行初始化。这是必需的,因为当您启动线程运行活动时,它会创建其新实例,并且当启动下一个活动时,它会创建BaseActivity的新实例,其中将初始化FragmenManager,当您回到线程启动活动时,fm对象将与第二个活动(已暂停)实例一起。应用程序将崩溃,因为您将尝试在已暂停的活动视图中显示对话框。您无法更改已暂停活动的视图,因此需要在每次调用onResume()时进行初始化。onResume()也在活动启动时调用,因此在那里初始化FragmentManager是安全的!

我们还需要担心什么?

你可能认为一切都很好,但实际上并非如此。你可能一开始没有意识到,但如果你的应用程序不在前台(而是在后台),所有的活动都会被暂停,但线程已经启动并正在运行。这是一个问题,因为线程将继续运行,并尝试显示DialogFragmernt(或者说更改未处于焦点的活动视图)。因此,解决方案是跟踪应用程序是否在前台。可以使用PackageManager来完成这个任务,但这将需要在清单文件中添加新的权限(用户不喜欢这样做,他们可能不想安装你的应用程序)。解决方案是拥有一个static boolean isOnBackground = false;字段,在每个活动的onResume()中使用isOnBackground = false;进行更改,在onPause()中使用isOnBackground = true;进行更改。这不是一个大的变化,因为BaseActivity是我们可以这样做的地方。这是安全的,因为每次启动新活动都会创建BaseActivity类的新实例,但由于该字段是static,它只创建一次,并且对所有实例都相同。在启动线程后方法的主体代码之前,我们需要检查isOnBackground的状态。

你可能会说因为onPause()onResume()之间的延迟可能会出问题,但你的应用程序需要设计成快速启动活动并不要在UI线程上执行长时间任务(需要在后台线程上完成)。因此,如果你的应用程序设计得好,你就不会有问题 :)

当应用程序不在前台时,可以使用线程运行方法的递归调用。这是安全的,不会导致循环,因为当应用程序的状态被重置(需要内存)或应用程序被用户或Android本身停止时,循环将停止,因为所有应用程序实例将从内存中释放!

注意:使用android支持库!

以下是代码:

BaseActivity.java

.......................
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
.......................

public class BaseActivity extends FragmentActivity {

    public static FragmentManager fm;
    public static boolean isOnBackground;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    protected void onResume() {
        super.onResume();
        BaseActivity.isOnBackground = false;
        BaseActivity.fm = getSupportFragmentManager();
    }

    @Override
    protected void onPause() {
        super.onPause();
        BaseActivity.isOnBackground = true;
    }
}

MainActivity.java

.....................
import android.support.v4.app.DialogFragment;
.....................

public class MainActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //This is just to call some second activity
        Button startNewActivity = (Button) findViewById(R.id.helloWorld);

        startNewActivity.setOnClickListener( new OnClickListener() {

            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this,
                            SecondActivity.class);
                startActivity(intent);
            }
        });

        showEditDialog();
    }

    private void showEditDialog() {
        (new Handler()).postDelayed(new Runnable() {
            @Override
            public void run() {
                if(!BaseActivity.isOnBackground) {
                    DialogFragment myFragmentDialog = new DialogFragment();
                    myFragmentDialog.show(BaseActivity.fm,
                            "fragment_my_dialog");
                } else {
                    //this is optional
                    showEditDialog();
                }
            } 

        }, 15000);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接