现在的位置: 首页 > 综合 > 正文

构建更加完善的Adobe AIR应用程序之十大秘诀

2013年05月23日 ⁄ 综合 ⁄ 共 12250字 ⁄ 字号 评论关闭

由于我们已经推出了AIR 2,我想这将是回顾我在过去几个月编写的所有 AIR 代码的一个绝佳时机,我会精选一些最佳代码段和概念在社区内分享。本文介绍了我用来提高 AIR 应用程序的性能、可用性及安全性,并使开发流程更加迅速简便的十大技巧:


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();
emailHeader.from
= email.PURL::author.PURL::name;
emailHeader.subject
= email.PURL::title;
emailHeader.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 constDEFAULT_FRAME_RATE:uint = 24;

然后,我采用下列方式侦测应用程序的激活和停用事件

 

this.nativeApplication.addEventListener(Event.ACTIVATE, onApplicationActivate);
this.nativeApplication.addEventListener(Event.DEACTIVATE, onApplicationDeactivate);

我还采用下列方式定义 ModelLocator 的可绑定变量:

 

 

[Bindable] public varframeRate: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 时同样的美妙体验呢?

解决方法是做到以下几点:

  1. 生成适当的随机密码。
  2. 采用EncryptedLocalStore存储密码。
  3. 使用密码生成密码安全数据库关键字。
  4. 利用生成的关键字加密和解密数据库。

这可能看起来很复杂,但幸运的是,您所需的绝大多数代码已经编写完毕。让我们进一步了解一下每个步骤。

生成随机密码

下列代码是我编写的一组函数,用于生成无法猜测的随机密码:

 

代码

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 应用程序设计为在应用程序主窗口关闭后退出,或者它们也可作为无头应用程序运行。让您的应用程序在应用程序主窗口关闭后继续运行的最简便方法是,将 NativeApplicationautoExit 属性设置为"假",如下所示:

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

抱歉!评论已关闭.