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

我一直在研究 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 分钟左右的时间,并且没有发现任何明显的问题。

答案

您需要重写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");
}

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

savedInstanceState仅用于保存与 Activity 的当前实例相关联的状态,例如当前的导航或选择信息,因此,如果 Android 销毁并重新创建 Activity,它可以像以前一样返回。请参阅文档onCreateonSaveInstanceState

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

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

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

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

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

编辑 :为进一步说明,这是onSaveInstanceState()文档:

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

我的同事写了一篇文章,解释了 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();
}

这是 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;
    }
}

当系统需要内存并杀死应用程序时,将调用onSaveInstanceState 。用户只是关闭应用程序时不调用它。所以我认为应用程序状态也应该保存在onPause并且应该保存到诸如PreferencesSqlite类的持久存储中

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

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

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

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

就我而言,节省国家充其量是最大的麻烦。如果需要保存持久性数据,只需使用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;

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

假设我有两个活动,即活动 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。

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

onSaveInstanceState()用于临时数据(已还原到onCreate() / onRestoreInstanceState() ), onPause()用于永久性数据(已还原到onResume() )。来自 Android 技术资源:

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

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