A common question in the forums and elsewhere is how to properly and thoroughly clean up scenes when using a scene manager like Storyboard or Director.

You need to understand how modules behave in Lua and how that impacts Storyboard. When you do…

storyboard.gotoScene( "myscene" )

…somewhere in the process the following code executes:

local myscene = require( "myscene" )

In Lua, if you load a module multiple times, only one copy is loaded, and it stays in memory until it’s specifically un-required. This is why we can have persistent values that survive between scenes. Storyboard scenes are modules that have a main chunk and several functions that are called by dispatched events. If your scene module looks like this:

--This is the module's "main chunk".
--Anything created here will persist until the scene is un-required.

--This code will be executed the very first time the module is loaded or
--if the scene is removed with storyboard.removeScene(). Purging the scene
--will not re-trigger any code at this level.

local storyboard = require( "storyboard" )
local scene = storyboard.newScene()

local enemies
local beep

function scene:createScene( event )
   local group = self.view
   --code in here will execute the 1st time
   --or if the scene is "purged" or "removed"
end

function scene:enterScene( event )
   local group = self.view

   --code here executes every time the scene is entered regardless
   --of it's creation state.
end

function scene:exitScene( event )
   local group = self.view

   --code here executes every time someone leaves the scene
end

function scene:destroyScene( event )
   local group = self.view

   --code here only executes if the scene is being purged or removed
end

function scene:overlayEnded( event )
    local group = self.view
end

scene:addEventListener( "createScene", scene )
scene:addEventListener( "enterScene", scene )
scene:addEventListener( "exitScene", scene )
scene:addEventListener( "destroyScene", scene )
scene:addEventListener( "overlayEnded", scene )

return scene

You can see with the comments above which parts of the scene are executed at certain times. It’s important to understand that variables that live in the main chunk are rarely cleared during the execution of your code.

Objects and Groups

A common practice in Storyboard or Director is to organize your display objects — sprites, UI elements, etc. — into display groups that are under the “umbrella” display group for the particular scene. With Storyboard, your code might look like:

function scene:createScene( event )
   local group = scene.view
   local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight )
   group:insert( background )
   local doSomething = display.newImageRect( "button.png", 64, 32 )
   group:insert( doSomething )
end

What’s happening is the display.[API] functions create a table and an allocated chunk of memory associated with it to hold the image to be displayed. This table contains a pointer to that memory as well as function references to things like removeSelf(), setReferencePoint(), etc. (the various methods and properties that make it a display object).

When you call object:removeSelf(), the memory is marked for deletion, and when you nil the variable, the Lua garbage collector will free up the memory in short order — assuming, of course, that you do not have other references to the object locking up the memory chunk!

Both of these scene managers remove their “umbrella display group” when disposing the scene. They do so by calling removeSelf() on the group. This iterates over the objects (and sub-groups of objects) in the group and frees the texture memory. It also sets these to nil so the Lua memory can also be freed. Likewise, any event listener that is “attached” to an object is removed.

However, in our example, the table doSomething still holds references to everything that was the object. That little bit of memory is still allocated, but you do not need to worry about it.

Be Careful…

Local references in functions are nil’ed out when the function ends. That is, they only live for the time that the function is executing. These will be cleaned up for you.

Where you can create a small memory leak is if you do this:

local enemy

local function spawnEnemy()
   enemy = display.newImageRect( "badguy.com", 64, 64 )
   return enemy
end

function scene:createScene( event )

   local group = scene.view
   local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight )
   group:insert( background )
   local doSomething = display.newImageRect( "button.png", 64, 32 )
   group:insert( doSomething )

   for i = 1, 100 do
      group:insert( spawnEnemy() )
   end
end

Similar to the first example, the enemies will all be cleaned up when Storyboard destroys the scene, however the “enemy” variable will stick around until the scene is “un-required.”

The table returned by spawnEnemy() is cleaned up because the group retains a reference to it as a remnant copy of the last enemy allocated. Of course if you re-enter the scene, this block will continue to persist, it probably should get nil‘led out, but its not that much memory and it’s not going to grow.

The Trouble With Global Variables

You frequently hear that global variables are bad. Memory leaks caused by global variables, in particular with a scene manager like Storyboard, are particularly problematic. Consider the code we’re working with:

function scene:createScene( event )
   local group = scene.view
   background = display.newRect( 0, 0, display.contentWidth, display.contentHeight )
   group:insert( background )
   doSomething = display.newImageRect( "button.png", 64, 32 )
   group:insert( doSomething )
end

In this example, we’ve made background and doSomething global by taking the “local” off of them. Similar to what’s happening in the main chunk, if this scene gets re-created, background and doSomething will overwrite their previous table with new tables, and some of that memory can’t be regained. The group still holds a reference to the object so the texture memory will be reclaimed, but the Lua memory will not. If you use similar code in multiple scenes, that background variable will be overwritten multiple times.

What About Event Listeners?

There are two types of event listeners. One type is attached to display objects and the other is attached to the global Runtime.

When you create an object and add a touch handler to it, you’re attaching a block of code via reference to the table. That is, you’re not adding the code to the table, but a pointer to the code. Code is in its own memory and it’s not something you allocate and dispose of. The code persists and there’s only one copy of it.

Once an object is removed, there is no way to trigger events against it, so in effect the event handlers are removed for you. You do not have to remove these if they are attached to an object that’s in a scene-managed group.

Runtime listeners, on the other hand, must be removed because they’ll continue to run after the scene is taken out of memory and the functions that are executing are likely going to reference an object that is no longer around. This can cause your app to crash.

