四、用于设备上持久性的 JSON

Abstract

移动应用或游戏的一个关键方面是需要设备上的持久性。Android 为持久性提供了许多非常好的选项,包括关系存储。然而,所有提供的使用关系存储的机制都需要相当长的开发时间。本章为许多简单到中等程度的持久性需求提供了一条捷径。该方法使用 GSON/JSON 技术将对象树直接存储在非 SQL 存储中,例如设备上的共享首选项和内部文件存储方式。

移动应用或游戏的一个关键方面是需要设备上的持久性。Android 为持久性提供了许多非常好的选项,包括关系存储。然而,所有提供的使用关系存储的机制都需要相当长的开发时间。本章为许多简单到中等程度的持久性需求提供了一条捷径。该方法使用 GSON/JSON 技术将对象树直接存储在非 SQL 存储中,比如共享首选项和内部文件存储。

JSON 代表 JavaScript 对象符号。GSON 是 Google 项目中的一个 Java 库,它使用 JSON 结构将 Java 对象序列化和反序列化为字符串。

你会惊讶地发现 GSON 是如何提高你的移动应用的工作效率的。尤其是那些需要中等持久性的应用可以从 GSON 中受益。在这种方法中,您将应用的持久状态表示为一个 JSON 字符串。然后,您将使用神奇的 GSON Java 库将您的应用对象转换为 JSON,并将生成的 JSON 字符串持久化,或者保存在共享的首选项中,或者保存在设备的内部文件中。

这种方法适合快速编写中等大小的应用。使用这种方法,每隔几周发布一个新的应用是合理的,而开发这样的应用可能需要一两个月。事实上,在某些应用上,你可能会获得两到三倍的优势。即使当您考虑复杂的应用时,您也可以从这种方法中获得显著的优势,因为您可以将想法原型化,并在有限的版本中进行测试。

为了帮助您理解这种基于 JSON 的应用存储方法,我们在本章中介绍了以下内容:

Using GSON to convert Java objects to JSON strings and back   Storing and retrieving JSON strings using shared preferences   Storing and retrieving JSON strings from Android internal file storage  

