This post describes a memory issue I had with AIR on Android when processing multiple large images — was having app crashes/restarts.
AIR, Droids, and Memory
I'm a bit leery about writing this post, since it recommends forcing execution of the Flash Player's garbage collection, something you generally don't want to do. However, it may be acceptable when you're running on a Droid and processing large images. Let me make my case, and I'd be happy to get feedback if you think I'm out of line on this.
One of the nice features of AIR on Android is having access to the phone's camera and cameraRoll (the latter provides access to saved images). However, default camera settings create multi-MB images (Droid X has a 8MP camera) that can use a lot of memory when you read them in. As a result, you need to keep a close eye on memory usage as you work with these images.
To be clear, when I talk about memory issues here I'm not talking about memory leaks. Rather, I'm talking about memory that is available for garbage collection but garbage collection just isn't running frequently enough — at least not frequently enough for the Droid, which of course has limited memory (Droid X = 512MB).
Memory use that wouldn't cause problems on a PC can cause stability issues on a Droid — the code I provide below runs without problems on a PC but consistently causes problems on a Droid. Use more than your share of memory on a Droid and the OS will simply whack your app.
The problem
The problem addressed here is caused by an ever-increasing memory footprint while waiting for garbage collection to run.
To illustrate this I've written a little diagnostic app (source code is later in the post). Below are 2 screencaps. The image to the left shows the memory usage after having selected just 6 images from CameraRoll. Note the msgline, which reports the memory footprint (from System.privateMemory). Yes, you read that right, that's 196MB of memory on a phone with a total of 512MB. And all we've done is read 6 images in, consecutively assigning them to an Image control's source property. The image to the right is taken a moment later, after a forced execution of garbage collection — quite a drop in the app's memory footprint.
IMAGE LEFT: after selecting 6 images from cameraRoll: 196MB
IMAGE RIGHT: immediately after forcing garbage collection: 93MB
My Solution
So, my solution wasn't rocket science, I just force garbage collection after every image read using System.gc(). Note that this requires Flash Player 10, which shouldn't be an issue for Android apps since they already require FP 10.
To better illustrate the before/after picture I've included some Flex profiler screen grabs below (these profiler execs were done on my PC, and I'm assuming the FP's behavior is similar in both PC and Droid environments, though they certainly aren't the same, since I've noted generally higher memory usage when running on the phone).
In this execution I select an image at the 5 second mark and then select another every 5 seconds. You can see that garbage collection is running at times — where the blue line dips from the red — but it isn't sufficient to keep us from clinbing to near 150MB. At the end (the 50 second mark) I manually execute garbage collection.
So, here's the memory profile with gc running on its own schedule...
Fig 1. Using normal GC — peak memory 143 MB
...and below is the same app with a force of gc after every image selection/read. Now memory peaks at only 60MB and generally settles down to around 32 MB after gc runs.
Fig 2. Using forced GC — peak memory 60 MB
Conclusion
As noted above, this wasn't a memory leak problem — once GC executes memory is reclaimed. Using this approach — forcing gc after every image selection — changed my app from flaky to stable.
I admit I don't fully understand what's going on here (yes, in both scenarios memory is eventually reclaimed, but why is overall post-gc memory use lower when the System.gc call is introduced?). In any case, I wanted to post this in case others are having similar problems. As noted above, if you have insight into this or want to take issue with my fix feel free to use the Contact me link at the bottom of the page.
BTW, none of this is a criticism of how gc is implemented in the FP, you certainly don't want it running every time you perform a task in your app, since it can add significant processing to a frame. On the other hand, it appears that garbage collection isn't quite aggressive enough for phone apps working with large images. And in this case I think it's acceptable to help it along by using System.gc(). Keep in mind that this is an exceptional case — in general you shouldn't be manually running garbage collection. For more on this topic see the references section below.
Below is the source code for this test — not my original code, obviously, just a diagnostic program to illustrate the problem for this post.
Note: I realize that this diagnostic program doesn't represent a real-world situation — i.e., reading 6 images consecutively like this isn't something you'd often do, at least not without performing other tasks that might trigger garbage collection. Still, it does reflect the memory profile I was seeing in my app.
1: <?xml version="1.0" encoding="utf-8"?>
2: <s:View xmlns:fx="http://ns.adobe.com/mxml/2009"
3: xmlns:s="library://ns.adobe.com/flex/spark"
4: creationComplete="onCreationComplete()">
5:
6: <s:titleContent>
7: <s:Label id="msgText" width="100%"/>
8: </s:titleContent>
9:
10: <fx:Script>
11: <![CDATA[
12: //------------------------------------------------------------------------
13: // This illustrates an issue with loading several large images on a phone
14: // causing a too-large memory footprint which can cause stability problems.
15: // Issue appears to be gc not frequent enough; solution is System.gc call.
16: // But do NOT use System.gc() lightly - see blog post for details.
17: //
18: // When running on PC this loads images from disk via Load Image button.
19: // When running on the phone this loads images through CameraRoll button.
20: //------------------------------------------------------------------------
21:
22: private var cameraRoll:CameraRoll;
23: private var lastLocalImagePosition:Number = 0 ;
24: private var localImages:Array = [
25: "file:///C:/tempImages/camDemoInput2.JPG"
26: , "file:///C:/tempImages/camDemoInput1.JPG"
27: , "file:///C:/tempImages/camDemoInput3.JPG"
28: , "file:///C:/tempImages/camDemoInput5.JPG"
29: ] ;
30:
31:
32: private function onCreationComplete():void
33: {
34: reportMemory() ;
35: if (CameraRoll.supportsBrowseForImage)
36: {
37: cameraRoll = new CameraRoll();
38: cameraRoll.addEventListener(MediaEvent.SELECT,onRollSelect,false,0,true);
39: loadImageButton.enabled = false ;
40: }
41: else
42: cameraRollButton.enabled = false ;
43: }
44:
45: // for non-phone exec we just load images from disk
46: private function loadImageButtonHandler():void
47: {
48: img.source = localImages[lastLocalImagePosition] ;
49: msgText.text = "" ;
50: if (lastLocalImagePosition == localImages.length-1)
51: lastLocalImagePosition=0 ;
52: else
53: lastLocalImagePosition++ ;
54:
55: // next is for PC exec w/Profiler: comment in and you get a nice flatline
56: // peak memory, comment it out and you hit much higher peak memory
57: callLater(forceGC) ;
58: }
59:
60: // for phone execw we load images via CameraRoll
61: private function cameraRollButtonHandler():void
62: {
63: cameraRoll.browseForImage();
64: }
65:
66: private function onRollSelect(event:MediaEvent):void
67: {
68: var media:MediaPromise = event.data;
69: img.source = media.file.url ;
70: //msgText.text = media.file.url ; // + "(" + media.file.size + "kb)" ;
71: msgText.text = "" ;
72: }
73:
74: // This has dramatic effect on memory usage when using many large images,
75: // memory footprint is much lower if you GC whenever you change the image
76: private function forceGC():void
77: {
78: System.gc() ;
79: }
80:
81: private function reportMemory():void
82: {
83: msgText.text = "App memory (MB):"
84: +(System.privateMemory/1000000).toFixed(2);
85: }
86: ]]>
87: </fx:Script>
88:
89: <s:layout>
90: <s:VerticalLayout horizontalAlign="center" verticalAlign="middle" gap="10"/>
91: </s:layout>
92:
93: <s:Image id="img" maxWidth="380" maxHeight="380"/>
94:
95: <s:Button id="loadImageButton" width="330" label="Load disk image"
96: click="loadImageButtonHandler()"/>
97: <s:Button id="cameraRollButton" width="330" label="Load cameraRoll image"
98: click="cameraRollButtonHandler()"/>
99: <s:Button width="330" label="Show memory used" click="reportMemory()"/>
100: <s:Button width="330" label="run system.gc()" click="forceGC()"/>
101:
102: </s:View>
References
Here are some links on memory usage and garbage collection you might find useful. Note the cautions on use of System.gc().
- Grant Skinner presentation on resource management, incl garbage collection
- Some System.gc() and System.totalMemory Tips
- "Kick starting the garbage collector in Actionscript 3 with AIR"
- "Garbage Collection with Flex and Adobe Air"
- Stackoverflow: "Force Garbage Collection in AS3?"
- ...and here is a link which has a collection of more links on the topic.
JavaScript
JavaScript appdev primers
General JavaScript
SPA Demo Application
Backbone Demo Application
Recommended sites
- Addy Osmani's blog
- Derick Bailey's Backbone posts
- Murphey's JQuery Fundamentals (original)
- Crockford's JS videos
- MSDN Project Silk
Flex/AIR
Flex/AIR primers
- Primer on Flex/AIR Multiscreen Development
- Primer on Mobile App Development w/Flex 4.5
- Primer on Flex 3 Component Lifecycle
- Primer on Flexlib MDI
Flex demo apps
all require Flash Player!
AIR mobile dev
- AIR mobile dev Tips
- AIR and Android Back key
- AIR, StageWebView, displaying local content
- AIR for Android memory issue w/large images