由于Adobe已经推出了AIR 2,我想这将是回顾我在过去几个月编写的所有 AIR 代码的一个绝佳时机,我会精选一些最佳代码段和概念在社区内分享。本文介绍了我用来提高 AIR 应用程序的性能、可用性及安全性,并使开发流程更加迅速简便的十大技巧:
- 保持低内存使用率
- 降低 CPU 使用率
- 存储敏感数据
- 编写"无头"应用程序
- 更新停靠与系统托盘图标
- 处理网络连接变更
- 创建"调试"和"测试"模式
- 在用户处于空闲状态时进行检测
- 管理辅助窗口
- 针对不同的操作系统进行编程
要求
Adobe AIR
必备知识
本文假设您对 Adobe AIR 具有基本了解,并且习惯使用 ActionScript。
1. 保持低内存使用率
最近,我编写了一套电子邮件通知应用程序,叫做 MailBrew。MailBrew 可监控 Gmail 和 IMAP 帐户,随后便会在新邮件到来时,发出低吼般的通知并释放警报。由于该应用程序旨在随时就您收到的新电子邮件进行通知,因此,显然它必须一直运行,而由于它始终运行,所以必须在内存使用方面非常谨慎(见图 1)
图 1. MailBrew 初始化时会消耗一些内存,并在每次检查邮件时消耗少量内存,但总会回归原来的内存量。
由于运行时会自动进行垃圾回收,作为 AIR 开发人员,您不必刻意管理内存,但是这并不意味着您不用为此担心。事实上,AIR 开发人员仍须对创建新的对象进行谨慎考虑,尤其保留参考内容,从而使它们不会遭到清除。下列秘诀将有助于您保持较低而稳定的 AIR 应用程序内存使用率:
- 务必移除事件侦听器
- 记得处理您的 XML 对象
- 编写您自己的dispose() 函数
- 采用 SQL 数据库
- 介绍您的应用程序
务必移除事件侦听器
您从前可能听说过这种做法,但是值得重复的是: 当您处理完引发事件的对象后,请移除所有事件侦听器,以便能够进行垃圾回收。
下面是一些来自我编写的一款名为 PluggableSearchCentral 的应用程序(简化)代码,阐释添加和移除事件侦听器的正确方法:
- private function onDownloadPlugin():void
- {
- var req:URLRequest = new URLRequest(someUrl);
- var loader:URLLoader = new URLLoader();
- loader.addEventListener(Event.COMPLETE, onRemotePluginLoaded);
- loader.addEventListener(IOErrorEvent.IO_ERROR, onRemotePluginIOError);
- loader.load(req);
- }
- private function onRemotePluginIOError(e:IOErrorEvent):void
- {
- var loader:URLLoader = e.target as URLLoader;
- loader.removeEventListener(Event.COMPLETE, onRemotePluginLoaded);
- loader.removeEventListener(IOErrorEvent.IO_ERROR, onRemotePluginIOError);
- this.showError("Load Error", "Unable to load plugin: " + e.target, "Unable to load plugin");
- }
- private function onRemotePluginLoaded(e:Event):void
- {
- var loader:URLLoader = e.target as URLLoader;
- loader.removeEventListener(Event.COMPLETE, onRemotePluginLoaded);
- loader.removeEventListener(IOErrorEvent.IO_ERROR, onRemotePluginIOError);
- this.parseZipFile(loader.data);
- }
- 另一种技巧是创建具备事件侦听器功能的变量,以便事件侦听器能够轻松地进行自我删除,就像这样:
- public function initialize(responder:DatabaseResponder):void
- {
- this.aConn = new SQLConnection();
- var listener:Function = function(e:SQLEvent):void
- {
- aConn.removeEventListener(SQLEvent.OPEN, listener);
- aConn.removeEventListener(SQLErrorEvent.ERROR, errorListener);
- var dbe:DatabaseEvent = new DatabaseEvent(DatabaseEvent.RESULT_EVENT);
- responder.dispatchEvent(dbe);
- };
- var errorListener:Function = function(ee:SQLErrorEvent):void
- {
- aConn.removeEventListener(SQLEvent.OPEN, listener);
- aConn.removeEventListener(SQLErrorEvent.ERROR, errorListener);
- dbFile.deleteFile();
- initialize(responder);
- };
- this.aConn.addEventListener(SQLEvent.OPEN, listener);
- this.aConn.addEventListener(SQLErrorEvent.ERROR, errorListener);
- this.aConn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, this.encryptionKey); }
记得处理您的 XML 对象
在 Flash Player 10.1 和 AIR 1.5.2 中,我们为名为 disposeXML() 的系统类增加了静态函数,从而确保取消对所有 XML 对象节点的引用,并且立即可供进行垃圾回收。如果您的应用程序可解析 XML 对象,请务必确保在您完成 XML 对象解析后调用此函数。如果您不使用System.disposeXML()函数,您的 XML 对象将可能会循环引用,从而将会阻止它进行垃圾回收。
下面是解析 Gmail 生成的 XML 源的一些代码的简化版本:
- var ul:URLLoader = e.target as URLLoader;
- var response:XML = new XML(ul.data);
- var unseenEmails:Vector.<EmailHeader> = new Vector.<EmailHeader>();
- for each (var email:XML in response.PURL::entry)
- {
- var emailHeader:EmailHeader = new EmailHeader();
- emailemailHeader.from = email.PURL::author.PURL::name;
- emailemailHeader.subject = email.PURL::title;
- emailemailHeader.url = email.PURL::link.@href;
- unseenEmails.push(emailHeader);
- }
- var unseenEvent:EmailEvent = new EmailEvent(EmailEvent.UNSEEN_EMAILS);
- unseenEvent.data = unseenEmails;
- this.dispatchEvent(unseenEvent);
- System.disposeXML(response);
编写您自己的 dispose() 函数
如果您要为具有多个类别的大型应用程序编写媒介,养成添加 "dispose" 函数的习惯是个好主意。事实上,您可能想要创建一个名为 IDisposable 的界面,来执行这一操作。dispose() 函数旨在确保对象不会具有阻止其进行垃圾回收的任何引用。至少,dispose() 函数应将所有类级别变量设置为空值。当具有采用 IDisposable的代码时,应在完成时调用其 dispose() 函数。在大多数情况下,此操作并非绝对,因为通常这些引用无论如何都会进行垃圾回收(假设代码中不存在错误),但是明确地将引用设置为空值以及刻意调用dispose() 函数,具有以下两项非常重要的好处:
- 它将迫使您思考如何分配内存的问题。如果您针对所有类别编写 dispose() 函数,您可能很少会留意可能阻止对象进行清除的实例引用(这样可能会导致内存泄露)。
- 它使垃圾回收流程更加简便。如果所有实例均已明确地设置为空值,垃圾回收器便能够更加轻松高效地回收内存。如果您的应用程序会定期扩大规模(与 MailBrew 从多个不同的帐户查找新邮件时相同),在完成该操作后,您甚至可能会想要调用 System.gc() 函数。
下面是直接执行内存管理的一些 MailBrew 代码的简化版本:
- private function finishCheckingAccount():void
- {
- this.disposeEmailService();
- this.accountData = null;
- this.currentAccount = null;
- this.newUnseenEmails = null;
- this.oldUnseenEmails = null;
- System.gc();
- }
- private function disposeEmailService():void
- {
- this.emailService.removeEventListener(EmailEvent.AUTHENTICATION_FAILED, onAuthenticationFailed);
- this.emailService.removeEventListener(EmailEvent.CONNECTION_FAILED, onConnectionFailed);
- this.emailService.removeEventListener(EmailEvent.UNSEEN_EMAILS, onUnseenEmails);
- this.emailService.removeEventListener(EmailEvent.PROTOCOL_ERROR, onProtocolError);
- this.emailService.dispose();
- this.emailService = null;
- }
采用 SQL 数据库
保存 AIR 应用程序数据有多种不同的方法:
- 平面文件
- 本地共享对象
- EncryptedLocalStore
- 对象序列化
- SQL 数据库
这些方法中的每一种均具有其自身的优缺点(优缺点阐释不在本文的阐述范围之内)。采用 SQL 数据库的其中一个优点在于,它有助于您的应用程序保持较低的内存使用率,而不会从平面文件向存储器加载大量数据。例如,如果您将该应用程序数据存储在数据库中,您便能够仅在必要时选择所需的数据,然后在使用完毕后轻松地将数据从存储器中移除。
MP3 播放器应用程序就是一个很好的例子。如果您要将所有用户曲目相关数据以 XML 文件进行存储,但用户只想搜寻特定艺人或某一流派的曲目,您可能要同时将所有曲目存入存储器,但仅向用户显示该数据的一个子集。利用 SQL 数据库,您可以非常迅速地精确选择用户想要寻找的曲目,并将您的存储使用率降至最低。
介绍您的应用程序
无论您多么善于进行内存管理,或者您的应用程序多么简便,在发布之前进行介绍都是一个很好的主意。Flash Builder 探查器介绍不在本文的讨论范围之内(探查器运用既是一门艺术也是一门科学),但是如果您真的要构建一套功能良好的 AIR 应用程序,您还必须对其进行认真介绍。
2. 降低 CPU 使用率
由于应用程序耗费的 CPU 量精确地具体于应用程序的功能,因此难以提供有关各种 AIR 应用程序 CPU 使用率的一般性诀窍,但有一种通用方式,可降低所有 AIR 应用程序的 CPU 使用率:在应用程序不活跃时,降低应用程序的帧速率。
Flex 框架内置帧速率限制。WindowedApplication 级 backgroundFrameRate 属性可指示应用程序不活跃时采用的帧速率,因此如果您要使用 Flex,请将此属性设置为相应的低值(如 1)。
但是,在编写 MailBrew 时我发现,有时帧速率限制可能稍微复杂一些。MailBrew 有两套通知系统,在新邮件到来时发出低吼般的通知(见图 2),以逐步采用 alpha tween 操作清除它们。当然,这些通知甚至在应用程序不活跃时也会出现,并且需要设定一个适当的帧速率,以便顺利地淡入淡出。因此,我不得不关闭 Flex 框架速率限制机制,编写一套自己的规则。
图 2. MailBrew 通知淡入与淡出,因此在应用程序停用时,帧速率须至少为 24。
测试通知
本通知是说明 MailBrew 如何就新邮件向您发出通知的测试通知。
点击通知取消当前通知,然后点击"X"取消所有等待发布的通知。
我采用的技巧是在我的 ModelLocator 类中指定应用程序的默认帧速率。如果您采用 Cairngorm 框架,则处理方法类似;如果您不采用上述框架,ModelLocator 仅仅代表 MVC 框架模型的类别。该常量按照下列方法进行定义:
- public static const DEFAULT_FRAME_RATE:uint = 24;
然后,我采用下列方式侦测应用程序的激活和停用事件
- this.nativeApplication.addEventListener(Event.ACTIVATE, onApplicationActivate);
- this.nativeApplication.addEventListener(Event.DEACTIVATE, onApplicationDeactivate);
我还采用下列方式定义 ModelLocator 的可绑定变量:
- [Bindable] public var frameRate:uint;
如果管理帧速率的应用程序部分发生变更,则利用 ChangeWatcher 采用下列方式侦测frameRate变量变更:
- ChangeWatcher.watch(ModelLocator.getInstance(), "frameRate", onFrameRateChange);
现在,只要该代码的任何一部分变更 ModelLocator 的 frameRate 变量,便会调用onFrameRateChange 函数:
- private function onFrameRateChange(e:PropertyChangeEvent):void
- {
- this.stage.frameRate = ml.frameRate;
- }
最后,当应用程序激活或遭到停用时,我会采用下列方式,相应地更新帧速率:
- private function onApplicationActivate(e:Event):void
- {
- this.ml.frameRate = ModelLocator.DEFAULT_FRAME_RATE;
- }
- private function onApplicationDeactivate(e:Event):void
- {
- this.ml.frameRate = 1;
- }
所有此类基础设施均可让我做到以下几点:
- 仅通过变更 ModelLocator 的frameRate变量,即可在代码的任何位置变更应用程序帧速率。
- 在应用程序不活跃时(在后台,或在应用程序主窗口关闭后),降低帧速率。
- 在显示通知前,将帧速率恢复至 DEFAULT_FRAME_RATE指定的值,然后在通知淡出后再将其降低。
编写您自己的帧速率限制框架比使用Flex的内建帧速率限制框架复杂得多,但是如果您需要更大的灵活性(不含双关语意),而同时您仍然想要使您的应用程序在不活跃时保持低 CPU 使用率,便值得进行额外的时间投资。
3. 存储敏感数据
如上所述,有几种方法可以保存 AIR 应用程序数据,每种方法均有其各自的优缺点。但是,如果您想要安全地存储数据,则有下列三种最佳选择:
- EncryptedLocalStore 类
- 加密 SQL 数据库
- 自行加密
如果您仅需要存储用户名和密码,我会建议您采用 EncryptedLocalStore (ELS) 类。但是,如果您想要存储大量数据,您可能会想要使用加密数据库(AIR 提供全面支持的加密数据库),或者自行加密,以及将加密数据保存到磁盘上。(由于加密管理指导不在本文的讨论范围之内,因此我假设您采用加密数据库。)
采用 ELS 的奇妙之处在于,您并不需要密码或者口令加密或解密数据,这样便使您的应用程序更加实用。例如,针对服务存储用户名和密码并不会带来任何好处,如果您还不得不提示他们输入其他密码或口令解密原始凭据。所以,在您必须加密超出 ELS 容量的更多数据时,如何才能提供用户采用 ELS 时同样的美妙体验呢?
解决方法是做到以下几点:
- 生成适当的随机密码。
- 采用 EncryptedLocalStore 存储密码。
- 使用密码生成密码安全数据库关键字。
- 利用生成的关键字加密和解密数据库。
这可能看起来很复杂,但幸运的是,您所需的绝大多数代码已经编写完毕。让我们进一步了解一下每个步骤。
生成随机密码
下列代码是我编写的一组函数,用于生成无法猜测的随机密码:
- private static const POSSIBLE_CHARS:Array = ["abcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZ","0123456789","~`!@#$%^&*()_-+=[{]}|;:'\"\\,<.>/?"];
- private function generateStrongPassword(length:uint = 32):String
- {
- if (length < 8) length = 8;
- var pw:String = new String();
- var charPos:uint = 0;
- while (pw.length < length)
- {
- var chars:String = POSSIBLE_CHARS[charPos];
- var char:String = chars.charAt(this.getRandomWholeNumber(0, chars.length - 1));
- var splitPos:uint = this.getRandomWholeNumber(0, pw.length);
- pw = (pw.substring(0, splitPos) + char + pw.substring(splitPos, pw.length));
- charPos = (charPos == 3) ? 0 : charPos + 1;
- }
- return pw;
- }
- private function getRandomWholeNumber(min:Number, max:Number):Number
- {
- return Math.round(((Math.random() * (max - min)) + min));
- }
既然您已经具备随机密码,便可以进行存储。
存储您的随机密码
安全存储您将用来生成数据库加密密钥密码的最佳方式是采用 EncryptedLocalStore 类方法。ELS API 易于使用;不过,我通常采用 as3preferenceslib 项目取而代之。采用 as3preferenceslib 的优势在于,我可以利用相同的 API 存储所有应用程序首选项。在幕后,as3preferenceslib 利用 ELS 存储您出于安全原因指定的数据。该代码如下所示:
- var ml:ModelLocator = ModelLocator.getInstance();
- var prefs:Preference = ml.prefs;
- var databasePassword:String = prefs.getValue(PreferenceKeys.DATABASE_PASSWORD);
- if (databasePassword == null)
- {
- databasePassword = this.generateStrongPassword();
- ml.prefs.setValue(PreferenceKeys.DATABASE_PASSWORD, databasePassword, true); // The third argument indicates secure storage
- ml.prefs.save();
- }
生成密码安全数据库关键字
生成密码安全加密密钥十分复杂,但幸运的是,我们具备供您完成此操作的代码。我采用位于as3corelib project 的 EncryptionKeyGenerator。
通过将您的随机密码与特定的用户帐户及特定的计算机相关联,利用 EncryptionKeyGenerator 将您的加密数据安全性提高一个层次。换句话说,即使有人发现了您的随机密码,除非他们具备用户的计算机并以用户身份登录,否则不会产生任何效果。
采用 EncryptionKeyGenerator 时,不要存储返回的密码密钥,这一点至关重要;恰恰相反,您可存储用于查看用途的密码,然后生成随选密码密钥。下列代码将演示正确方法:
- // Get the databasePassword from the Preference object using the code shown above, then...
- var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
- var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(databasePassword);
- // Now use the encryptionKey to encrypt and decrypt your database.
加密与解密您的数据库
既然您已经具备密码安全数据库密钥,您唯一要做的就是在创建数据库连接时输入密钥。下列代码示例(由于缺乏事件侦听器,因此经过简化)说明了加载加密数据库文件及建立数据库连接的方法:
- var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
- var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(databasePassword);
- var dbFile:File = File.applicationStorageDirectory.resolvePath("myEncryptedDatabase.db");
- var aConn:SQLConnection = new SQLConnection();
- aConn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, this.encryptionKey);
4. 编写"无头服务器"应用程序
在主窗口关闭后仍然继续运行的应用程序,时常被称作"无头"应用程序。许多应用程序在 Mac 及 Windows 系统上采用这种范例,一些应用程序(即时通信和电子邮件客户端,例如)极少"最小化到系统托盘"(见图 3)。
图 3. MailBrew 最小化到 Windows 系统托盘。
可将 AIR 应用程序设计为在应用程序主窗口关闭后退出,或者它们也可作为无头应用程序运行。让您的应用程序在应用程序主窗口关闭后继续运行的最简便方法是,将NativeApplication 的autoExit 属性设置为"假",如下所示:
- private function onApplicationComplete():void
- {
- NativeApplication.nativeApplication.autoExit = false;
- }
如果你准备采用 Flex 框架,则您还可以利用 WindowedApplicatio 标签的autoExit属性设置此属性,如下所示:
- <s:WindowedApplication
- xmlns:fx="http://ns.adobe.com/mxml/2009"
- xmlns:s="library://ns.adobe.com/flex/spark"
- xmlns:mx="library://ns.adobe.com/flex/halo"
- xmlns:c="com.mailbrew.components.*"
- width="500" height="400" minWidth="500" minHeight="400"
- showStatusBar="false" backgroundFrameRate="-1"
- autoExit="true"
- applicationComplete="onApplicationComplete();">
由于您已经成功阻止应用程序在应用程序主窗口关闭后退出,您必须在用户再次需要使用时(或许是它们点击停靠或系统托盘图标时),重新打开应用程序主窗口。设计您的应用程序支持此类互动的最简便方式是,将您的主要应用程序界面作为 NativeWindow的子级置于其自身的组件中。下列代码显示了在主应用程序隐藏或最小化到系统托盘后,重新打开主应用程序的跨平台方法:
- private function onApplicationComplete():void
- {
- NativeApplication.nativeApplication.autoExit = false;
- if (NativeApplication.supportsDockIcon)
- {
- NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE, onShowWindow);
- }
- else if (NativeApplication.supportsSystemTrayIcon)
- {
- SystemTrayIcon(NativeApplication.nativeApplication.icon).addEventListener(ScreenMouseEvent.CLICK, onShowWindow);
- }
- }
- private function onShowWindow(e:Event):void
- {
- var mainApplicationUI:MainApplicationUI = new MainApplicationUI();
- mainApplicationUI.open(true);
- }
编写无头应用程序的另一种方法是,不关闭您的应用程序主窗口,而是仅将其隐藏。这种方法不需要您将 NativeWindow 的 autoExit属性设置为"假" ,这是因为您并没有真正关闭应用程序主窗口,但是需要您编写代码阻止该窗口关闭,并将其 visilibity属性设置为" 假",如下所示:
- private function onWindowClosing(e:Event):void
- {
- e.preventDefault();
- this.visible = false;
- ml.frameRate = 1;
- }
恢复您的应用程序主窗口的方法与上述方法类似,但并非创建新的主要应用程序界面实例,您只需将您的应用程序 NativeWindow.visibility 属性再次设置为"真 ",如下所示:
- private function onShowWindow(e:Event):void
- {
- this.visible = true;
- ml.frameRate = ModelLocator.DEFAULT_FRAME_RATE;
- this.nativeWindow.activate();
- }
我认为,拖动主应用程序 visibility 属性的方法是较为简易的方式,并在绝大多数情况下均可发挥作用。这种方法唯一不能奏效的情况是,在您想要用户像在 PixelWindow 等应用程序中那样,能够打开多个主要应用程序界面实例的情况下。
5. 更新停靠与系统托盘图标
为了使无头应用程序能够保持某种可视状态,它们往往利用 Mac 停靠功能或者 Windows 系统托盘功能的优势。这些图标为用户提供了一种恢复应用程序主窗口的方式,并且也为应用程序提供了一种为最终用户传递信息的方式。AIR 不会提供叠加应用程序图标上的文本信息的 API,但是会有动态生成位图,以及更新停靠或系统托盘的应用程序图标的 API(见图 4)。
图 4. 叠加多封未读邮件的 MailBrew 停靠图标。
下列代码是取自 MailBrew 的一个示例,用来说明如何向停靠图标上添加文本和图形,以便用户能够一目了然地看到未读邮件的数量(见图 4)。类似的方法也可用于系统托盘图标,但系统托盘图标仅为 16 方形像素,因而难以在其上叠加太多文本。Windows 7 可支持更多富于表现力的任务栏图标,未来我们计划支持新型 API。
- // If we're on Windows, return. This icon wouldn't look very good in the system tray.
- if (NativeApplication.supportsSystemTrayIcon) return;
- var unseenCount:uint = getUnreadMessageCount(); // Function for counting the number of unread messages.
- var unreadCountSprite:Sprite = new Sprite();
- unreadCountSprite.width = 128;
- unreadCountSprite.height = 128;
- unreadCountSprite.x = 0;
- unreadCountSprite.y = 0;
- var padding:uint = 10;
- // Use FTE APIs to get the best looking text.
- var fontDesc:FontDescription = new FontDescription("Arial", "bold");
- var elementFormat:ElementFormat = new ElementFormat(fontDesc, 30, 0xFFFFFF);
- var textElement:TextElement = new TextElement(String(unseenCount), elementFormat);
- var textBlock:TextBlock = new TextBlock(textElement);
- var textLine:TextLine = textBlock.createTextLine();
- textLine.x = (((128 - textLine.textWidth) - padding) + 2);
- textLine.y = 32;
- unreadCountSprite.graphics.beginFill(0xE92200);
- unreadCountSprite.graphics.drawEllipse((((128 - textLine.textWidth) - padding) - 3), 2, textLine.textWidth + padding, textLine.textHeight + padding);
- unreadCountSprite.graphics.endFill();
- unreadCountSprite.addChild(textLine);
- var shadow:DropShadowFilter = new DropShadowFilter(3, 45, 0, .75);
- var bevel:BevelFilter = new BevelFilter(1);
- unreadCountSprite.filters = [shadow, bevel];
- var unreadCountData:BitmapData = new BitmapData(128, 128, true, 0x00000000); unreadCountData.draw(unreadCountSprite);
- // The Dynamic128IconClass referenced below is embedded.
- var appData:BitmapData = new Dynamic128IconClass().bitmapData;
- appData.copyPixels(unreadCountData, new Rectangle(0, 0, unreadCountData.width, unreadCountData.height),
- new Point(0, 0),
- null, null, true);
- var appIcon:Bitmap = new Bitmap(appData);
- // If you do want to change the system tray icon on Windows, as well, add a 16x16 icon to the array below.
- InteractiveIcon(NativeApplication.nativeApplication.icon).bitmaps = [appIcon];
6. 处理网络连接变更
随着人们将越来越多的数据转移至云中,本地访问和缓存该类数据的桌面应用程序也将变得日益重要起来。Adobe AIR 是编写这些类型应用程序的理想平台,理由如下:
- 广泛的协议支持。 您可以在运行时间本身及第三方 ActionScript 库之间,采用您想要采用的任何协议,交换桌面客户端与 Web 服务数据 — 或者在 HTTP 或 TCP 套接字上轻松地编写您自己的协议。
- 多平台支持。 由于 Web 本身就是跨平台发挥作用,如果您打算在 Web 服务上编写桌面客户端,这也使得它具有跨平台的特性。
- 支持 Web 技术。 由于 AIR 支持 Web 技术,您可以利用同样的工具和技术,构建您的 Web 应用程序采用的桌面客户端。
在 Web 服务上编写桌面客户端面临的其中一项挑战在于,确定网络连接。即使采用 WiFi 扩散及 3G(很快将会演进为 4G)等高速无线数据协议,事实上,我们仍然无法一直保持连接。因此,依赖网络连接的应用程序需要寻求一种确切了解它们是否处于连接状态的方法。
我们首次尝试解决这个问题,引发了 NativeApplication 类的 NETWORK_CHANGE 事件。每当网络开始连接或我们最初认为足以连接但断开时,都会触发 NETWORK_CHANGE 事件。不过,我们很快便意识到,这些信息不足以让开发人员了解他们是否能够获取特定的服务。
例如,网络连接会随着 VPN 连接的打开或关闭、虚拟机启动或停止、无线网络进入或超出范围、电缆插入或拔出等条件影响而连接或断开。并且当然,有时无法预测应用程序服务将在何处驻留;它们可能位于公共互联网、防火墙后,或者甚至在本地计算机上。最后,即使您能够确定可以获取服务,也无法保证在您需要访问时该服务能够实时响应。所有这些因素都向我们表明,我们需要更加全面的 API,以及有关如何使用它们的一些最佳实践做法。
我发现,桌面客户端应用程序一般分为两类:访问一系列已知服务(Twitter 和 Facebook 客户端,例如)的应用程序,以及访问任意数量的不可预见服务的应用程序(RSS 聚合器、电子邮件或 IM 客户端等)。根据我的经验,分别处理这两种类型的应用程序的连接变更将会十分有效。
访问一系列已知服务的应用程序
如果您了解您的应用程序需要访问哪些 Web 服务,则监控其可用性的最简便方式是采用air.net.URLMonitor 类(或者 air.net.SocketMonitor,如果您使用 TCP 而不是 HTTP)。URLMonitor 类一般会定期对一组特定的 URL 进行民意测验,从而让您了解任何状态变更,如下列代码所示:
- private var urlMonitor:URLMonitor;
- private function onCreationComplete():void
- {
- var req:URLRequest = new URLRequest("http://www.myserver.com/myservice");
- this.urlMonitor = new URLMonitor(req, [200, 304]); // Acceptable status codes
- this.urlMonitor.pollInterval = 60 * 1000; // Every minute
- this.urlMonitor.addEventListener(StatusEvent.STATUS, onStatusChange);
- this.urlMonitor.start();
- }
- private function onStatusChange(e:Event):void
- {
- if (this.urlMonitor.available)
- {
- // Everything is fine.
- }
- else
- {
- // Service is not available.
- // Consider alerting the user.
- }
- }
访问任意服务的应用程序
如果您并不知道您的应用程序将要访问哪些服务,因为服务须由用户配置(与电子邮件客户端的情况类似),或者您并不了解您的应用程序将要访问多少项服务(与 RSS 聚合器的情况类似),则采用URLMonitor 并不可行。事实上,应用程序访问的服务越是不可预见,则越是难以确定服务是否可供访问。例如,即使您的应用程序意识到其公共互联网连接不可靠,可能仍然需要在防火墙后聚合 RSS 源;或者,即使您的应用程序发现根本不存在可供使用的网络连接,可能依然要访问本地计算机上运行的服务。由于网络连接变得越来越难以预测和度量,在很多情况下,最好的方式只是尝试建立连接,并在连接失败时报告错误。
MailBrew 就是一个很好的例子。由于 MailBrew 能够配置访问任意数量的电子邮件账户,因此在尝试连接之前,并不存在任何一种可靠方法,供应用程序了解是否真的能够获取服务。所以,该应用程序通过执行下列操作,适当地处理网络错误:
- 注册指示某些部分发生错误的任何事件(IOErrorEvent,HTTP_RESPONSE_STATUS_EVENT等)。
- 如果发现连接问题,请更新指定服务无法访问的数据库标志。
-
更新通知用户服务不可用的用户界面。在图 5 中,我的两个 Gmail 账户均可访问,因为我建立了外界网络连接,但是我的 Adobe 账户无法访问,因为我并未登录 VPN。
注意:即使您采用 URLMonitor检查您的服务可用性,您也不能依赖它,这是因为在监视器上次检查到您的应用程序到需要进行访问这段时间期间,服务很可能不再有效。因此,您必须一直侦听可能指示连接问题的相应事件,并适当地处理所有问题。
图 5.如果出现连接错误,则帐户名称变为红色,并且用户可以选择打开错误信息窗口。
网络连接可能十分复杂,但是应用程序须以直观、内容丰富的方式展现给最终用户,这一点至关重要。采用上述两种方法须保证您的应用程序在任何情况下均正常运转,无论连接多么不可靠或者难以预测。
7. 创建"调试"和"测试"模式
所有应用程序开发人员都明白,编写代码是一个高度重复性过程。这项工作的流程通常是编写一些代码,运行应用程序进行测试,然后重复几十、几百,甚至几千次,重复次数取决于应用程序规模的大小。
如果您的应用程序将要访问外部服务,但是,事实上,由于可能受到速率限制(在 Twitter 或 IM 客户端的情况下),此流程可能会十分复杂,或者访问远程服务可能会大幅降低这一过程的速度(当平均超过数百次重复时)。我采用下列两种方法管理这些类型的方案:测试模式和调试模式。
测试模式
当我编写其中一套初期移动 AIR 应用程序(TweetCards)时,我很快意识到,在每次重复时从 Twitter 中抓取数据无法形成规模。这不仅会减慢开发速度,而且我也可能会受到速度限制(即由于我连接过于频繁,Twitter 会在某段时间拒绝我的请求),因而在我未建立网络连接时,将无法在该应用程序上开展工作(我编写了一些长途飞行编码)。
图 6. 以"测试模式"运行且带有假数据的 TweetCards。
T答案是创建测试模式,从而在输入凭据字段的用户名和密码均为"测试"时进行启用(见图 6)。下列代码演示了这一概念:
- private function onSaveAccountInfo(e:MouseEvent = null):void
- {
- var username:String = this.usernameInput.value;
- var password:String = this.passwordInput.value;
- var ml:ModelLocator = ModelLocator.getInstance();
- ml.testMode = (this.usernameInput.value == "test" && this.passwordInput.value == "test");
- ml.credentials = {"username":this.usernameInput.value, "password":this.passwordInput.value};
- ml.currentScreen = Screen.READ_SCREEN; }
当应用程序处于测试模式,而不是向 Twitter 发出数据请求时,我生成了我自己的测试数据:
- private function getTweets():void
- {
- var ml:ModelLocator = ModelLocator.getInstance();
- if (ml.testMode)
- {
- this.createTestData();
- }
- else
- {
- this.queryTwitter();
- }
- }
其结果是,数据将瞬间加载,而我不必向 Twitter 发出任何请求便可测试我的应用程序。
调试模式
我在编写 MailBrew 时探索出的另一种方法是,为我的应用程序构建"调试"模式。调试模式这一需求是我编写代码在应用程序启动时检查用户的电子邮件账户的瞬间产生。从那以后,每当我想要测试应用程序的组件 — 甚至小到一个按钮的位置 — 我都要向多项电子邮件服务发出请求,这样便开始放慢我的开发速度。下列代码就是解决方法:
- private function onApplicationComplete():void
- {
- // If we're running from ADL, put the app in debug mode.
- ModelLocator.debugMode = Capabilities.isDebugger;
- }
问题解决了。自那时起,从 ADL 运行 MailBrew 时,便不再发送 CheckMailEvent。同时还有其他一些额外的好处。举例来说,我决定从 ADL 运行程序时,不再采用新的整体误差处理 AIR 2 功能,因此我将下列代码行:
- this.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError);
变更为:
- if (!ModelLocator.debugMode) this.loaderInfo.uncaughtErrorEvents.addEventListener(UncaughtErrorEvent.UNCAUGHT_ERROR, onUncaughtError);
最后,当从 ADL 调用时,Updater.update() 及 NativeApplication.startAtLogin 等某些 API 将引发运行异常,这是因为它们在开发和测试环境下无法发挥作用。如果您的应用程序中采用任何一种这些 API,将它们置于检查调试模式条件下将会是一个好主意。
注意: 请注意,我在其中一个位置采用Capabilities.isDebugger 设置debugMode全局标志的基本架构,而不是在我想要检查模式的所有位置均采用Capabilities.isDebugger 这样做的优点在于,我可以轻松地变更标准,决定应用程序在一个位置是否处于 debugMode 模式。例如,我可能决定利用命令行参数支持让应用程序进入调试模式,因此安装版本也可能进入调试模式,以供测试。
8. 在用户处于空闲状态时进行检测
当我们确定可以为 AIR 应用程序编写通知系统后,我们意识到,我们还需要寻求一种方法来检测用户是否真的在计算机前操作,以便该应用程序了解实际显示这些通知是否有意义。例如,如果用户并不能实际看到或听到通知,则 MailBrew 持续显示和播放通知将毫无意义。
我们通过在 NativeApplication 类上采用 USER_IDLE 和 USER_PRESENT 事件的方式解决了这一问题。这些事件在注册时,将会告知应用程序用户何时开始一直处于空闲状态,以及他们又是何时返回计算机。下列代码显示了一个简单的例子:
- private function onCreationComplete():void
- {
- NativeApplication.nativeApplication.idleThreshold = 2 * 60; // In seconds -- the default is 5 minutes.
- NativeApplication.nativeApplication.addEventListener(Event.USER_IDLE, onUserIdle);
- NativeApplication.nativeApplication.addEventListener(Event.USER_PRESENT, onUserPresent);
- }
- private function onUserIdle(e:Event):void
- {
- // No keyboard or mouse input for 2 minutes.
- }
- private function onUserPresent(e:Event):void
- {
- // The user is back!
- }
我发现在现实方案中,在我的应用程序中直接注册这些事件并不那么有效;相反,通知框架等库执行该操作往往更加有效。例如,MailBrew采用的通知框架可自动注册这些事件,并在用户处于空闲状态时列队等候显示通知,然后在用户返回后自动开始显示这些通知。此外,在使用这些 API 时,请确保将应用程序工作流程考虑在内。例如,如果您的应用程序显示视频,由于用户可能并非真的处于空闲状态,您可能会想要在媒体播放期间禁用空闲计时器。
如需有关这些 API 运行的简单而又现实的例子,请参见我的屏幕保护示例应用程序,叫做 SPF,或者屏幕保护系数。
9. 管理辅助窗口
AIR 应用程序针对首选项或者方框开启辅助窗口这一操作不足为奇。在 AIR 程序中打开辅助窗口非常容易,但有些方面必须注意:打开多个相同的实例。由于 AIR 程序没有模式窗口这一概念,如果您想要阻止用户同时打开多个窗口,您必须自行制定方案。
图 7. MailBrew 设置窗口。每次仅可打开一个窗口。
幸运的是,这很容易做到。我具有 WindowManager 类,其中包含处理下列类型的辅助窗口的多项有效功能:
- /**
- * Returns an instance of a NativeWindow if it's already open.
- */ public static
- function getWindowByTitle(title:String):NativeWindow {
- var allWindows:Array = NativeApplication.nativeApplication.openedWindows;
- for each (var win:NativeWindow in allWindows)
- {
- if (win.title == title)
- {
- return win;
- }
- }
- return null;
- }
getWindowByTitle 功能使我能够打开下列窗口:
- private function onOpenSettings(e:MouseEvent):void
- {
- var win:NativeWindow = WindowManager.getWindowByTitle(WindowManager.PREFERENCES);
- if (win != null)
- {
- win.activate();
- }
- else
- {
- var prefsWin:PreferencesWindow = new PreferencesWindow();
- prefsWin.open(true);
- }
- }
这种方法可保证每次仅可开启一个首选项窗口 — 即使用户不小心双击"Preferences"按钮也是一样。如果"Preferences"窗口已经打开,也只会被激活(来到前面的窗口,并予以关注),而不会遭到复制。
作为额外奖励,下面是我的 WindowManager 类的另一项有用功能:
- /**
- * Hand me a window, and I'll center it on the primary monitor.
- */
- public static function centerWindowOnMainScreen(win:NativeWindow):void
- {
- var initialBounds:Rectangle = new Rectangle((Screen.mainScreen.bounds.width / 2 - (win.width/2)), (Screen.mainScreen.bounds.height / 2 - (win.height/2)), win.width, win.height);
- win.bounds = initialBounds;
- win.visible = true;
- }
10. 针对不同的操作系统进行编程
AIR 具有跨平台运行的特性,但这并不意味着应用程序不能或不应该识别平台之间的差异,我们已经在本文中了解到,采用 Windows 系统托盘与 Mac OS X 停靠开展工作时如何采用不同的图标尺寸,但是有时平台差异并没有那么简单。例如,在 MailBrew 设置窗口中,我拥有 Mac OS X 选项,在新邮件到来时停靠图标会不停跳动,而在 Windows 上并不存在这种图标,但在 Windows 上,我具有任务栏图标闪烁选项,而 Mac OS X 上不存在该选项。
有多种方法可供处理这些类型的平台特定问题,因此,与其说是一种方法正确或者高级,不如只是向您介绍我在编写 MailBrew 时尝试使用的几种方法。下面是处理 PreferencesWindow 组件平台差异的相关代码:
- <s:Window creationComplete="onCreationComplete();">
- <fx:Script>
- <![CDATA[
- private function onCreationComplete():void
- {
- if (NativeApplication.supportsDockIcon)
- {
- this.currentState = "supportsDockIcon";
- this.bounceDockIconCheckbox.selected = prefs.getValue(PreferenceKeys.APPLICATION_ALERT, false);
- }
- else
- {
- this.currentState = "supportsSystemTray";
- this.flashTaskBarCheckbox.selected = prefs.getValue(PreferenceKeys.APPLICATION_ALERT, false);
- }
- }
- ]]>
- </fx:Script>
- <s:states>
- <s:State name="supportsDockIcon"/>
- <s:State name="supportsSystemTray"/>
- </s:states>
- <s:VGroup width="100%" height="100%">
- <s:Group width="100%" includeIn="supportsDockIcon">
- <s:Label text="Bounce Dock Icon" fontWeight="bold" y="5" left="5"/>
- <s:Label y="20" width="210" left="5">Do you want the Dock icon to bounce when you get new messages?</s:Label>
- <s:CheckBox id="bounceDockIconCheckbox" y="5" right="20"/>
- </s:Group>
- <s:Group width="100%" includeIn="supportsSystemTray">
- <s:Label text="Flash Task Bar Icon" fontWeight="bold" y="5" left="5"/>
- <s:Label y="20" width="210" left="5">Do you want the task bar icon to flash when you get new messages?</s:Label>
- <s:CheckBox id="flashTaskBarCheckbox" y="5" right="20"/>
- </s:Group>
- </s:VGroup>
- </s:Window>
正如您所见,此处的关键在于利用非常便利的 Flex 状态功能完成绝大部分工作。但是当然,还有其他方法也可以完成这项操作。例如,MailBrew 的另一项平台差异在于应用程序主窗口工具栏。在 Mac OS X 上,标题和徽标位于窗口左侧,按钮位于窗口右侧(见图 8)。
图 8. MailBrew 在 Windows 上位于后台,而在 Mac OS X 上位于前台。请注意工具栏的不同布局。
不过,我发现在 Windows 上,将互动工具置于窗口右侧很容易一不小心点击窗口控制器而意外关闭窗口。答案是改变次序,如下列代码所示:
- <s:Group creationComplete="onCreationComplete();">
- <fx:Script>
- <![CDATA[
- private function onCreationComplete():void
- {
- if (NativeApplication.supportsSystemTrayIcon) // Windows
- {
- this.logoBitmap.visible = false;
- this.logoLabel.right = 3;
- this.buttonGroup.left = 5;
- }
- else
- {
- this.logoBitmap.visible = true;
- this.logoBitmap.left = 4;
- this.logoLabel.left = 31;
- this.buttonGroup.right = 5;
- }
- }
- ]]>
- </fx:Script>
- <s:BitmapImage id="logoBitmap" source="{ModelLocator.getInstance().TopLeftLogo}" top="3"/>
- <s:Label id="logoLabel" text="MailBrew" top="11" fontSize="20" color="0xf2f2f2" fontFamily="_sans"/>
- <s:HGroup id="buttonGroup" top="5">
- <mx:Button id="checkNowButton" label="Check Now" icon="{ModelLocator.getInstance().CheckNowIconClass}"/>
- <mx:Button id="settingsButton" label="Settings" icon="{ModelLocator.getInstance().ConfigureIconClass}"/>
- <mx:Button id="aboutButton" label="About" icon="{ModelLocator.getInstance().AboutIconClass}"/>
- </s:HGroup>
- </s:Group>
在这个例子中,我不采用状态,而是只是利用其left, right, 及 visible 属性重新排列或移除组件。
虽然我尽量将应用程序间的平台差异降至最低,但现实情况是差异偶尔还是会出现。在我看来,最好编写一些代码来处理平台间的差异,而不是假装它们不存在。
关于作者
Christian Cantrell 是 Adobe AIR 小组的一名应用程序开发人员。他的工作是通过构建应用程序来验证 AIR API,通过研究新兴技术帮助指导平台方向。阅读他的博客*或通过 Twitter* 了解他的动态。