# 七、应用数据

大多数*安卓编程简洁地说*专注于开发安卓应用的用户界面。了解如何管理活动生命周期、显示信息、收集输入和布局屏幕将对创建您的第一个安卓应用程序大有帮助。然而，大多数应用程序也需要一种方法来存储它们收集的数据。在这一章中，我们将简要介绍安卓的几种数据存储选项。

首先，我们将研究共享首选项，它们是简单的键值对，在应用程序之外持续存在。然后，我们将学习如何访问安卓的内部存储。最后，我们将介绍安卓的 SQLite API。 *ApplicationData* 项目提供了这三种存储机制的工作示例。

## 共享偏好

安卓的共享偏好框架是用户会话之间存储信息最简单的方式。它允许您使用键值对存储原始数据(布尔值、浮点数、整数、长整型和字符串)，很像一个持久哈希表。一个活动可以有一个或多个与之关联的`SharedPreferences`对象。

[共享引用](http://developer.android.com/reference/android/content/SharedPreferences.html)类提供对存储值的访问，[共享引用。编辑器](http://developer.android.com/reference/android/content/SharedPreferences.Editor.html)类允许您修改这些值。要使用`SharedPreferences`存储值，您首先需要访问该活动的共享首选项。如果一个活动只需要一个首选项文件，应该使用 [getPreferences()](http://developer.android.com/reference/android/app/Activity.html#getPreferences(int)) ，但是如果需要多个文件，可以使用 [getSharedPreferences()](http://developer.android.com/reference/android/content/ContextWrapper.html#getSharedPreferences(java.lang.String,%20int)) 方法指定首选项文件名。

这两种方法还允许您指定允许谁访问和修改首选项文件。虽然可以使用`MODE_WORLD_READABLE`和`MODE_WORLD_WRITEABLE`创建可从其他应用程序访问的偏好文件，但这可能会在您的应用程序中打开安全漏洞。所以，你应该*始终*使用`MODE_PRIVATE`作为首选文件的范围。如果您需要与其他应用程序共享存储的值，您应该使用类似 [ContentProvider](http://developer.android.com/reference/android/content/ContentProvider.html) 的东西。

一旦有了`SharedPreferences`的实例，就可以通过将所需的键传递给`getBoolean()`、`getInt()`、`getFloat()`和`getString()`等方法来读取保存的值。要记录值，首先需要通过在`SharedPreferences`实例上调用`edit()`来获取一个`SharedPreferences.Editor`对象。然后，可以用`putBoolean()`、`putFloat()`等方法设置键值对。最后，您*必须*调用编辑器的`commit()`方法来保存任何更新的值。

以下版本的`MainActivity.java`向您展示了如何使用`SharedPreferences`记录来自`<EditText>`元素的输入，并在活动加载时显示该值:

```java
    package com.example.applicationdata;

    import android.os.Bundle;
    import android.app.Activity;
    import android.view.Menu;
    import android.view.KeyEvent;
    import android.widget.TextView;
    import android.widget.TextView.OnEditorActionListener;
    import android.widget.EditText;

    import android.content.SharedPreferences;

    public class MainActivity extends Activity {

    private static String SHARED_PREFS_KEY = "existingInput";

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

    // Set up the EditText
    EditText prefsText = (EditText) findViewById(R.id.sharedPrefsText);
    prefsText.setOnEditorActionListener(new OnEditorActionListener() {
    public boolean onEditorAction(TextView textView,
    int actionId,
                                     KeyEvent event) {
                      String input = textView.getText().toString();
    saveStringWithSharedPreferences(SHARED_PREFS_KEY, input);
    return false;
                    }
    });

    // Load the string from SharedPreferences
    SharedPreferences prefs = getPreferences(MODE_PRIVATE);
    String existingInput = prefs.getString(SHARED_PREFS_KEY, "");
    prefsText.setText(existingInput);
    }

    public void saveStringWithSharedPreferences(String key, String value) {
    // Get the SharedPreferences editor.
    SharedPreferences prefs = getPreferences(MODE_PRIVATE);
    SharedPreferences.Editor editor = prefs.edit();

    // Save the string.
    editor.putString(key, value);

    // Commit the changes.
    editor.commit();
    }
    }

```

请注意，`SharedPreferences`的 getter 方法允许您指定一个默认值作为它们的第二个参数，正如您可以在上面代码中的`pref.getString(SHARED_PREFS_KEY, "")`调用中看到的。

## 内储

虽然`SharedPreferences`为存储简单数据提供了方便的抽象，但它并不总是适合更复杂的数据结构或记录用户的文档。作为替代方案，安卓应用程序可以直接在设备的硬盘上存储信息。然而，像`SharedPreferences`一样，这些文件是私有的——由于安全漏洞，其他应用程序不应该被允许访问它们。卸载应用程序时，保存到内部存储的文件会被删除。

要将数据保存到文件中，首先需要用 [Context.openFileOutput()](http://developer.android.com/reference/android/content/Context.html#openFileOutput(java.lang.String,%20int)) 打开文件。这将返回一个[文件输出流](http://developer.android.com/reference/java/io/FileOutputStream.html)，其`write()`方法使您能够向文件中添加字节。当你完成向文件写入数据时，你必须用它的`close()`方法关闭它。下面的方法向您展示了如何在文件中存储字符串:

```java
    public void saveStringWithInternalStorage(String filename,
    String value) throws IOException {
    FileOutputStream output = openFileOutput(filename, MODE_PRIVATE);
    byte[] data = value.getBytes();
    output.write(data);
    output.close();
    }

```

注意`FileOutputStream`是用字节工作的，所以字符串在传递给`write()`之前必须转换。

要读回这个字符串数据，需要用 [openFileInput()](http://developer.android.com/reference/android/content/Context.html#openFileInput(java.lang.String)) 打开文件，返回一个 [FileInputStream](http://developer.android.com/reference/java/io/FileInputStream.html) 对象。然后，您可以将文件内容读入字节数组。完成后，不要忘记关闭输入流。以下示例加载前一个片段保存的字符串，并将其显示在`EditText`小部件中:

```java
    FileInputStream input = null;
    try {
    // Open the file.
    input = openFileInput(FILENAME);
    // Read the byte data.
    int maxBytes = input.available();
    byte[] data = new byte[maxBytes];
    input.read(data, 0, maxBytes);
    while (input.read(data) != -1) {};
    // Turn it into a String and display it.
    String existingInput = new String(data);
    prefsText.setText(existingInput);
    } catch (IOException e) {
    e.printStackTrace();
    } finally {
    if (input != null) {
    try {
    input.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }

```

`FileInputStream.available()`方法返回可以读取的估计字节数，而不会阻塞更多的输入。我们用它来计算新字节数组的长度，它由`FileInputStream.read()`填充。这些字节然后被转换成字符串并显示给用户。

## SQLite 数据库

安卓还为本地 SQLite 数据库提供内置支持。这对于需要收集大量信息并高效查询的数据密集型应用程序非常有用。本节向您展示如何在安卓设备上创建 SQLite 数据库，创建一个表，向其中插入数据，并使用 SQL 查询它。

将原始 SQL 与应用程序代码的其余部分混合在一起会很快变得混乱，因此安卓使用某些约定来尽可能多地从应用程序代码中抽象出数据库交互。例如，每个表应该由一个专用类来表示。这提供了定义表和列名的单一位置，以及创建、升级和访问表的标准化方法。

### 代表数据库

要为特定的表创建类表示，您需要扩展 [SQLiteOpenHelper](http://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelper.html) 类，并分别覆盖其`onCreate()`和`onUpgrade()`方法来创建表并将其升级到新版本。在这个类中，通常将表名和所有列定义为静态最终变量。例如，存储在 SQLite 数据库文件 *messages.db* 中的一个名为 *messages* 的表可能由一个名为`MessageOpenHelper`的类表示，如下所示:

```java
    import android.content.Context;
    import android.database.sqlite.SQLiteDatabase;
    import android.database.sqlite.SQLiteDatabase.CursorFactory;
    import android.database.sqlite.SQLiteOpenHelper;

    public class MessageOpenHelper extends SQLiteOpenHelper {

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

    public static final String TABLE_MESSAGES = "messages";
    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_AUTHOR = "author";
    public static final String COLUMN_MESSAGE = "message";

    private static final String DATABASE_CREATE = "create table "
    + TABLE_MESSAGES + "("
    + COLUMN_ID + " integer primary key autoincrement, "
    + COLUMN_AUTHOR + " text not null"
    + COLUMN_MESSAGE + " text not null);";

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

    @Override
    public void onCreate(SQLiteDatabase db) {
    db.execSQL(DATABASE_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    // This implementation will destroy all the old data, which
    // probably isn't what you want to do in a real application.
    db.execSQL("drop table if exists " + TABLE_MESSAGES);
    onCreate(db);
    }

    }

```

整个表由类开头的最终静态变量定义。`DATABASE_VERSION`和`DATABASE_NAME`定义了 SQLite 数据库版本和文件名，而下面的四行定义了一个名为*消息*的表，该表有三列，分别名为`_id`、`author`和`message`。最后，`DATABASE_CREATE`变量包含创建*消息*表的原始 SQL。这是您将编写原始 SQL 的唯一地方之一，它被整齐地包含在静态最终变量中。同样，这种结构更像是一种建议的惯例，而不是一种硬性规定。

该类的构造函数需要将数据库名称和版本传递给超类。`onCreate()`方法的工作是初始化与类关联的 SQL 表。它被传递了一个表示数据库的[sqliteradatabase](http://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html)实例，我们所要做的就是执行我们之前使用其`execSQL()`方法定义的`DATABASE_CREATE`字符串。同样，`onUpgrade()`方法的工作是将表升级到新版本。上面的实现只是删除表并重新创建它，这对于您的实际应用程序来说可能是需要的，也可能是不需要的。

### 访问数据库

`SQLiteOpenHelper`方法使得创建和访问底层数据库变得非常容易。你所要做的就是实例化你的自定义`SQLiteOpenHelper`并请求一个带有`getReadableDatabase()`或`getWritableDatabase()`的数据库。例如，要创建一个名为`messages.db`的数据库并连接到它，您只需要以下内容:

```java
    MessageOpenHelper dbHelper = new MessageOpenHelper(this);
    SQLiteDatabase db = dbHelper.getWritableDatabase();

```

`getReadableDatabase()`和`getWritableDatabase()`方法负责创建底层的 SQLite 数据库(如果它还不存在的话)*和*打开它进行读取和/或写入。由`MessageOpenHelper`定义的`onCreate()`方法用于创建数据库。返回的`SQLiteDatabase`对象提供了改变和查询数据库中的表的方法。

当您完成与数据库的交互时，您应该总是通过在您的`SQLiteOpenHelper`实例上调用`close()`来关闭数据库。

### 插排

将行插入`SQLiteDatabase`对象是一个两步的过程。首先，您需要创建一个[内容值](http://developer.android.com/reference/android/content/ContentValues.html)对象来表示您想要插入的值。一个单独的`ContentValues`实例代表一个单独的记录，您可以通过将每个列和值传递给其`put()`方法来定义它。通常，您会希望从自定义`SQLiteOpenHelper`中定义的静态变量中获取列名。

其次，您需要将那个`ContentValues`对象传递给`SQLiteDatabase`的`insert()`方法，以及您想要插入的表的名称。例如，以下方法(在示例项目的`MainActivity.java`中定义)在每次调用时都会向*消息*表中添加一条新记录。

```java
    public void saveStringWithDatabase(String value) {
    // Store the author and message in a ContentValues object.
    ContentValues values = new ContentValues();
    values.put(MessageOpenHelper.COLUMN_AUTHOR, AUTHOR_NAME);
    values.put(MessageOpenHelper.COLUMN_MESSAGE, value);

    // Record that ContentValues in a SQLite database
    MessageOpenHelper dbHelper = new MessageOpenHelper(this);
    SQLiteDatabase db = dbHelper.getWritableDatabase();
    long id = db.insert(MessageOpenHelper.TABLE_MESSAGES, null, values);
    Log.d(TAG,
    String.format("Saved new record to database with ID: %d", id));
    dbHelper.close();
    }

```

注意当我们使用完数据库时，我们是如何调用`dbHelper.close()`调用的。还要注意我们是如何通过`MessageOpenHelper`的静态变量访问表名的，这样就不会不小心输入错误。

### 查询数据库

要从 SQLiteDatabase 实例中检索记录，需要将查询信息传递给它的一个`query()`方法。各种查询()重载为所有标准的 SQL 查询参数提供参数。您可以定义要选择的列、选择约束、分组和排序行为以及选择限制。

记录作为[光标](http://developer.android.com/reference/android/database/Cursor.html)对象返回，您可以使用它来遍历选定的行，并将包含的值转换为 Java 类型。例如，以下代码片段打开`messages.db`并从*作者*列中有`AUTHOR_NAME`的行中选择 *_id* 和*消息*列:

```java
    // Load the most recent record from the SQLite database.
    MessageOpenHelper dbHelper = new MessageOpenHelper(this);
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    // Fetch the records with the appropriate author name.
    String[] columns = {MessageOpenHelper.COLUMN_ID,
    MessageOpenHelper.COLUMN_MESSAGE};
    String selection = MessageOpenHelper.COLUMN_AUTHOR + " = '" + AUTHOR_NAME + "'";
    Cursor cursor = db.query(MessageOpenHelper.TABLE_MESSAGES,
    columns, selection, null, null, null, null);
    // Display the most recent record in the text field.
    cursor.moveToLast();
    long id = cursor.getLong(0);
    String message = cursor.getString(1);
    Log.d(TAG, String.format("Retrieved info from database. ID: %d Message: %s", id, message));
    prefsText.setText(message);
    // Clean up.
    cursor.close();
    dbHelper.close();

```

`moveToLast()`方法将光标移动到最后选定的行，在这种情况下，该行应该是最近的记录。要提取值，可以使用像`getLong()`和`getString()`这样的方法，在列位置传递。请注意，这个位置是由我们传递给`query()`的列数组定义的，而不是数据库中列的顺序。当我们完成结果后，我们通过在`cursor`和`SQLiteOpenHelper`上调用`close()`进行清理。

虽然这一部分只介绍了安卓的 SQLite API 的基础知识，但请记住，安卓还提供了更高级的 SQL 功能，包括数据库锁定和事务。

## 总结

本章讨论了在安卓设备上存储数据的三种最常见的方法。我们从共享首选项开始，它提供了一种存储键值对的便捷方式。然后，我们学习了如何将数据存储在设备内部存储的文件中，这比共享首选项更灵活。最后，我们通过创建一个 SQLite 数据库，插入一些行，然后再把它们读出来，简单地看了一下 Android 内置的 SQLite 功能。

这本书的大部分讨论了如何显示和收集用户的信息。结合本章，您现在应该能够收集和存储您可能需要的几乎任何类型的用户数据。我希望，有了这些技能，你已经准备好冒险进入安卓生态系统，开始构建自己的安卓应用程序。祝你好运！