Audio, Transitions, and Timers

Audio needs special handling with Storyboard and other scene managers.  You are responsible for freeing up the memory for audio with the audio.dispose() API call.  You have three options to load sound within your Storyboard scenes.

  1. In the main chunk.
  2. In the scene:createScene() phase.
  3. In the scene:enterScene() phase.

Of course, you could also load them in their own module or in main.lua. Each of the three methods have various consequences that you need to be aware of:

1. Loading in the main chunk:

Since this will likely execute just once, if you do audio.loadSound() or audio.loadStream() calls here, you can’t really dispose of them in the exitScene() phase or the destroyScene() phase since calling the scene again won’t execute the main chunk. As such, there’s no good way to know when the scene is being un-required, so you can dispose of the sounds in advance.

2. Loading in createScene():

This is a logical place to set up sounds and audio streams. Just remember that if you load them here, you must dispose of them in the destroyScene() function. The createScene() and destroyScene() events go together as a pair. If a scene gets purged or removed, destroyScene() is called, and the next time the scene is entered, createScene() is called again to re-create all the assets.

3. Loading in enterScene():

Since enterScene() can be called multiple times without createScene() getting called, you need to dispose of the audio in enterScene()‘s partner, exitScene(). This keeps your audio.loadSound and audio.dispose() sounds balanced.

Transitions must also be cancelled, since clearing a scene (and its objects) that contain “unresolved” transitions can cause the transitions to get stuck in memory. If you only have a few transitions isolated to a few objects, a convenient method to cancel these is to attach them as a property of the object itself. Then, when you clear the scene, you simply check if the object has a transition on it — if so, cancel it and nil out the object.

local player = display.newImage( "myPlayer.png" )
player.moveTrans = transition.to( player, { ... } )

--on scene clear
if ( player.moveTrans ) then
   transition.cancel( player.moveTrans )
   player.moveTrans = nil
end

Finally, timers must be manually removed as well. If, upon completion, a timer references object(s) that are no longer in existence because they were cleared with the scene’s display group, an error will result.

“Memory Creep”

Monitoring memory can be a tricky business. One useful method is to run a memory-checking function on a repeating timer as follows (you can comment out the timer which triggers it when you don’t want to test):

local function checkMemory()
   collectgarbage( "collect" )
   local memUsage_str = string.format( "MEMORY = %.3f KB", collectgarbage( "count" ) )
   print( memUsage_str, "TEXTURE = "..(system.getInfo("textureMemoryUsed") / (1024 * 1024) ) )
end
timer.performWithDelay( 1000, checkMemory, 0 )

IMPORTANT: With this function, the important thing to watch for is “memory creep.” That is, a steadily-increasing memory footprint that continues to climb (creep) upward as you navigate from scene to scene, back to the original scene, etc. Even if you move among three scenes as in A → B → C → A → B → C, memory will not always return to the exact same value it was when you started in scene A and then later return to scene A. This is not necessarily reason for concern. More concerning is if the memory continues to climb steadily as you navigate back to the same scene time after time after time. In that case, you most likely have a memory leak and you should resolve where it’s coming from!

In Summary…

This tutorial is a quick summary of memory cleanup practices, especially when using a scene manager. Proper memory management and cleanup is one of the keystones of app development, and we can’t emphasize enough that this crucial task should be treated with great respect from the very start, up to the final line in your code.

  1. Really nice summary of the subject. I’m getting ready to do a new series of videos on Storyboard and had been scouring the docs and forum to make sure I didn’t forget anything — and here you’ve collected most of the really important things in one spot! :)

  2. I’ve developed a habit to “forward declare” most variables and functions as local before createScene, then when I declare them in createScene or enterScene I declare them as globals. Am I walking a slippery slope?

    • Hi Opus…

      If you define them as “local” at the top of the scene, then just reference them inside of createScene/enterScene, they are still local variables. They are local to that module. For a variable to be global, you would not put “local” on it anywhere.

      It doesn’t sound like a slippery slope to me, just be aware that when you do a storyboard.purgeScene() that those local variables outside of any function may be holding on to object and table references that storyboard won’t clean up on its own.

      • After reading through a few of these storyboard tutorials I’m still a bit confused as to proper/necessary practice (even though this should all be very simple). I have the following questions:

        – is it best to forward declare all objects one will create in createScene and/or enterScene, or just declare them ‘local’ when created?

        – if declaring ‘local’ in the following bare bones createScene function, do I need to worry about deleting the ‘background’ object in the destroyScene function and if so how how should I reference it?:

        function scene:createScene( event )
        local scene_view = self.view
        local background = display.newImageRect( scene_view, ‘bg_img.png’, app_settings.image_dim.x, app_settings.image_dim.y )
        end

  3. this is indeed a good teach.. one thing i’ve been trying to understand and cant seem to find an answer anywhere is what does the statement
    local group = self.view
    do. I understand the ‘local group’ part of it but ‘self.view’ ???
    why ‘self’ ???
    and why is that defined in all/most scene functions ?

    thanks

  4. @ edualc because
    scene={}
    is a table which has many functions attached to it like createScene() etc.
    scene table also has a member as group
    scene.group –it is a display group.
    when you add display objects, your are actually adding then into scene.group
    so that when your scene get purged or removed, storyboard module simply remove
    scene.group which is actually your main group of the module.

    so when in any storyboard module you write

    scene:addEventListener( “createScene”, scene )

    since we used : colon it pass self as it’s first parameter to createScene()
    so in self we get reference to scene and we want to reach till scene.group
    so we write self.view which return us reference to main group.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>