如何使用保存实例状态保存活动状态?


2619

我一直在使用Android SDK平台,但尚不清楚如何保存应用程序的状态。因此,对“ Hello,Android”示例进行了一些小的重新设计:

package com.android.hello;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class HelloAndroid extends Activity {

  private TextView mTextView = null;

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mTextView = new TextView(this);

    if (savedInstanceState == null) {
       mTextView.setText("Welcome to HelloAndroid!");
    } else {
       mTextView.setText("Welcome back.");
    }

    setContentView(mTextView);
  }
}

我认为这对于最简单的情况就足够了,但是无论我如何离开该应用程序,它总是以第一条消息做出响应。

我敢肯定解决方案就像重写onPause或类似的东西一样简单,但是我已经在文档中花了30分钟左右的时间,并且没有发现任何明显的问题。


9
什么时候saveInstanceState == null,什么时候不为null?
Trojan.ZBOT 2013年

90
您正在明确破坏您的活动,就像您所说的那样,远离活动,例如按回去。实际上,使用此“ savedInstanceState”的场景是Android破坏您的活动进行娱乐时。例如:如果您在活动运行时更改了手机的语言(因此需要加载项目中的其他资源)。另一个非常常见的情况是将手机旋转到侧面,以便重新创建活动并将其显示在横向中。
villoren 2014年

16
要获取第二条消息,请在开发人员选项中启用“不要保留活动”。按下主屏幕按钮,即可返回最近的状态。
Yaroslav Mytkalyk,2014年


6
您可以执行以下操作:onSaveInstanceState(Bundle savedInstanceState)
VahidHoseini

Answers:


2568

您需要覆盖onSaveInstanceState(Bundle savedInstanceState)并编写要更改为Bundle参数的应用程序状态值,如下所示:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
  super.onSaveInstanceState(savedInstanceState);
  // Save UI state changes to the savedInstanceState.
  // This bundle will be passed to onCreate if the process is
  // killed and restarted.
  savedInstanceState.putBoolean("MyBoolean", true);
  savedInstanceState.putDouble("myDouble", 1.9);
  savedInstanceState.putInt("MyInt", 1);
  savedInstanceState.putString("MyString", "Welcome back to Android");
  // etc.
}

捆绑包本质上是一种存储NVP(“名称-值对”)映射的方法,它将传入onCreate()onRestoreInstanceState()从中提取活动值,如下所示:

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  // Restore UI state from the savedInstanceState.
  // This bundle has also been passed to onCreate.
  boolean myBoolean = savedInstanceState.getBoolean("MyBoolean");
  double myDouble = savedInstanceState.getDouble("myDouble");
  int myInt = savedInstanceState.getInt("MyInt");
  String myString = savedInstanceState.getString("MyString");
}

或从一个片段。

@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
    super.onViewStateRestored(savedInstanceState);
    // Restore UI state from the savedInstanceState.
    // This bundle has also been passed to onCreate.
    boolean myBoolean = savedInstanceState.getBoolean("MyBoolean");
    double myDouble = savedInstanceState.getDouble("myDouble");
    int myInt = savedInstanceState.getInt("MyInt");
    String myString = savedInstanceState.getString("MyString");
}

通常,您将使用此技术来存储应用程序的实例值(选择,未保存的文本等)。


24
这有可能在手机上起作用,但在模拟器上不起作用吗?我似乎无法获得非空的savedInstanceState。
亚当·杰克

491
注意:您需要先将super.onSaveInstanceState(savedInstanceState)调用,然后再将值添加到Bundle中,否则它们将在调用后消失(Droid X And​​roid 2.2)。
jkschneider 2011年

121
注意:官方文档指出,您应该将重要信息保存在onPause-Method中,因为onsaveinstance-method并不是android生命周期的一部分。developer.android.com/reference/android/app/Activity.html
schlingel 2012年

32
这个事实实际上使onSaveInstanceState几乎没有用,除了屏幕方向改变的情况。在几乎所有其他情况下,您永远都不能依靠它,而需要在其他地方手动保存UI状态。或者防止您的应用因覆盖“后退”按钮行为而被杀死。我不明白为什么他们一开始就这样实现它。完全不直观。除非使用这种非常特殊的方法,否则您不可能拥有系统捆绑包来将内容保存到其中。
chakrit

12
请注意,对于已分配ID的,将自动处理将UI状态保存到Bundle中/从Bundle中恢复UI状态。从文档中:“默认实现通过调用层次结构中具有ID的每个视图,并保存当前关注的视图的ID(所有已还原的视图,来为您处理大部分UI实例状态)的默认实现)”ViewonSaveInstanceStateonSaveInstanceState()onRestoreInstanceState(Bundle)
Vicky Chijwani

433

savedInstanceState只保存与活动的当前实例相关联的状态,例如当前导航或选择信息,因此,如果Android的破坏和重新创建一个活动,它可以回来,因为它以前。请参阅文件onCreateonSaveInstanceState

对于更长的状态,请考虑使用SQLite数据库,文件或首选项。请参阅保存持久状态


3
什么时候saveInstanceState == null,什么时候不为null?
Trojan.ZBOT

6
当系统正在创建您的Activity的新实例时,savedInstanceState为null;而在还原时,则为null。
加布里埃尔·卡马拉

6
...提出了一个问题,即系统何时需要创建一个新的Activity实例。退出应用程序的某些方法不会创建捆绑包,因此必须创建一个新实例。这是根本问题。这意味着人们不能依赖捆绑软件的存在,而必须采取其他一些持久存储方式。onSave / onRestoreInstanceState的好处是它是系统可以突然执行的一种机制,而不会消耗大量系统资源。因此,很好地支持它,并具有持久的存储空间,可以更优雅地退出应用程序。
ToolmakerSteve