在涵盖这些主题的过程中,我们回答了以下关键问题:(1)使用共享偏好作为简单应用和游戏的数据存储有哪些限制?(2)除了简单的键/值对之外,有没有一个官方的词不使用共享首选项?(3)使用此方案可以存储的数据量是否有最大限制?(4)我是否应该探索其他数据存储选项?(Android 内部存储和外部存储是什么意思?(6)我应该使用共享首选项还是内部文件?(7)我应该使用外部存储卡上的文件吗?(8)我何时需要使用 SQL 存储?以及(8)我能否以这样一种方式编写我的应用,以便为以后的版本迁移到 SQL 存储?

我们首先快速回顾一下 Android 中用于管理持久状态的数据存储选项。

Android 中的数据存储选项

Android 中存储数据的方式有五种:(1)共享首选项,(2)内部文件,(3)外部文件,(4) SQLite,(5)云端网络存储。让我们逐一回顾一下。

共享偏好设置是应用和设备内部的。该数据不可用于其他应用。用户不能通过安装到 USB 端口上来直接操作这些数据。删除应用时,数据会自动删除。共享首选项是结构化的键/值对数据,并遵循 Android 为使用存储的数据作为首选项而强加的一些其他语义。共享首选项以 XML 文件的形式维护。

内部文件是非结构化的私有文件,应用可以在其中创建和存储数据。像共享首选项一样,内部文件遵循应用的生命周期。卸载应用时,由应用创建的内部文件也会被删除。

外部文件存储在 SD 卡上。这些成为公共文件,其他应用包括用户可以在你的应用环境之外看到。外部文件通常用于主要由用户创建并且在创建它的应用之外有意义的数据,例如音频文件、视频文件、word 文档等。当数据量很大(比如 10MB 或更大)时,外部文件也是合适的。

SQLite 是一个关系数据库。对于结构化数据,比如您计划使用 JSON 的数据,SQLite 是首选。然而,在 Java 对象和关系数据库之间要做大量的工作。即使在使用精心制作的 o/r 映射工具或库的最简单的情况下,仍然有大量的工作要做。然而,SQLite 是为后续版本调整和重构应用的绝佳选择。这种重构将使您的应用响应更快,使用更少的能量。数据库也是应用私有的,外部应用无法使用,除非您将数据库包装在内容提供者框架中。(有关内容供应器的详细报道,请参考我们的配套书籍 Pro Android 4。)

网络存储允许您的应用选择将持久数据保存在云中。然而,网络存储并不是很多应用的完整选项,因为你可能需要应用在断开互联网连接时工作。可能有更多机会使用 parse.com 或类似的 BaaS(后端即服务)平台来实现混合方法,从而将一些数据存储在云中,并在需要时与设备同步。

我们的研究使我们建议在使用 GSON 将对象转换成 JSON 后,使用共享参数或内部文件作为对象的存储机制。这些存储选项有两个优点。第一,它们是私有的;第二,当应用被删除时,它们也被删除。它们的缺点是尺寸有限;如果文件变得太大(几十兆字节或者有图像或视频),一旦你的应用启动,你可以在后续版本中迁移到 SQLite。

使用 JSON 实现持久性的一般方法

JSON 是一种字符串格式,将 JavaScript 对象表示为字符串。您可以将一组 JavaScript 对象转换成 JSON 字符串。然后,这些字符串可以通过网络传输,并作为 JavaScript 对象读回。由于 Java 对象类似于 JavaScript 对象,所以可以使用相同的 JSON 字符串格式将 Java 对象转换为字符串。产生的 JSON 字符串可以转换回 Java 对象。

因为 JSON 已经成为跨网络传输对象的主要格式,所以围绕 JSON 出现了许多工具来简化这个过程。在我们的例子中,我们使用 JSON 字符串,不是为了传输而是为了将它们持久化到磁盘上并读回它们。在这种方法中,我们使用 Google 工具 GSON 将 Java 对象转换成 JSON 字符串,然后再转换回来。如上所述,JSON 字符串可以存储在内部文件或共享的首选项中。

与 GSON 合作

GSON 是一个 Java 库(一个单独的 jar 文件),可以用来将 Java 对象转换成 JSON 或者相反。(您可以在本章末尾的参考资料中看到主页、用户指南和 API 的链接。)让我们在将 Java 对象序列化和反序列化为 JSON 的同时,快速看一下 GSON 的特性和局限性。

GSON 的特点

大多数类型的 Java 对象都可以使用 GSON 转换成 JSON 字符串。使用 GSON,您可以在嵌套结构中的对象中包含对象。如果您镜像 Java 对象并关注它们的存储结构,那么您可以将大多数(如果不是全部)对象序列化为 JSON 字符串。因此,您希望您的 Java 对象主要表示数据,而不是可能干扰用 GSON 序列化它们的成熟行为。

只要您的成员集合使用 Java 泛型,GSON 就可以成功地序列化您的集合。您将很快看到一个这样的例子。使用 Java 泛型时,需要指定集合的类型。这有助于 GSON 在反序列化时实例化正确类型的对象。(您可以参考 GSON 用户指南了解更多功能和限制。)

GSON 还将转义引号字符和任何其他对 JSON 特殊的字符。默认情况下,GSON 对 HTML 和 XML 字符进行转义。在我们的研究中,我们发现这种行为非常令人满意,因为它允许您序列化任何 Java 对象——即使它的成员包含具有任何类型字符的任意字符串。

将 GSON Jar 添加到应用中

要开始在 Android 代码中使用 GSON,您需要将 GSON jar 文件添加到您的 Eclipse/ ADT 项目中。下面是添加 GSON jar 文件所需的步骤:

Go to GSON home page: http://code.google.com/p/google-gson/ .   download the GSON jar file.   Create a subdirectory under the root of your Eclipse project called "libs” (a sibling of src).   Copy the GSON jar to that lib directory.   Go go “project properties.”   Go to the “Java Build Path” option in project properties.   Go to “Library” tab.   Add the GSON jar as an external jar.   Go to the “order/export” tab.   Choose the GSON jar to be exported as well.  

这些步骤将使 GSON jar 可用于编译您的代码,也可用于将它与 GSON jar 一起部署到仿真器或设备上。添加外部 jar 所需的步骤可能会随着您所使用的 Eclipse/ADT 版本的不同而略有不同。

您也许能够链接外部 GSON jar,而不需要复制到您的 APK 项目,如这里所示。在这种情况下,您将能够成功地编译和链接。但是,当您在设备或模拟器上运行应用时,将会出现“未找到运行时类”异常。因此,按照上述步骤将 GSON jar 添加到您的 Eclipse/ADT 项目中。

为 GSON 规划 Java 对象

您可以将您的持久性结构建模为一组相互连接的 Java 对象。然后,您可以使用 GSON 将根 Java 对象转换成 JSON。我们向您展示了几个具有代表性的 Java 对象;使用这些例子,您可以用类似的方式对您的存储对象建模。

我们从根对象开始,我们称它为MainObject。清单 4-1 是这个根MainObject.的源代码

清单 4-1。用于存储应用状态的根对象的示例结构

public class MainObject

{

public int intValue = 5;

public String stringValue = "st<ri>\"ng\"Value<node1>test</node2>";

public String[] stringArray;

public ArrayList<ChildObject> childList = new ArrayList<ChildObject>();

...

}

注意,我们已经在清单 4-1 的MainObject中模拟了许多存储类型。我们有普通的整数、字符串、数组和嵌入的子对象集合。在字符串值中,我们甚至存储了嵌套的 XML 节点。这些嵌套的 XML 节点将允许我们测试 GSON 和 Android 共享偏好的转义特性。

在清单 4-1 中,还要注意我们如何使用一个通用的ArrayList集合来保存子对象的集合。每个子对象都有自己的内部结构。子对象的类定义如清单 4-2 所示。

清单 4-2。存储为 JSON 的子对象的示例结构

public class ChildObject {

public String name;

public int age;

public boolean likesVeggies = false;

public ChildObject(String inName, int inAge)   {

name = inName;

age = inAge;

}

}

尽管我们已经将ChildObject的成员变量表示为公共变量,但 GSON 确实允许它们作为私有成员,只要您提供与它们的名称匹配的 get/set 方法。

尽管清单 4-1 和 4-2 中的这些对象主要是为了建模存储需求,但是您可以向这些类添加基本行为,并提供以结构化方式初始化和存储数据的好方法。你可以在清单 4-3 中看到这一点,我们扩展了MainObject来添加一些行为。

清单 4-3。显示行为的对象

public class MainObject

{

public int intValue = 5;

public String strinValue = "st<ri>\"ng\"Value<node1>test</node2>";

public String[] stringArray;

public ArrayList<ChildObject> childList = new ArrayList<ChildObject>();

public void addChild(ChildObject co)   {

childList.add(co);

}

public void populateStringArray()   {

stringArray = new String[2];

stringArray[0] = "first";

stringArray[1] = "second";

}

//This method is used to create a sample MainObject

public static MainObject createTestMainObject()   {

MainObject mo = new MainObject();

mo.populateStringArray();

mo.addChild(new ChildObject("Eve",30));

mo.addChild(new ChildObject("Adam",28));

return mo;

}

//this method is used to verify two MainObject

//instances are the same.

public static String checkTestMainObject(MainObject mo)   {

MainObject moCopy = createTestMainObject();

if (!(mo.strinValue.equals(moCopy.strinValue)))

{

return "String values don't match:" + mo.strinValue;

}

if (mo.childList.size() != moCopy.childList.size())

{

return "array list size doesn't match";

}

//get first child

ChildObject firstChild = mo.childList.get(0);

ChildObject firstChildCopy = moCopy.childList.get(0);

if (!firstChild.name.equals(firstChildCopy.name))

{

return "first child name doesnt match";

}

return "everything matches";

}

}

将 Java 对象转换成 JSON

既然我们已经为我们的持久性需求定义了对象结构,让我们看看如何获取一个MainObject的实例并将其转换成 JSON。我们还将获取生成的 JSON 字符串,并将其转换回实例MainObject。然后我们将比较这个生成的MainObject实例,看它是否与原型MainObject匹配。清单 4-4 显示了这样做的代码。

清单 4-4。使用 GSON 序列化和反序列化 Java 对象

public void testJSON()

{

MainObject mo = MainObject.createTestMainObject();

Gson gson = new Gson();

//Convert to string

String jsonString = gson.toJson(mo);

//Convert it back to object

MainObject mo1 = gson.fromJson(jsonString, MainObject.class);

String compareResult = MainObject.checkTestMainObject(mo1);

Log.i(“sometag”,compareResult);

}

非常简单。

现在剩下的就是如何在一个持久化的地方存储和检索这个结果 JSON 字符串。我们有两个选项:共享首选项和内部文件存储。我们首先讨论共享偏好。

使用 JSON 持久性的共享首选项

在 Android 中,共享偏好用于满足两个主要需求。Android SDK 使用共享首选项机制来创建为应用自动创建首选项屏幕所必需的 UI。Android SDK 还直接公开了共享的首选项,没有 UI 组件。在后一种模式中,共享首选项只是键/值对的存储/检索机制。Android SDK 提供了许多类和方法来直接处理这些键/值对。(你可以在我们的同伴 Pro Android 系列书籍中阅读更多关于共享偏好的内容。)

共享首选项的核心是一个 XML 文件,它以持久的方式保存键/值对。XML 文件是实现细节;Android 将来可能会选择不同的表现形式,如果它决定这样做的话。您可以拥有任意数量的共享首选项(XML 文件)。每个共享首选项可以有任意多的键/值对。

这些共享的首选项 XML 文件是您的应用专用的。卸载应用时,它们会被移除。要获取对共享首选项的引用,您需要一个上下文对象。Activity是 Android 中上下文对象的一个例子。一旦有了上下文对象,就可以用文件名调用方法getSharedPreferences()来访问共享的首选项对象。然后,您可以使用该对象来保存和检索键/值对。

如果您可以访问一个Activity,您可以只调用getPreferences()来代替。这个方法只是调用前面的getShaerdPreferences(),用活动名作为 XML 文件的名称。

获取对应用上下文的访问

应用中的每个活动都可以有自己的共享首选项文件。但是我们感兴趣的是整个应用的持久性需求,而不仅仅是特定的活动。所以我们不能在Activity类上使用getPreferences()方法。据您所知,您甚至可能在您的应用中没有活动。

因此,我们需要一个适用于整个应用级别的上下文。为此,您需要覆盖Application类并创建应用的一个实例。清单 4-5 显示了这一点。

清单 4-5。收集应用上下文

public class MyApplication extends Application

{

public final static String tag="MyApplication";

public static Context s_applicationContext = null;

@Override

public void onConfigurationChanged(Configuration newConfig) {

super.onConfigurationChanged(newConfig);

Log.d(tag,"configuration changed");

}

@Override

public void onCreate() {

super.onCreate();

s_applicationContext = getApplicationContext();

Log.d(tag,"oncreate");

}

@Override

public void onLowMemory() {

super.onLowMemory();

Log.d(tag,"onLowMemory");

}

@Override

public void onTerminate() {

super.onTerminate();

Log.d(tag,"onTerminate");

}

}

清单 4-5 中的关键行用粗体突出显示。在这个清单中,我们获取应用上下文并将其存储在一个公共静态变量中,以便应用上下文在应用中全局可用。

一旦你有了你的应用MyApplication,如清单 4-5 所示,你需要调整你的清单文件来注册MyApplication,如清单 4-6 所示。

清单 4-6。在清单文件中注册应用对象

<application android:name="com.androidbook.testjson.MyApplication"

...

</application>

清单 4-6 中的声明将导致调用MyApplicationonCreate()方法,如清单 4-5 所示

使用共享偏好设置存储和恢复字符串

有了MyApplication中的静态全局应用上下文,您可以使用清单 4-7 中的代码来保存和恢复共享首选项中的字符串。

清单 4-7。使用共享偏好设置存储和恢复字符串

//Use an XML file called myprefs.xml to represent shared preferences

//for this example.

private SharedPreferences getSharedPreferences()

{

SharedPreferences sp

= MyApplication

.s_applicationContext

.getSharedPreferences("myprefs", Context.MODE_PRIVATE);

return sp;

}

public void testEscapeCharactersInPreferences()

{

//Use a string that is a bit more complicated

//to see how escape characters work

String testString = "<node1>blabhhh</node1>";

//get shared preferences

SharedPreferences sp = getSharedPreferences();

//Prepare the shared preferences for save

SharedPreferences.Editor spe = sp.edit();

//add a key/value pair

spe.putString("test", testString);

//Commit the changes to persistence

spe.commit();

//Retrieve what is stored

String savedString = sp.getString("test", null);

if (savedString == null)

{

Log.d(tag,"no saved string");

return;

}

//Compare the two strings

Log.d(tag,savedString);

if (testString.equals(savedString))

{

Log.d(tag,"Saved the string properly. Match");

return;

}

//they dont match

Log.d(tag,"They don't match");

return;

}

让我们回顾一下清单 4-7 中的代码。我们首先使用静态全局上下文来使用myprefs.xml作为共享首选项文件来测试字符串的存储。我们还指出了在私有模式下创建和维护myprefs.xml。其他可能的模式如清单 4-8 所示。

清单 4-8。共享偏好设置的模式位

MODE_PRIVATE (value of 0 and default)

MODE_WORLD_READABLE

MODE_WORLD_WRITEABLE

MODE_MULTI_PROCESS

在清单 4-7 所示的测试方法中,我们使用了一个包含 XML 字符的字符串,知道共享的首选项存储在 XML 节点中。我们想看看共享偏好机制是否足够聪明来避开这些字符。

此时,您可能想知道共享首选项文件myprefs.xml是何时创建的,因为我们没有明确要求创建任何文件。文档表明,当我们试图使用清单 4-9 中所示的代码段保存第一个键/值对时,创建了共享首选项的底层 XML 文件。(这个清单摘自前面的清单 4-7。)

清单 4-9。使用编辑器保存和提交首选项

SharedPreferences.Editor spe = esp.edit();

spe.put...();

spe.commit();

从清单 4-9 中的代码,您可以看到SharedPreferences有点奇怪。我们不直接使用SharedPreferences对象来保存值。相反,我们使用它的内部类SharedPreferences.Editor来完成保存。这只是一种你必须习惯的模式。在项目的生命周期中,您可以选择向一个共享的首选项添加多个键/值对。最后,一个commit()将把它写到持久性存储中,这将保存对文件的多次写入。

此时一个奇怪的问题是,这个myprefs.xml存储在设备的什么地方?这个文件存储在设备的路径中,如清单 4-10 所示。

清单 4-10。共享首选项文件路径

/data/data/YOUR_PACKAGE_NAME/shared_prefs/myprefs.xml

在 eclipse/ADT 中,您使用文件管理器工具来查看模拟器或设备上的文件,并将myprefs.xml文件拖到本地驱动器并查看它。清单 4-11 显示了清单 4-7 中的代码创建的myprefs.xml文件。看看 Android 首选项如何对字符串值中的 XML 字符进行转义。

清单 4-11。共享首选项 XML 文件的内容

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>

<map>

<string name="test">&lt;node1&gt;blabhhh&lt;/ndoe1&gt;</string>

</map>

在共享偏好设置中使用 GSON 保存/恢复对象

现在,您已经拥有了从共享首选项中保存和恢复 Java 对象所需的所有信息。清单 4-12 显示了这是如何做到的。在这个例子中,我们创建了MainObject并使用 GSON 将其转换成 JSON。然后,我们使用共享首选项来存储它。我们像前面一样使用文件名myprefs.xml,我们使用MainObject JSON 字符串的键作为json。一旦我们成功存储了对象,我们就检索 JSON 字符串并将其转换回MainObject。然后,我们将它与原始对象进行比较,看它们是否匹配。

清单 4-12 .从共享首选项中存储/检索对象

public void storeJSON()

{

MainObject mo = MainObject.createTestMainObject();

//

Gson gson = new Gson();

String jsonString = gson.toJson(mo);

Log.i(tag, jsonString);

MainObject mo1 = gson.fromJson(jsonString, MainObject.class);

Log.i(tag, jsonString);

SharedPreferences sp = getSharedPreferences();

SharedPreferences.Editor spe = sp.edit();

spe.putString("json", jsonString);

spe.commit();

}

public void retrieveJSON()

{

SharedPreferences sp = getSharedPreferences();

String jsonString = sp.getString("json", null);

if (jsonString == null)

{

Log.i(tag,"Not able to read the preference");

return;

}

Gson gson = new Gson();

MainObject mo = gson.fromJson(jsonString, MainObject.class);

Log.i(tag,"Object successfully retrieved");

String compareResult = MainObject.checkTestMainObject(mo);

if (compareResult != null)

{

//there is an error

Log.i(tag,compareResult);

return;

}

//compareReesult is null

Log.i(tag,"Retrieved object matches");

return;

}

您可能想看看在执行清单 4-12 中的代码后,myprefs.xml是什么样子,因为这将让您有机会看到 GSON 是如何对字符串进行转义的。下面是清单 4-13 中的文件。

清单 4-13。演示共享首选项中的转义字符

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>

<map>

<string name="test">&lt;node1&gt;blabhhh&lt;/ndoe1&gt;</string>

<string name="json">{&quot;childList&quot;:[{&quot;name&quot;:&quot;Adam&quot;,&quot;likesVeggies&quot;:false

&quot;age&quot;:30},{&quot;name&quot;:&quot;Eve&quot;,&quot;likesVeggies&quot;:false

&quot;age&quot;:28}],&quot;stringArray&quot;:[&quot;first&quot;,&quot;second&quot;]

&quot;strinValue&quot;:&quot;st\u003cri\u003e\&quot;ng\&quot;Value\u003cnode1\u003etest\u003c/node2\u003e

&quot;,&quot;intValue&quot;:5}</string>

</map>

请注意清单 4-13 中 GSON 和 Android 首选项之间的转义是如何工作的。两者都有转义序列。所以不用担心包含特殊字符的对象中的字符串值。它们被 GSON 和 Android 偏好自动处理得很好。

作为讨论共享首选项的最后一点,我们提供了一个问题的答案,共享首选项中可以存储的数据量有限制吗?答案是它相当大,至少理论上是如此。实质上,设备上可用的内部存储量提供了上限。重要的是,Android 没有对共享偏好文件的大小施加任何任意限制。但是,由于所有应用共享同一个存储设备,所以您需要谨慎使用多少存储设备。可能任何小于 10MB 的都是合理的。

为 JSON 使用内部存储

最终,共享的首选项作为键/值对存储在 XML 文件中。Android 以一种特殊的方式处理共享偏好,正如前面共享偏好一节所展示的。例如,共享首选项是文件这一事实不会直接暴露给程序员。此外,键/值对作为节点/值对存储在 XML 文档中。相反,您可能希望完全控制存储 JSON 的文件。在这种情况下,您可以使用 Android 提供的内部存储选项。这些内部文件与共享首选项 XML 文件非常相似,也是专门为您的应用存储在设备上的,默认情况下是私有的。

从内部存储器存储和检索

在内部文件中存储和检索 JSON 非常简单。Android SDK 有一个打开和关闭内部文件的 API。Android SDK 还决定这些文件驻留在哪里。作为一名程序员,你可以控制文件名。您还可以控制何时读取/写入它们。您可以使用标准的 Java I/O 库从文件流中读取和写入文件流。

清单 4-14 和 4-15 中的代码段演示了如何完成以下工作:

Open an internal file for writing   Convert objects to JSON and write to the file   Open the same internal file for reading   Read the JSON string from the file   Convert the JSON string to objects   Compare the source and target objects to ensure they are same  

清单 4-14 .从内部文件存储中存储/检索对象

public void saveJSONToPrivateStorage()

{

String json = createJSON();

saveToInternalFile(json);

String retrievedString = this.readFromInternalFile();

//Create the object from retrievedString

Gson gson = new Gson();

MainObject mo = gson.fromJson(retrievedString, MainObject.class);

//makesure it is the same object

MainObject srcObject = MainObject.createTestMainObject();

String compareResult = mo.checkTestMainObject(srcObject);

Log.i(tag,compareResult);

}

private String createJSON()

{

MainObject mo = MainObject.createTestMainObject();

Gson gson = new Gson();

String jsonString = gson.toJson(mo);

return jsonString;

}

private String readFromInternalFile()

{

FileInputStream fis = null;

try {

Context appContext = MyApplication.s_applicationContext;

fis = appContext.openFileInput("``datastore-json.txtT2】

String jsonString = readStreamAsString(fis);

return jsonString;

}

catch(IOException x)

{

Log.d(tag,"Cannot create or write to file");

return null;

}

finally

{

closeStreamSilently(fis);

}

}

private void saveToInternalFile(String ins)

{

FileOutputStream fos = null;

try {

Context appContext = MyApplication.s_applicationContext;

fos = appContext.openFileOutput("datastore-json.txt"

,Context.MODE_PRIVATE);

fos.write(ins.getBytes());

}

catch(IOException x)

{

Log.d(tag,"Cannot create or write to file");

}

finally

{

closeStreamSilently(fos);

}

}

一点都不复杂。清单 4-14 中的代码使用了几个文件工具方法。你可以根据自己的喜好来设计这些方法。但是如果你想看看它们的快速实现,清单 4-15 中有这些方法。

清单 4-15。支持文件实用程序方法

private void copy(InputStream reader, OutputStream writer)

throws IOException

{

byte byteArray[] = new byte[4092];

while(true)   {

int numOfBytesRead = reader.read(byteArray,0,4092);

if (numOfBytesRead == -1)      {

break;

}

// else

writer.write(byteArray,0,numOfBytesRead);

}

return;

}

private String readStreamAsString(InputStream is)

throws FileNotFoundException, IOException

{

ByteArrayOutputStream baos = null;

try    {

baos = new ByteArrayOutputStream();

copy(is,baos);

return baos.toString();

}

finally    {

if (baos != null)

closeStreamSilently(baos);

}

}

private void closeStreamSilently(OutputStream os)

{

if (os == null) return;

//os is not null

try {os.close();} catch(IOException x)    {

throw new RuntimeException(

"This shouldn't happen. exception closing a file",x);

}

}

private void closeStreamSilently(InputStream os)

{

if (os == null) return;

//os is not null

try {os.close();} catch(IOException x)    {

throw new RuntimeException(

"This shouldn't happen. exception closing a file",x);

}

}

在清单 4-14 中,我们使用了文件名datastore-json.txt。您可以在您的设备上找到这个文件,位置如清单 4-16 所示。

清单 4-16。Android 内部文件的文件位置

data/data/<your-pkg-name>/files/datastore-json.txt

一旦执行了清单 4-14 中的代码,就可以使用 Eclipse/ ADT 中的文件管理器来查看这个文件的内容。这个文件的内容将类似于清单 4-17 所示的文本。

清单 4-17。内部存储文件中的 JSON 字符串

{"childList":[{"name":"Adam","likesVeggies":false,"age":30}

{"name":"Eve","likesVeggies":false,"age":28}],"stringArray":["first","second"]

"strinValue":"st\u003cri\u003e\"ng\"Value\u003cnode1\u003etest\u003c/node2\u003e"

"intValue":5}

您可以使用多个文件来存储多个 Java 根对象,以细分存储的粒度。这将有助于某种程度的优化。

在外部存储器上存储 JSON

Android SDK 提供 API 来控制外部 SD 卡上的目录和文件。正如我们在前面的“数据存储选项”一节中指出的,这些外部文件是公共的。如果您选择将 JSON 字符串存储在这些外部文件中,那么您可以遵循与上一节中描述的内部文件相似的模式。然而,在大多数情况下,外部存储选项有很好的理由。

因为通常您表示为 JSON 的数据是特定于您的应用的,所以将其作为外部存储是没有意义的,外部存储通常用于音乐文件、视频文件或其他应用可以理解的通用格式的文件。因为诸如 SD 卡的外部存储器可以处于各种状态(可用、未安装、已满等。),当数据足够小,可以通过更简单的内部文件管理时,很难为简单的应用编写这种卡。

因此,我们不认为应用状态是在外部存储上维护的。如果应用需要音乐和照片,那么混合方法可能是有意义的,这些可以放在外部存储上,而您将核心状态数据保存在 JSON 和内部文件中。如果您选择使用外部存储,请参考 Android SDK 文档,了解一些正确管理外部存储的 API。

将 SQLite 用于结构化存储

没有什么像 GSON 一样简单和甜蜜的事情可以不存在。如果每次发生变化时都要编写文件,那么这种 JSON 方法的成本会很高。当然,存在一个平衡点,在这个平衡点上,您希望将代码迁移到 SQLite 并使用粒度更新。

如果是这样的话,您可以构建您的代码,这样您就可以用 SQLite 之类的替代品来交换持久层,而不会对您的代码进行重大修改。要做到这一点,把你的应用分成一个持久性服务层和其余部分。您将需要足够的训练来使用服务层作为一组无状态的服务,您可以在以后使用不同的实现。记住接口和服务接口防火墙。在参考资料中,我们包含了一篇文章的链接,这篇文章是作者之一在 2006 年写的,内容是关于极限原型的。您可以借用相同的原则来构建一个可以交换的服务层。

参考

我们发现以下链接对本章的研究很有帮助。

下载本章专用的测试项目:www.androidbook.com/expertandroid/projects.ZIP 文件的名称是ExpertAndroid_ch04_TestGSON.zip

摘要

GSON 有助于将您的应用快速部署到市场。这是测试市场实力的好方法。它也有助于快速发布大量简单的应用,只需做最少的持久性工作。为了实现这个目标,我们在本章中介绍了如何使用 GSON 将对象存储在共享的首选项或内部文件中。在这个过程中,我们还讨论了 Android 的其他数据存储选项,看它们是否适合存储 JSON。

复习问题

以下一组问题应该可以巩固您在本章中学到的内容:

What are the five different storage options for Android applications?   How would you use JSON for persisting Android application state?   What are the pros and cons of using JSON for persistence?   What is GSON?   Can you save nested objects using GSON?   Can you save nested collections of objects using GSON?   How are characters in strings escaped in GSON?   How do you add external jar files like the GSON jar to the Android APK file?   What are shared preferences?   What is the difference between getSharedPreferences and getPreferences?   How do you get a context independent of an activity?   Where are shared preference files stored?   How do you save JSON to a shared preference file?   What does the saved preference file look like?   How do you read JSON from shared preferences back as Java objects?   How do escape characters work in Android preferences?   What is internal storage?   How do you save to internal storage?   How do you restore objects from internal storage?   Should you use external storage like the SD card?   What are the useful references while working with this approach?   How do you code in such a way that you can migrate in the future to SQLite?