目录
需求
预备知识
熟悉 ActionScript 3 和它面向对象的功能,以及熟悉 Flash 应用程序开发。
用户水平
中级
需要的产品
- Flash Professional(下载试用版)
使 Apple iOS 成为如此优秀的移动平台的一个重要原因是它的用户界面。无论您在本地开发还是使用 Adobe Flash Professional CS5.5,iOS 设备的用户都希望您(应用程序开发人员)让您的应用程序适合现有的 iOS 体验。该体验的一个重要部分是可中断性:您的 Adobe AIR 应用程序需要频繁并自动地保存它的状态,以便它可在用户希望时(以及根据苹果公司在其 iPhone 人机界面指南中的指定)自动切换。
本文介绍一些在 iOS 设备上保存应用程序状态的策略。其中涵盖了 iOS 设备的基本功能,以及有助于提供无缝用户体验的应用程序设计技术。我还将提供代码来演示保存状态的不同技术。
考虑状态问题
苹果公司iOS 一次仅运行一个用户应用程序,以保持设备的高响应能力。只要用户启动另一个应用程序,或者电话呼叫等外部事件发生,您的应用程序可能被终止。这可能影响用户的情绪:想象如果您正在编写文本消息或电子邮件,这时拨入了一个电话,而您丢失了所做的工作!出于此原因,应用程序定期并自动地保存其状态至关重要。
我在PushButton Labs 的同事和我在去年使用Flash Professional CS5 为iPhone 开发了一个名为 Trading Stuff in Outer Space 的游戏(参见图 1)。它是一个简单的太空贸易游戏,我们用了 8 天时间开发了它,以便探索该技术。项目的一个目标是提供一种与任何其他 iPhone 游戏不同的体验。结果,我们必须解决如何实现状态保存。
图 1. Trading Stuff in Outer Space 游戏屏幕
在iOS 设备上保存状态
我们构想了应用程序在被中断时丢失数据的场景。当然,内置的iOS 应用程序不会出现这种情况,当您在中断通话之后返回到文本消息时,您可以从离开的地方接着开始工作。这是通过在操作系统要求应用程序退出时保存状态的简单措施来完成的。在Objective-C 领域中,您将必须自行实现此功能(参见 iOS 应用程序编程指南中的合适部分),并且在您使用 Flash 开发时没什么不同。
退出时保存
在 Flash 中您如何知道合适保存?因为用于 iOS 的 AIR 应用程序与用于 Adobe AIR 的 AIR 应用程序使用了相同的 API,您可以监听 NativeApplication.EXITING 事件来知道何时保存状态:
package { import flash.desktop.NativeApplication; import flash.display.Sprite; import flash.events.Event; public class Save1 extends Sprite { public function Save1() { // Listen for exiting event. NativeApplication.nativeApplication.addEventListener(Event.EXITING, onExit); // Load data. load(); } private function onExit(e:Event):void { trace("Save here."); } private function load():void { trace("Load here."); } } }
在发生重要事件时保存
但是,您还应该在发生重要事件或经过一定时间间隔之后保存。对于 Trading Stuff in Outer Space,重要的事件包括当游戏者交易时(因为这是一项需要太多思考并对 gameplay 具有重要影响的操作),当游戏者到达一颗行星时,以及当游戏者开始或结束游戏时。对于时间间隔,我们选择 30-60 秒的时间段,以便用户从不会丢失超过 1 分钟的娱乐时间。我们选择该阈值是因为,我们感觉它已经足够高到实现反应迅速的游戏,但又足够低到在某些事情导致退出时保存失败时激怒用户。
不幸的是,EXITING 事件不是 100% 的可靠。它并不总是会触发——有时是因为操作系统中的时间限制,有时是因为其他原因。您的应用程序可能崩溃(尽管不太可能发生),在这种情况下正常的退出行为不会发生。所以我们所做的是在用户执行每项主要操作之后保存,以及大约 1 分钟保存一次。这样,即使用户试图在应用程序未保存状态时就退出,他们也不太可能丢失超过几十秒的工作:
package { import flash.desktop.NativeApplication; import flash.display.Sprite; import flash.events.Event; import flash.utils.setInterval; public class Save2 extends Sprite { public function Save2() { // Listen for exiting event. NativeApplication.nativeApplication.addEventListener(Event.EXITING, onExit); // Also save every 30 seconds. setInterval(save, 30*1000); // Load data. load(); } private function onExit(e:Event):void { save(); } private function save():void { trace("Save here."); } private function load():void { trace("Load here."); } } }
通用性
iOS 设备上的 AIR 应用程序面临的问题与浏览器中的 SWF 内容所面对的问题没什么不同。用户的浏览器可能崩溃,或者用户可能(偶尔或特意)离开页面。甚至桌面用户可能希望他们的应用程序始终具有状态。您可以使用相同的保存代码来增强 Web、设备和桌面上的用户体验。
保存方法
您对于 iOS 设备上的 AIR 应用程序,您可以通过两种主要方式保存状态。一种是使用 LSO(本地 SharedObject)。众所周知,SharedObject.getLocal()(参见文档)可用于在本地存储数据。这在许多方面都很方便,尤其是您可以使用 AMF 存储对象图:
private function save():void { // Get the shared object. var so:SharedObject = SharedObject.getLocal("myApp"); // Update the age variable. so.data['age'] = int(so.data['age']) + 1; // And flush our changes. so.flush(); // Also, indicate the value for debugging. trace("Saved generation " + so.data['age']); } private function load():void { // Get the shared object. var so:SharedObject = SharedObject.getLocal("myApp"); // And indicate the value for debugging. trace("Loaded generation " + so.data['age']); }
最后,在此游戏应用程序中,我们没有直接序列化对象。取决于您的应用程序,使用 SharedObject 可能非常适合。对于 Trading Stuff,我有大量相互关联的数据要存储,我还希望将游戏状态划分到频繁更改和不频繁更改的元素,所以使用 LSO 不太适合。下面将详细介绍。
使用 File 对象保存
另一种方法是直接使用 File 对象。您可以异步写入来避免断断续续的帧率。如果游戏状态的不同部分以不同的频率更改,您可以将它们存储在独立的文件中,以便仅保存实际的更改。您必须自行序列化,但这并没有听起来那么糟。(您甚至可以使用 readObject() 和 writeObject() 一次存储整个对象,但这在某些情况下可能导致问题。)
public var age:int = 0; /** * Get a FileStream for reading or writing the save file. * @param write If true, we will write to the file. If false, we will read. * @param sync If true, we do synchronous writes. If false, asynchronous. * @return A FileStream instance we can read or write with. Don't forget to close it! */ private function getSaveStream(write:Boolean, sync:Boolean = true):FileStream { // The data file lives in the app storage directory, per iPhone guidelines. var f:File = File.applicationStorageDirectory.resolvePath("myApp.dat"); if(f.exists == false) return null; // Try creating and opening the stream. var fs:FileStream = new FileStream(); try { // If we are writing asynchronously, openAsync. if(write && !sync) fs.openAsync(f, FileMode.WRITE); else { // For synchronous write, or all reads, open synchronously. fs.open(f, write ? FileMode.WRITE : FileMode.READ); } } catch(e:Error) { // On error, simply return null. return null; } return fs; } private function load():void { // Get the stream and read from it. var fs:FileStream = getSaveStream(false); if(fs) { try { age = fs.readInt(); fs.close(); } catch(e:Error) { trace("Couldn't load due to error: " + e.toString()); } } trace("Loaded age = " + age); } private function save():void { // Update age. age++; // Get stream and write to it – asynchronously, to avoid hitching. var fs:FileStream = getSaveStream(true, false); fs.writeInt(age); fs.close(); trace("Saved age = " + age); }
这两种方法应该都容易理解。如果您执行过 Flash 或 AIR 开发,您一定使用过 SharedObject 或 File 。如果没有,您可能希望参阅它们的文档了解详细信息和示例。
您也可以使用 readObject() 和 writeObject() 存储或从 File 对象检索对象。此功能很强大,使您无需编写大量重复性的序列化代码。但是,如果您不太小心,它可能引起问题:例如,由于涉及到各种帮助器对象和复杂的关系,保存 DisplayObject 没有生效。甚至保存纯数据类也可能充满风险,因为它们可能拥有对您不希望虚拟化的其他对象的引用。
当然,保存整个应用程序的状态更加复杂。接下来将探讨此问题。
状态管理体系结构
我遇到的较大的问题是序列化我的所有应用程序数据。确定在何处写入数据很简单,但确定写入什么数据就有点复杂。因为我是程序员,所以我自然希望通过尽可能少的工作来完成此任务。
我尝试的第一件事是通过 SharedObject 中的 AMF 直接序列化我的 DisplayObject 层次结构的各部分和游戏状态。这最初很吸引我,因为我发现我可以将对应用程序的一些重要部分的引用抛入 SharedObject 并在这里完成。但是,这没有效:DisplayObject 拥有大量帮助器对象,它们具有不是 AMF 友好的陌生的结构限制。它还导致不受控制的序列化,其中我序列化的对象包含对其他对象的引用,最终涉及到了我的应用程序的大部分内容,我不希望浪费时间调试被序列化的陌生对象的问题。
所以,相反,我转而使用 File save() 方法。这意味着我必须编写一些代码来加载和保存构成应用程序状态的对象。这首先涉及到更多的代码,但因为它们简单明了(而且我没有计划修改应用程序必须经历的启动过程),所以与更加自动化的解决方案所需的更少代码相比,此方法最终可以更快地调试和编写。
以下代码片段展示了从用户当前的停留点来看,保存和加载是什么样的:
// Serialize current waypoint. gfs.writeInt(currentWaypointIndex); gfs.writeBoolean(wayPoint != null); if(wayPoint != null) { gfs.writeFloat(wayPoint.x); gfs.writeFloat(wayPoint.y); } // Load waypoint state. currentWaypointIndex = gfs.readInt(); if(gfs.readBoolean()) { wayPoint = new Point(); wayPoint.x = gfs.readFloat(); wayPoint.y = gfs.readFloat(); } else { wayPoint = null }
可以看到,序列化代码必须在恰当时刻分配对象;它必须检测变量是否是空的,并在文件中注明;而且它还必须使用正确的类型编写每个字段。您最初可能觉得这比较难懂,但有了一定实践经验之后,您将迅速认识到您在几乎每个任务中重用一小组通用的方言。
我还创建了一个单一实例来管理所有游戏状态。它跟踪游戏者的库存、宇宙中一些事务的位置、敌人的状态、任务进度,等等。从这个单一实例,我创建了方法来重置游戏状态、写入它和再次读取它。在一些情况下(比如库存或任务),状态归该单一实例所有。在其他情况下(比如游戏者和行星等游戏对象的位置),状态在其他地方由其他对象管理,但该单一实例可获取它用于保存。以此为基础,可以轻松实现各种保存策略,如前面所述。
延伸阅读
保存状态是让内容顺利地适合iOS 体验的一个重要部分。因为一次只有一个应用程序可在iOS 设备上运行,所以不仅存储用户的数据,而且还存储应用程序UI 状态很重要。这一细节并不复杂,但很重要。我希望本文有助于您理解在自己的应用程序中实现此重要功能所涉及的重要知识。
保存状态是一个宽泛的主题,没有一体适用的解决方案。关于众多不同的序列化选项的概述可在Wikipedia 上的 Serialization 条目中找到。Flash 提供了对 XML(参见 Colin Moock 的图书 Essential ActionScript 3 ,查阅关于 XML 和 E4X 的优秀章节)和 AMF 的原生支持,它们都是满足您的序列化需要的重要要素。还有 as3corelib 等库添加了对 JSON 和其他序列化格式的支持。当然,当您在各次运行之间保存应用程序状态时,最重要的是您选择的技术简单、易于使用且可靠。
现在,您可能希望获取本文中包含的示例代码并尝试将它集成到您自己的内容中。在许多地方需要做决定,比如使用本地 SharedObject 还是 File 对象,频繁保存还是不频繁地保存。请确保在做出某个决定之前运行了一些测试,精明的决策始终是最佳的,而且您需要的信息将取决于您特定的应用程序。有了解决方案之后,通过在程序执行期间频繁退出来测试它。可能无法准确恢复状态的所有部分,但只需极少的工作,您就会获得足够准确的状态,使用户能够在他们真实的 iOS 设备体验中接受您的应用程序。
请参阅此系列文章,帮助您了解关于使用 Flash Professional 进行 iOS 开发的更多信息:
本作品依据 Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License 授权。
评论