415

注意它是 使用安全onSaveInstanceStateonRestoreInstanceState 持久性数据,根据在活动状态的文档 http://developer.android.com/reference/android/app/Activity.html

该文档指出(在“活动生命周期”部分中):

请注意,保存持久性数据非常重要,onPause()而不是保存持久性数据,onSaveInstanceState(Bundle) 因为后者不是生命周期回调的一部分,因此不会在文档中描述的每种情况下都调用它。

换句话说,将用于持久数据的保存/恢复代码放在onPause()和中onResume()

编辑:为进一步澄清,这里是onSaveInstanceState()文档:

在活动被杀死之前将调用此方法,以便在将来某个时间返回活动时可以恢复其状态。例如,如果在活动A之前启动了活动B,并且在某个时候活动A被杀死以回收资源,则活动A将有机会通过此方法保存其用户界面的当前状态,以便当用户返回时对于活动A,可以通过onCreate(Bundle)或 恢复用户界面的状态onRestoreInstanceState(Bundle)


55
只是挑剔:这也不是不安全的。这仅取决于您要保留什么以及保留多长时间,@ Bernard在他最初的问题中并不清楚。InstanceState非常适合保留当前UI状态(数据输入控件,列表中的当前位置等),而Pause / Resume是长期持久存储的唯一可能性。
Pontus Gagge 2010年

30
这应该被否决。像生命周期方法一样使用on(Save | Restore)InstanceState是不安全的(即在其中执行除保存/恢复状态之外的任何其他操作)。它们非常适合保存/恢复状态。另外,您要如何在onPause和onResume中保存/恢复状态?您不会在可以使用的那些方法中获得捆绑软件,因此必须在数据库,文件等中使用其他一些节省状态的方法,这很愚蠢。
菲利克斯(Felix)2010年

141
我们不应该对此人投反对票,至少他要努力阅读文档,我认为我们的人在这里实际上是在建立一个知识渊博的社区,并且互相帮助,不要投反对票。因此,请1投票赞成这项工作,我将要求大家不要投反对票,而应该投赞成票或不投赞成票...。此人清除了人们在审阅文档时希望拥有的困惑。1票:)
AZ_

21
我认为这个答案不应该被否决。Atleast他努力回答,并引用了doco的一部分。
GSree 2011年

34
这个答案是绝对正确的,值得赞成,而不是反对!让我为那些看不见的人澄清州之间的差异。GUI状态(如选定的单选按钮和输入字段中的某些文本)比数据状态(如将记录添加到ListView中显示的列表中)要重要得多。后者必须在onPause中存储到数据库中,因为这是唯一保证的调用。如果将其放在onSaveInstanceState中,则可能会丢失未调用的数据。但是,如果出于相同原因未保存单选按钮选择,则没什么大不了的。
JBM

206

我的同事写了一篇文章,解释了Android设备上的应用程序状态,包括有关活动生命周期和状态信息,如何存储状态信息以及如何保存到状态的说明BundleSharedPreferences在此处进行了介绍

本文介绍了三种方法:

使用实例状态包存储局部变量/ UI控制数据,以保持应用程序生命周期(即临时)

[Code sample  Store state in state bundle]
@Override
public void onSaveInstanceState(Bundle savedInstanceState)
{
  // Store UI state to the savedInstanceState.
  // This bundle will be passed to onCreate on next call.  EditText txtName = (EditText)findViewById(R.id.txtName);
  String strName = txtName.getText().toString();

  EditText txtEmail = (EditText)findViewById(R.id.txtEmail);
  String strEmail = txtEmail.getText().toString();

  CheckBox chkTandC = (CheckBox)findViewById(R.id.chkTandC);
  boolean blnTandC = chkTandC.isChecked();

  savedInstanceState.putString(“Name”, strName);
  savedInstanceState.putString(“Email”, strEmail);
  savedInstanceState.putBoolean(“TandC”, blnTandC);

  super.onSaveInstanceState(savedInstanceState);
}

使用共享首选项在应用程序实例之间(即永久)存储局部变量/ UI控制数据

[Code sample  store state in SharedPreferences]
@Override
protected void onPause()
{
  super.onPause();

  // Store values between instances here
  SharedPreferences preferences = getPreferences(MODE_PRIVATE);
  SharedPreferences.Editor editor = preferences.edit();  // Put the values from the UI
  EditText txtName = (EditText)findViewById(R.id.txtName);
  String strName = txtName.getText().toString();

  EditText txtEmail = (EditText)findViewById(R.id.txtEmail);
  String strEmail = txtEmail.getText().toString();

  CheckBox chkTandC = (CheckBox)findViewById(R.id.chkTandC);
  boolean blnTandC = chkTandC.isChecked();

  editor.putString(“Name”, strName); // value to store
  editor.putString(“Email”, strEmail); // value to store
  editor.putBoolean(“TandC”, blnTandC); // value to store
  // Commit to storage
  editor.commit();
}

使用保留的非配置实例在应用程序生存期内的活动之间的活动之间使对象实例保持活动状态

[Code sample  store object instance]
private cMyClassType moInstanceOfAClass; // Store the instance of an object
@Override
public Object onRetainNonConfigurationInstance()
{
  if (moInstanceOfAClass != null) // Check that the object exists
      return(moInstanceOfAClass);
  return super.onRetainNonConfigurationInstance();
}

