Wednesday, May 4, 2011

Building better Adobe AIR applications

This Post describes ten techniques I've used to improve performance, usability, and security of AIR applications, and to make the development process faster and easier:

1. Keeping memory usage low

I recently wrote an email notification application called MailBrew. MailBrew monitors Gmail and IMAP accounts, then shows Growl-like notifications and plays alerts when new messages come in. Since it's supposed to keep you informed of new email, it obviously has to run all the time, and since it's always running, it has to be very conservative in terms of memory usage (see Figure 1).
MailBrew consumes some memory while initializing, and uses a little every time it checks mail, but it always goes back down.
Figure 1. MailBrew consumes some memory while initializing, and uses a little every time it checks mail, but it always goes back down.
Since the runtime does automatic garbage collection, as an AIR developer you don't have to manage memory explicitly, but that doesn't mean you are exempt from having to worry about it. In fact, AIR developers should still think very carefully about creating new objects, and especially about keeping references around so that they can't be cleaned up. The following tips will help you keep the memory usage of your AIR applications both low and stable:
  • Always remove event listeners
  • Remember to dispose of your XML
  • Write your own dispose() functions
  • Use SQL databases
  • Profile your applications

Always remove event listeners

You've probably heard this before, but it's worth repeating: when you're done with an object that throws events, remove all your event listeners so that it can be garbage collected.
Here's some (simplified) code from an application I wrote called PluggableSearchCentral that shows adding and removing event listeners properly:

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);
 }

Another technique is to create a variable that holds an event listener function so that the event listener can easily remove itself, like this:

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); }
 

Remember to dispose of your XML

In Flash Player 10.1 and AIR 1.5.2, we added a static function to the System class called disposeXML() which makes sure all the nodes in an XML object are dereferenced and immediately available for garbage collection. If your application parses XML, make sure to call this function when you're finished with an XML object. If you don't use System.disposeXML(), it's possible that your XML object will have circular references which will prevent it from ever being garbage collected.
Below is a simplified version of some code that parses the XML feed generated by Gmail:

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);
 
 

Write your own dispose() functions

If you are writing a medium to large application with a lot of classes, it's a good idea to get into the habit of adding "dispose" functions. In fact, you will probably want to create an interface called IDisposable to enforce this practice. The purpose of a dispose() function is to make sure an object isn't holding on to any references that might keep it from being garbage collected. At a minimum, dispose() should set all the class-level variables to null. Wherever there is a piece of code using an IDisposable, it should call its dispose() function when it's finished with it. In most cases, this isn't strictly necessary since these references will usually get garbage collected anyway (assuming there are no bugs in your code), but explicitly setting references to null and explicitly calling the dispose() function has two very important advantages:
  • It forces you to think about how you're allocating memory. If you write a dispose() function for all your classes, you are less likely to inadvertently retain references to instances which could prevent objects from getting cleaned up (which might cause a memory leak).
  • It makes the garbage collector's life easier. If all instances are explicitly nulled out, it's easier and more efficient for the garbage collector to reclaim memory. If your application grows in size at predictable intervals (like MailBrew when it checks for new messages from several different accounts), you might even want to call System.gc() when you're finished with the operation.
Below is a simplified version of some MailBrew code that does explicit memory management:

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; 
}
 

Use SQL databases

There are several different methods for persisting data in AIR applications:
  • Flat files
  • Local shared objects
  • EncryptedLocalStore
  • Object serialization
  • SQL database
Each of these methods has its own set of advantages and disadvantages (an explanation of which is beyond the scope of this article). One of the advantages of using a SQL database is that it helps to keep your application's memory footprint down, rather than loading a lot of data into memory from flat files. For example, if you store your application's data in a database, you can select only what you need, when you need it, then easily remove the data from memory when you're finished with it.
A good example is an MP3 player application. If you were to store data about all the user's tracks in an XML file, but the user only wanted to look at tracks from a specific artist or of a particular genre, you would probably have all the tracks loaded into memory at once, but only show users a subset of that data. With a SQL database, you can select exactly what the user wants to see extremely quickly and keep your memory usage to a minimum.

Profile your applications

No matter how good you are at memory management or how simple your application is, it's a very good idea to profile it before you release it. An explanation of the Flash Builder profiler is beyond the scope of this article (using profilers is as much an art as a science), but if you're serious about building a well-behaved AIR application, you also have to be serious about profiling it.

2. Reducing CPU usage

It's very difficult to provide general tips about CPU usage in AIR applications since the amount of CPU an application uses is extremely specific to the application's functionality, but there is one universal way to reduce CPU usage across all AIR applications: lower your application's frame rate when it's not active.
The Flex framework has frame rate throttling built in. The WindowedApplication class's backgroundFrameRate property indicates the frame rate to use when the application isn't active, so if you're using Flex, simply set this property to something appropriately low like 1.
As I learned when writing MailBrew, sometimes frame rate throttling can be slightly more complicated, however. MailBrew has a notification system that brings up Growl-like notifications when new messages come in (see Figure 2), easing them in with a gradual alpha tween. Of course, these notifications appear even when the application isn't active, and require a decent frame rate in order to fade in and out smoothly. Therefore, I had to turn off the Flex frame rate throttling mechanism and write one of my own.

 

 

 

No comments:

Post a Comment