3
@ MartinBelcher-Eigo文章对SharedPreferences中的数据说:“此数据已写入设备上的数据库。。”我相信该数据存储在文件系统的应用程序目录中的文件中。
汤姆(Tom)

2
@Tom SharefPrefs数据被写入xml文件。xml是一种数据库吗?我会说是;)
MaciejGórski'13

148

这是Android开发的经典“陷阱”。这里有两个问题:

  • Android框架存在一个细微的错误,至少在旧版本上,这使开发过程中的应用程序堆栈管理非常复杂(不确定是否/何时/如何修复)。我将在下面讨论此错误。
  • 解决此问题的“正常”或预期方式本身非常复杂,因为onPause / onResume和onSaveInstanceState / onRestoreInstanceState的双重性

浏览所有这些线程,我怀疑开发人员在很多时候会同时谈论这两个不同的问题……因此,所有的混淆和报告“这对我都不起作用”。

首先,要阐明“预期”行为:onSaveInstance和onRestoreInstance易碎,仅用于瞬态。预期的用途(辅助)是在旋转手机(方向更改)时处理“活动”娱乐。换句话说,预期的用法是您的活动在逻辑上仍处于“最重要”的位置,但仍必须由系统重新实例化。保存的Bundle不会持久保存在process / memory / gc之外,因此,如果您的活动进入后台,您将无法真正依靠它。是的,也许您的Activity的内存可以幸免于它的后台访问并逃脱GC,但这是不可靠的(也不是可预测的)。

因此,如果您遇到在应用程序的“启动”之间存在有意义的“用户进度”或状态的情况,那么指南就是使用onPause和onResume。您必须自己选择并准备一个持久性存储。

但是-有一个非常令人困惑的错误,使所有这些问题变得复杂。详细信息在这里:

http://code.google.com/p/android/issues/detail?id=2373

http://code.google.com/p/android/issues/detail?id=5277

基本上,如果您的应用程序是使用SingleTask标志启动的,然后在以后从主屏幕或启动器菜单启动它,则随后的调用将创建一个NEW任务……您将有效地拥有应用程序的两个不同实例居住在同一个堆栈中...很快就会变得非常奇怪。这似乎是在开发过程中启动应用程序时发生的(即从Eclipse或Intellij),因此开发人员经常遇到这种情况。而且还可以通过某些应用商店更新机制(因此也会影响您的用户)。

在意识到自己的主要问题是此错误而不是预期的框架行为之前,我在这些线程中进行了数小时的奋战。一个伟大的文章和解决方法 (更新:见下文)似乎来自用户@kaciula在此答案中:

Home键按下行为

2013年6月更新:几个月后,我终于找到了“正确的”解决方案。您不需要自己管理任何有状态的startedApp标志,您可以从框架中检测到此问题并适当地保释。我在LauncherActivity.onCreate的开始附近使用它:

if (!isTaskRoot()) {
    Intent intent = getIntent();
    String action = intent.getAction();
    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && action != null && action.equals(Intent.ACTION_MAIN)) {
        finish();
        return;
    }
}

87

onSaveInstanceState当系统需要内存并杀死应用程序时调用。用户只是关闭应用程序时不调用它。因此,我认为应用程序状态也应保存在中onPause,也应保存到某些持久性存储中,例如PreferencesSqlite


36
对不起,那不是很正确。在需要重新创建活动之前,将调用onSaveInstanceState。即每次用户旋转设备时。它用于存储瞬态视图状态。当android强制关闭应用程序时,实际上不会调用onSaveInstanceState(这就是存储重要应用程序数据不安全的原因)。但是,一定要在活动被杀死之前调用onPause,因此应将其用于在首选项或Squlite中存储永久信息。正确答案,错误原因。
moveaway00 2012年

74

这两种方法都是有用且有效的,并且都最适合于不同的情况:

  1. 用户终止应用程序并在以后重新打开它,但是应用程序需要从上一个会话中重新加载数据–这需要持久的存储方法,例如使用SQLite。
  2. 用户切换应用程序,然后返回到原始位置,并希望从上次停止的地方继续工作-将数据包数据(例如应用程序状态数据)保存并还原到其中onSaveInstanceState()onRestoreInstanceState()通常就足够了。

如果以持久方式保存状态数据,则可以通过onResume()onCreate()(或实际上在任何生命周期调用中)重新加载状态数据。这可能是或可能不是期望的行为。如果您将其存储在的捆绑包中InstanceState,则它是瞬态的,仅适合存储在同一用户“会话”中使用的数据(我宽松地使用术语“会话”),但不适用于“会话”之间的数据。

并非所有方法都比其他方法更好,重要的是了解您需要哪种行为并选择最合适的方法。


70

就我而言,节省国家充其量是最大的麻烦。如果需要保存持久性数据,只需使用SQLite数据库。Android使SOOO变得容易。

像这样:

import java.util.Date;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class dataHelper {

    private static final String DATABASE_NAME = "autoMate.db";
    private static final int DATABASE_VERSION = 1;

    private Context context;
    private SQLiteDatabase db;
    private OpenHelper oh ;

    public dataHelper(Context context) {
        this.context = context;
        this.oh = new OpenHelper(this.context);
        this.db = oh.getWritableDatabase();
    }

    public void close() {
        db.close();
        oh.close();
        db = null;
        oh = null;
        SQLiteDatabase.releaseMemory();
    }


    public void setCode(String codeName, Object codeValue, String codeDataType) {
        Cursor codeRow = db.rawQuery("SELECT * FROM code WHERE codeName = '"+  codeName + "'", null);
        String cv = "" ;

        if (codeDataType.toLowerCase().trim().equals("long") == true){
            cv = String.valueOf(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("int") == true)
        {
            cv = String.valueOf(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("date") == true)
        {
            cv = String.valueOf(((Date)codeValue).getTime());
        }
        else if (codeDataType.toLowerCase().trim().equals("boolean") == true)
        {
            String.valueOf(codeValue);
        }
        else
        {
            cv = String.valueOf(codeValue);
        }

        if(codeRow.getCount() > 0) //exists-- update
        {
            db.execSQL("update code set codeValue = '" + cv +
                "' where codeName = '" + codeName + "'");
        }
        else // does not exist, insert
        {
            db.execSQL("INSERT INTO code (codeName, codeValue, codeDataType) VALUES(" +
                    "'" + codeName + "'," +
                    "'" + cv + "'," +
                    "'" + codeDataType + "')" );
        }
    }

    public Object getCode(String codeName, Object defaultValue){

        //Check to see if it already exists
        String codeValue = "";
        String codeDataType = "";
        boolean found = false;
        Cursor codeRow  = db.rawQuery("SELECT * FROM code WHERE codeName = '"+  codeName + "'", null);
        if (codeRow.moveToFirst())
        {
            codeValue = codeRow.getString(codeRow.getColumnIndex("codeValue"));
            codeDataType = codeRow.getString(codeRow.getColumnIndex("codeDataType"));
            found = true;
        }

        if (found == false)
        {
            return defaultValue;
        }
        else if (codeDataType.toLowerCase().trim().equals("long") == true)
        {
            if (codeValue.equals("") == true)
            {
                return (long)0;
            }
            return Long.parseLong(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("int") == true)
        {
            if (codeValue.equals("") == true)
            {
                return (int)0;
            }
            return Integer.parseInt(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("date") == true)
        {
            if (codeValue.equals("") == true)
            {
                return null;
            }
            return new Date(Long.parseLong(codeValue));
        }
        else if (codeDataType.toLowerCase().trim().equals("boolean") == true)
        {
            if (codeValue.equals("") == true)
            {
                return false;
            }
            return Boolean.parseBoolean(codeValue);
        }
        else
        {
            return (String)codeValue;
        }
    }


    private static class OpenHelper extends SQLiteOpenHelper {

        OpenHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE IF  NOT EXISTS code" +
            "(id INTEGER PRIMARY KEY, codeName TEXT, codeValue TEXT, codeDataType TEXT)");
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }
    }
}

之后的一个简单电话

dataHelper dh = new dataHelper(getBaseContext());
String status = (String) dh.getCode("appState", "safetyDisabled");
Date serviceStart = (Date) dh.getCode("serviceStartTime", null);
dh.close();
dh = null;

9
由于加载SQLite数据库的时间太长,因此考虑到这是向用户显示应用程序UI的关键路径。我实际上尚未计时,因此很高兴得到纠正,但是确定加载和打开数据库文件会很快吗?
汤姆(Tom)

5
非常感谢您提供的解决方案,让新手可以将其剪切并粘贴到他们的应用中并立即使用!@Tom就速度而言,大约需要七秒钟来存储1000对,但是您可以在AsyncTask中完成它。但是,您需要添加一个finally {cursor.close()},否则这样做会因内存泄漏而崩溃。
本体

3
我遇到了这个问题,虽然看上去很整洁,但我还是不愿意在Google Glass上尝试使用它,这是我最近正在使用/使用的设备。
Stephen Tetreault,2014年

61

我想我找到了答案。让我用简单的话说说我做了什么:

假设我有两个活动,活动1和活动2,并且我正在从活动1导航到活动2(我在活动2中做了一些工作),然后通过单击活动1中的按钮再次回到活动1。现在,在这个阶段,我想回到活动2,并且我希望在我最后离开活动2时看到我的活动2处于相同的状态。

对于上述情况,我所做的是在清单中做了如下更改:

<activity android:name=".activity2"
          android:alwaysRetainTaskState="true"      
          android:launchMode="singleInstance">
</activity>

在按钮单击事件的activity1中,我这样做是这样的:

Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.setClassName(this,"com.mainscreen.activity2");
startActivity(intent);

在activity2的按钮单击事件中,我这样做是这样的:

Intent intent=new Intent();
intent.setClassName(this,"com.mainscreen.activity1");
startActivity(intent);

现在将发生的事情是,无论我们对活动2所做的更改如何,都不会丢失,并且我们可以以与之前离开时相同的状态查看活动2。

我相信这就是答案,这对我来说很好。如果我错了,请纠正我。


2
@bagusflyer的护理要更具体???您的评论没有帮助,因此没有人可以帮助您。
Stephen Tetreault 2014年

2
这是对不同情况的答案:同一应用程序中的两个活动。OP即将离开该应用程序(例如,主页按钮或其他切换到其他应用程序的方式)。
ToolmakerSteve

44

onSaveInstanceState()用于瞬态数据(在onCreate()/中还原onRestoreInstanceState()),onPause()对于持久数据(在中还原onResume())。来自Android技术资源:

如果Activity被停止,则onSaveInstanceState()将由Android调用,并且可能在恢复之前被杀死!这意味着在重新启动活动时,它应存储将其初始化为相同条件所需的任何状态。它与onCreate()方法相对应,实际上,传递给onCreate()的saveInstanceState Bundle与您在onSaveInstanceState()方法中构造为outState的Bundle相同。

onPause()onResume()也是互补方法。即使在活动结束时始终会调用onPause(),即使我们已对此进行了鼓励(例如,使用finish()调用)。我们将使用它来将当前注释保存回数据库。优良作法是也释放可以在onPause()期间释放的任何资源,以便在处于被动状态时占用更少的资源。


40

onSaveInstanceState()当Activity进入后台时才调用。

来自文档的引文:“在活动可能被杀死之前调用此方法,以便在将来某个时间返回时可以恢复其状态。” 资源


37

为了帮助减少样板,我使用以下内容interfaceclass读取/写入到Bundle保存实例状态。


首先,创建一个用于注释实例变量的接口:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.FIELD
})
public @interface SaveInstance {

}

然后,创建一个类,其中将使用反射将值保存到包中:

import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;

import java.io.Serializable;
import java.lang.reflect.Field;

/**
 * Save and load fields to/from a {@link Bundle}. All fields should be annotated with {@link
 * SaveInstance}.</p>
 */
public class Icicle {

    private static final String TAG = "Icicle";

    /**
     * Find all fields with the {@link SaveInstance} annotation and add them to the {@link Bundle}.
     *
     * @param outState
     *         The bundle from {@link Activity#onSaveInstanceState(Bundle)} or {@link
     *         Fragment#onSaveInstanceState(Bundle)}
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @see #load(Bundle, Object)
     */
    public static void save(Bundle outState, Object classInstance) {
        save(outState, classInstance, classInstance.getClass());
    }

    /**
     * Find all fields with the {@link SaveInstance} annotation and add them to the {@link Bundle}.
     *
     * @param outState
     *         The bundle from {@link Activity#onSaveInstanceState(Bundle)} or {@link
     *         Fragment#onSaveInstanceState(Bundle)}
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @param baseClass
     *         Base class, used to get all superclasses of the instance.
     * @see #load(Bundle, Object, Class)
     */
    public static void save(Bundle outState, Object classInstance, Class<?> baseClass) {
        if (outState == null) {
            return;
        }
        Class<?> clazz = classInstance.getClass();
        while (baseClass.isAssignableFrom(clazz)) {
            String className = clazz.getName();
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(SaveInstance.class)) {
                    field.setAccessible(true);
                    String key = className + "#" + field.getName();
                    try {
                        Object value = field.get(classInstance);
                        if (value instanceof Parcelable) {
                            outState.putParcelable(key, (Parcelable) value);
                        } else if (value instanceof Serializable) {
                            outState.putSerializable(key, (Serializable) value);
                        }
                    } catch (Throwable t) {
                        Log.d(TAG, "The field '" + key + "' was not added to the bundle");
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

    /**
     * Load all saved fields that have the {@link SaveInstance} annotation.
     *
     * @param savedInstanceState
     *         The saved-instance {@link Bundle} from an {@link Activity} or {@link Fragment}.
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @see #save(Bundle, Object)
     */
    public static void load(Bundle savedInstanceState, Object classInstance) {
        load(savedInstanceState, classInstance, classInstance.getClass());
    }

    /**
     * Load all saved fields that have the {@link SaveInstance} annotation.
     *
     * @param savedInstanceState
     *         The saved-instance {@link Bundle} from an {@link Activity} or {@link Fragment}.
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @param baseClass
     *         Base class, used to get all superclasses of the instance.
     * @see #save(Bundle, Object, Class)
     */
    public static void load(Bundle savedInstanceState, Object classInstance, Class<?> baseClass) {
        if (savedInstanceState == null) {
            return;
        }
        Class<?> clazz = classInstance.getClass();
        while (baseClass.isAssignableFrom(clazz)) {
            String className = clazz.getName();
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(SaveInstance.class)) {
                    String key = className + "#" + field.getName();
                    field.setAccessible(true);
                    try {
                        Object fieldVal = savedInstanceState.get(key);
                        if (fieldVal != null) {
                            field.set(classInstance, fieldVal);
                        }
                    } catch (Throwable t) {
                        Log.d(TAG, "The field '" + key + "' was not retrieved from the bundle");
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

}

用法示例:

public class MainActivity extends Activity {

    @SaveInstance
    private String foo;

    @SaveInstance
    private int bar;

    @SaveInstance
    private Intent baz;

    @SaveInstance
    private boolean qux;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Icicle.load(savedInstanceState, this);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Icicle.save(outState, this);
    }

}

注意:此代码改编自名为androidautowire的库项目,该项目已获得MIT许可


34

同时,我一般不再使用

Bundle savedInstanceState & Co

对于大多数活动而言,生命周期太复杂且不必要。

Google表示自己,甚至都不可靠。

我的方法是立即保存首选项中的所有更改:

 SharedPreferences p;
 p.edit().put(..).commit()

在某种程度上,SharedPreferences的工作方式类似于Bundles。当然,首先必须从首选项中读取此类值。

对于复杂数据,您可以使用SQLite而不是使用首选项。

应用此概念时,活动将继续使用上次保存的状态,而不管它是初次打开是在两次启动之间进行重新启动还是由于后退堆栈而重新打开。


31

直接回答原始问题。savedInstancestate为null,因为永远不会重新创建您的Activity。

在以下情况下,将仅使用状态捆绑包重新创建您的活动:

  • 配置更改(例如更改方向或电话语言)可能需要创建新的活动实例。
  • 在操作系统销毁活动之后,您将从后台返回应用程序。

当处于内存压力下或长时间处于后台运行时,Android会销毁后台活动。

测试您的hello world示例时,有几种方法可以离开并返回到Activity。

  • 当您按下返回按钮时,活动结束。重新启动该应用程序是一个全新的实例。您根本不会从后台恢复。
  • 当您按下主页按钮或使用任务切换器时,活动将进入后台。导航回应用程序时,仅在必须销毁Activity的情况下才调用onCreate。

在大多数情况下,如果您只是按回家,然后再次启动该应用程序,则无需重新创建活动。它已经存在于内存中,因此不会调用onCreate()。

在“设置”->“开发人员选项”下有一个名为“不保留活动”的选项。启用后,Android会始终销毁活动并在后台将其重新创建。这是在开发时保持启用状态的绝佳选择,因为它会模拟最坏的情况。(内存不足的设备会一直回收您的活动)。

其他答案很有价值,因为它们可以教给您正确的状态存储方式,但我不认为他们真正回答了为什么您的代码未按预期的方式工作。


28

onSaveInstanceState(bundle)onRestoreInstanceState(bundle)方法可用于仅在旋转的同时在屏幕上(方向改变)的数据持久性是有用的。
他们不是在应用程序之间切换(因为即使是好的onSaveInstanceState()方法被调用,但onCreate(bundle)onRestoreInstanceState(bundle)没有再次调用。
欲了解更多持久使用共享偏好。阅读这篇文章


2
在您的情况下onCreateonRestoreInstanceState不会被调用,因为Activity切换应用程序时不会完全破坏,因此无需还原任何内容。Android会调用onSaveInstanceState以防万一Activity稍后被销毁(旋转屏幕时会以100%的确定性发生,因为整个设备配置已更改,并且必须从头开始重新创建Activity)。
Vicky Chijwani

20

我的问题是我仅在应用程序生命周期内需要持久性(即一次执行,包括在同一应用程序中启动其他子活动并旋转设备等)。我尝试了上述答案的各种组合,但并没有在所有情况下都得到我想要的。最后,对我有用的是在onCreate期间获取对savedInstanceState的引用:

mySavedInstanceState=savedInstanceState;

并在需要时使用它来获取变量的内容,大致如下:

if (mySavedInstanceState !=null) {
   boolean myVariable = mySavedInstanceState.getBoolean("MyVariable");
}

我按照上面的建议使用onSaveInstanceStateonRestoreInstanceState,但是我想我也可以或者替代地使用我的方法在变量更改时保存变量(例如,使用putBoolean


19

尽管接受的答案是正确的,但是有一种更快更简便的方法可以使用名为Icepick的库在Android上保存“活动”状态。Icepick是一个注释处理器,负责为您保存和恢复状态时使用的所有样板代码。

用Icepick做这样的事情:

class MainActivity extends Activity {
  @State String username; // These will be automatically saved and restored
  @State String password;
  @State int age;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Override public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }
}

与此相同:

class MainActivity extends Activity {
  String username;
  String password;
  int age;

  @Override
  public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    savedInstanceState.putString("MyString", username);
    savedInstanceState.putString("MyPassword", password);
    savedInstanceState.putInt("MyAge", age); 
    /* remember you would need to actually initialize these variables before putting it in the
    Bundle */
  }

  @Override
  public void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    username = savedInstanceState.getString("MyString");
    password = savedInstanceState.getString("MyPassword");
    age = savedInstanceState.getInt("MyAge");
  }
}

Icepick将与任何使用来保存其状态的对象一起使用Bundle


16

创建活动时,将调用其onCreate()方法。

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

savedInstanceState是Bundle类的一个对象,该对象第一次为null,但在重新创建它时将包含值。要保存活动状态,您必须覆盖onSaveInstanceState()。

   @Override
    protected void onSaveInstanceState(Bundle outState) {
      outState.putString("key","Welcome Back")
        super.onSaveInstanceState(outState);       //save state
    }

将值放入outState.putString(“ key”,“ Welcome Back”)之类的“ outState”捆绑对象中,并通过调用super进行保存。当活动将被销毁时,其状态将保存在Bundle对象中,并可以在onCreate()或onRestoreInstanceState()中重新创建后恢复。onCreate()和onRestoreInstanceState()中收到的捆绑包是相同的。

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

          //restore activity's state
         if(savedInstanceState!=null){
          String reStoredString=savedInstanceState.getString("key");
            }
    }

要么

  //restores activity's saved state
 @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
      String restoredMessage=savedInstanceState.getString("key");
    }

15

基本上有两种方法可以实现此更改。

  1. 使用onSaveInstanceState()onRestoreInstanceState()
  2. 在明显android:configChanges="orientation|screenSize"

我真的不建议使用第二种方法。根据我的经验之一,从纵向旋转到横向旋转时,会导致一半的设备屏幕变黑。

使用上述第一种方法,我们可以在方向改变或任何配置更改发生时保留数据。我知道一种可以在saveInstance状态对象内存储任何类型的数据的方法。

示例:如果要保留Json对象,请考虑以下情况。使用getter和setter创建模型类。

class MyModel extends Serializable{
JSONObject obj;

setJsonObject(JsonObject obj)
{
this.obj=obj;
}

JSONObject getJsonObject()
return this.obj;
} 
}

现在在活动中的onCreate和onSaveInstanceState方法中执行以下操作。它看起来像这样:

@override
onCreate(Bundle savedInstaceState){
MyModel data= (MyModel)savedInstaceState.getSerializable("yourkey")
JSONObject obj=data.getJsonObject();
//Here you have retained JSONObject and can use.
}


@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//Obj is some json object 
MyModel dataToSave= new MyModel();
dataToSave.setJsonObject(obj);
oustate.putSerializable("yourkey",dataToSave); 

}

11

这是史蒂夫·莫斯利Steve Moseley)的回答(由ToolmakerSteve提出),它把事情放在了透视中(总体上,onSaveInstanceState与onPause,东成本与西成本传奇)

@VVK-我部分不同意。退出应用程序的某些方式不会触发onSaveInstanceState(oSIS)。这限制了oSIS的用途。它值得支持,以减少操作系统资源,但是如果应用程序想要使用户返回到他们所处的状态,则无论应用程序如何退出,都必须使用持久性存储方法。 我使用onCreate来检查包,如果丢失,则检查 持久性存储。这样可以集中决策。我可以从崩溃中恢复,也可以从后退按钮退出或自定义菜单项“退出”中恢复,也可以在许多天后恢复到屏幕上的用户状态。– ToolmakerSteve 15年9月19日在10:38


10

Kotlin代码:

救:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState.apply {
        putInt("intKey", 1)
        putString("stringKey", "String Value")
        putParcelable("parcelableKey", parcelableObject)
    })
}

然后在onCreate()onRestoreInstanceState()

    val restoredInt = savedInstanceState?.getInt("intKey") ?: 1 //default int
    val restoredString = savedInstanceState?.getString("stringKey") ?: "default string"
    val restoredParcelable = savedInstanceState?.getParcelable<ParcelableClass>("parcelableKey") ?: ParcelableClass() //default parcelable

如果您不想使用可选值,请添加默认值


9

要获取存储在中的活动状态数据onCreate(),首先您必须通过覆盖SaveInstanceState(Bundle savedInstanceState)方法将数据保存在savedInstanceState中。

当Activity destroy SaveInstanceState(Bundle savedInstanceState)方法被调用时,您将保存要保存的数据。与onCreate()活动重新启动时一样。(savedInstanceState不会为null,因为在活动被销毁之前已在其中保存了一些数据)


6

解决此问题的简单快速方法是使用IcePick

首先,在 app/build.gradle

repositories {
  maven {url "https://clojars.org/repo/"}
}
dependencies {
  compile 'frankiesardo:icepick:3.2.0'
  provided 'frankiesardo:icepick-processor:3.2.0'
}

现在,让我们在下面的示例中查看如何在“活动”中保存状态

public class ExampleActivity extends Activity {
  @State String username; // This will be automatically saved and restored

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Override public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }
}

它适用于“活动”,“片段”或需要在Bundle上序列化其状态的任何对象(例如,迫击炮的ViewPresenters)

Icepick还可以为自定义视图生成实例状态代码:

class CustomView extends View {
  @State int selectedPosition; // This will be automatically saved and restored

  @Override public Parcelable onSaveInstanceState() {
    return Icepick.saveInstanceState(this, super.onSaveInstanceState());
  }

  @Override public void onRestoreInstanceState(Parcelable state) {
    super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state));
  }

  // You can put the calls to Icepick into a BaseCustomView and inherit from it
  // All Views extending this CustomView automatically have state saved/restored
}

1
@ralphspoon是的,它适用于片段和自定义视图。请检查示例代码。我编辑了答案。我建议您在github.com/frankiesardo/icepick上查看官方文档,以找到更多代码示例。
THANN Phearum'9

@ChetanMehra是指自定义视图类,对吗?如果是自定义视图,则可以像上面的CustomView示例一样覆盖onSaveInstanceState和onRestoreInstanceState。
THANN Phearum '19

我的意思是,例如,视图类内部的类对象:CustomView类扩展View {@State ClassA a;}或CustomView类扩展View {@ State内部类{}}
Chetan Mehra

@THANNPhearum我应该再问一个问题吗?
Chetan Mehra

我知道了。如果是这样,您的ClassA应该是可包裹的。正如它提到的那样,它适用于“活动”,“片段”或需要在捆绑中序列化其状态的任何对象
THANN Phearum,

6

不知道我的解决方案是否被皱眉,但是我使用绑定服务来保持ViewModel状态。您将其存储在服务的内存中还是持久保存并从SQLite数据库中检索取决于您的要求。这是任何形式的服务所要做的,它们提供诸如维护应用程序状态和抽象通用业务逻辑之类的服务。

由于移动设备固有的内存和处理限制,我以与网页类似的方式对待Android视图。该页面不维护状态,它纯粹是表示层组件,其唯一目的是表示应用程序状态并接受用户输入。Web应用程序体系结构的最新趋势使用了古老的模型,视图,控制器(MVC)模式,其中页面是视图,域数据是模型,控制器位于Web服务之后。可以在Android中使用相同的模式,而View是……View,模型是您的域数据,而Controller被实现为Android绑定服务。每当您希望视图与控制器交互时,请在启动/恢复时绑定到该视图,并在停止/暂停时取消绑定。

这种方法为您提供了加强“关注分离”设计原则的额外好处,因为您可以将所有应用程序业务逻辑转移到您的服务中,从而减少了跨多个视图的重复逻辑,并允许视图执行另一个重要的设计原理,即“单一职责”。


5

科特林

您必须重写onSaveInstanceStateonRestoreInstanceState存储和检索要持久化的变量

生命周期图

存储变量

public override fun onSaveInstanceState(savedInstanceState: Bundle) {
    super.onSaveInstanceState(savedInstanceState)

    // prepare variables here
    savedInstanceState.putInt("kInt", 10)
    savedInstanceState.putBoolean("kBool", true)
    savedInstanceState.putDouble("kDouble", 4.5)
    savedInstanceState.putString("kString", "Hello Kotlin")
}

检索变量

public override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)

    val myInt = savedInstanceState.getInt("kInt")
    val myBoolean = savedInstanceState.getBoolean("kBool")
    val myDouble = savedInstanceState.getDouble("kDouble")
    val myString = savedInstanceState.getString("kString")
    // use variables here
}

2

现在,Android提供了用于保存状态的ViewModels,您应该尝试使用它而不是saveInstanceState。


3
这不是真的。从文档中:“与保存的实例状态不同,ViewModel在系统启动的进程死亡期间被破坏。这就是为什么您应将ViewModel对象与onSaveInstanceState()(或其他一些磁盘持久性)结合使用,将标识符存放在saveInstanceState中以帮助查看系统死后,模型会重新加载数据。”
Vyacheslav Martynenko '18 -10-5

只是在后台更改权限时遇到了这种情况。
Brill Pappin

我同意,从文档中可以得出“如果您需要处理系统启动的进程死亡,则可能要使用onSaveInstanceState()作为备份。”
Zhar

2

有一种方法可以使Android在不执行任何方法的情况下保存状态。只需将此行添加到您在“活动”声明中的清单中即可:

android:configChanges="orientation|screenSize"

它看起来应该像这样:

<activity
    android:name=".activities.MyActivity"
    android:configChanges="orientation|screenSize">
</activity>

在这里您可以找到有关此属性的更多信息。

与手动处理相比,建议让Android为您处理。


2
这与保存状态无关,您只是放弃方向更改,请记住,您可以针对不同事件随时重新启动,暂停和恢复应用程序
lord-ralf-adolf

1
此答案适用于那些希望在方向更改时保存状态并希望避免理解和执行复杂方法的人
IgniteCoders

公平地说,我明白您的意思,我认为大多数为节省状态而苦苦挣扎的人都在使用片段,因为只要ID有活动,活动实际上就会保存UI组件的状态,但是片段更为特殊,我曾经使用过片段,但我永远不会使用他们再次遇到了保存实例统计数据的问题
Lord-ralf-adolf

它的工作原理...谢谢
Fanadez

1

保存什么,不保存什么?

有没有想过为什么EditText方向改变时自动保存文本?好吧,这个答案是给你的。

当一个Activity的实例被销毁并且系统重新创建一个新实例时(例如,配置更改)。它尝试使用一组旧的活动状态(实例状态)的已保存数据来重新创建它。

实例状态是存储在对象中的键/值对的集合Bundle

例如,默认情况下,系统将View对象保存在Bundle中。

  • 文字输入 EditText
  • 滚动位置ListView等。

如果需要将另一个变量保存为实例状态的一部分,则应使用OVERRIDE onSavedInstanceState(Bundle savedinstaneState)方法。

例如,int currentScore在GameActivity中

有关在保存数据时onSavedInstanceState(Bundle savedinstaneState)的更多详细信息

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current game state
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(savedInstanceState);
}

因此,如果错误地忘记了调用 super.onSaveInstanceState(savedInstanceState);默认行为,则不会起作用,即EditText中的Text将不会保存。

恢复活动状态时应选择哪个?

 onCreate(Bundle savedInstanceState)

要么

onRestoreInstanceState(Bundle savedInstanceState)

两种方法都获得相同的Bundle对象,因此在何处编写恢复逻辑并不重要。唯一的区别是,在onCreate(Bundle savedInstanceState)方法中,您将必须进行空检查,而在后一种情况下则不需要。其他答案已经编写了代码片段。您可以参考他们。

有关onRestoreInstanceState(Bundle savedinstaneState)的更多详细信息

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState);

    // Restore state members from the saved instance
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
}

始终调用super.onRestoreInstanceState(savedInstanceState);以便系统默认还原视图层次结构

奖金

onSaveInstanceState(Bundle savedInstanceState)由系统调用,只有当用户打算回来了活动。例如,您使用App X,突然接到电话。您转到呼叫者应用,然后回到应用X。在这种情况下,onSaveInstanceState(Bundle savedInstanceState)将调用方法。

但是,如果用户按下“后退”按钮,请考虑一下。假定用户不打算返回该活动,因此在这种情况下onSaveInstanceState(Bundle savedInstanceState),系统不会调用该用户。重点是您在保存数据时应考虑所有方案。

相关链接:

默认行为演示
Android官方文档


1

现在,在视图模型中执行两种方法很有意义。如果要将第一个另存为已保存的实例:可以像这样在视图模型中添加状态参数 https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate#java

或者您可以在视图模型中保存变量或对象,在这种情况下,视图模型将保留生命周期,直到活动被销毁为止。

public class HelloAndroidViewModel extends ViewModel {
   public Booelan firstInit = false;

    public HelloAndroidViewModel() {
        firstInit = false;
    }
    ...
}

public class HelloAndroid extends Activity {

  private TextView mTextView = null;
  HelloAndroidViewModel viewModel = ViewModelProviders.of(this).get(HelloAndroidViewModel.class);
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mTextView = new TextView(this);

    //Because even if the state is deleted, the data in the viewmodel will be kept because the activity does not destroy
    if(!viewModel.firstInit){
        viewModel.firstInit = true
        mTextView.setText("Welcome to HelloAndroid!");
    }else{
       mTextView.setText("Welcome back.");
    }

    setContentView(mTextView);
  }
}

您是对的,但是该库仍在发布中,所以我认为我们应该等待...
Zhar

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.