Josh Tynjala and I were speaking today, well actually typing over IM. We were discussing a problem we both ran into recently, the fl.transitions.Tween class was not dispatching events consistently. Sometimes the TweenEvent.MOTION_FINISH event would be broadcasted, other times it would not. Sometimes our programmatic tweens were visually completing, other times they would not, in a few cases they wouldn’t even start. After a little debugging we realized that the AS3 Garbage Collection was killing the tweens. You may be asking why would the garbage collection be doing this? Let’s start out by looking at some example code.
var len:int = _model.getNumberOfItems();
for (var i:int = 0;i<len;i++)>
var clip:MovieClip = _scope.getChildByName('button'+i);
var myTween:Tween = new Tween(clip, 'alpha', Regular.easeOut, 0, 1, 0.5, true);
myTween.addEventListener(TweenEvent.MOTION_FINISH, buttonAnimationComplete);
}
Pretty simple code, in my AS2 days I used code similar to this a million times and never had a problem. All I am doing is looping through a set number of MovieClips and fading them up from 0 to 100%. Most times this worked fine, but a few times the tweens never completed and buttonAnimationComplete was not called. This is because the AS3 Garbage Collection was deleting the Tweens just like it should be. What? The tweens should be deleted? Yes.
Notice that I am storing the reference to my tween in a local variable that gets overridden in every iteration of the loop. This means there is no real reference to the object and when there is no reference to an object it is flagged for removal by the garbage collector. Since there is no real way to know when the garbage collection cycle is going to run the tweens were completing some of the time and other times were being thrown into the back of the big garbage truck that slowly creeps around your code.
The best way to ensure that this doesn’t happen is to store your reference in a class level member. I ended up storing all of my tween references in a class level array. Once all of the tweens were complete I cleared the array. Here is the updated code that I used, the entire class is not shown here, trust me your scroller will thank me.
private var _buttonTweens:Array = new Array();
private var _buttonTweenCompleteCount:int;
private function animateButtons():void {
var len:int = _model.getNumberOfItems();
for (var i:int = 0;i<len;i++)>
_buttonTweens.push(new Tween(clip, 'alpha', Regular.easeOut, 0, 1, 0.5, true));
_buttonTweens[_buttonTweens.length - 1].addEventListener(TweenEvent.MOTION_FINISH, buttonAnimationComplete);
}
}
private function buttonAnimationComplete(e:TweenEvent):void {
buttonTweenCompleteCount++
if (buttonTweenCompleteCount == _model.getNumberOfItems()) {
_buttonTweens = [];
dispatchEvent(new Event('buttonsReady'));
}
}
I am just using Tweens as an example, but you can run into this same issue with any object that has local references that are overridden. For example, I had a Timer event set to iterate every 100 milliseconds and it called loadAsset(), a method that did just what it says, it loaded an asset.
private function loadAsset():void {
_currentItemToLoad++;
var myLoader:Loader = new Loader();
myLoader.addEventListener(Event.COMPLETE, assetLoaded);
myLoader.load(new URLRequest('images/galleryImage' + currentItemToLoad + '.jpg'));
}
private function assetLoaded(e:Event):void {
var holder:Sprite = new Sprite();
holder.visible = false;
holder.addChild(e.target.content);
_scope.addChild(holder);
e.target.removeEventListener(Event.COMPLETE, assetLoaded);
}
Just like the Tween example above my Event.COMPLETE event was not firing all the time. This is because I was storing the reference to the Loader in a local variable that was overridden each time the loadAsset method was called. To get around this I stored all my references in a class level member (an object or an array work nicely).
Hopefully this helps you figure out why you are not seeing certain events in your code, it had me stumped for a bit. Now you know, and as G.I. Joe said “knowing is half the battle”.
Tags: actionscript 3.0, Flash, Flex, Garbage Collection
November 19th, 2007 at 1:36 am
You’re almost correct about the references I think. It’s quite clear that the GC is running and is causing the issues, but you DO have references to the objects because of the event listeners. However, since the GC is running I’m assuming that these are weak references which I presume means that the GC is going to grab them anyway.
Please do correct me if I’m wrong, I’m very interested in more info about the GC and how it works.
November 19th, 2007 at 3:40 am
Hey - I’ve had this problem as well. Got around it using class members, but that can be quite messy on big projects. I really should use something other than the fl.* classes. Tried any good AS3 tweening engines?
November 19th, 2007 at 5:22 am
I ran into that issue on the first Flash9 / AS3 project I worked. Got so fed up I emailed Robert Penner, and he suggested that might be the problem.
November 19th, 2007 at 11:14 am
Marcus, you DON’T have references to your Tweens if you add event listeners, actually. The Tween will hold a reference to any listener it has, but that doesn’t mean that the listener is holding a reference to the Tween, which is the important detail here. In the eyes of the garbage collector, there’s no Tween reference once the local variable goes out of scope, and it’s safe to destroy.
Jensa, I recommend Tweener.
November 19th, 2007 at 11:18 am
I second Tweener.
November 20th, 2007 at 10:37 am
I’m with Marcus. In my experience, adding a strong referenced eventListener does not allow an object to be garbage collected. When the GC is doing reference counting, the strong listener is the definition of what will keep an object from being GCed.
Try out the following code. I would like to know why the local var objects are never GCed if the GC acts as claimed above.
package {
import flash.events.EventDispatcher;
import flash.utils.Timer;
import flash.events.TimerEvent;
public class Actor extends EventDispatcher {
public var id:uint;
public function Actor( id:uint, timer:Timer ):void {
this.id = id;
// Add strong reference eventListener
timer.addEventListener(TimerEvent.TIMER, onTimer);
}
public function onTimer( evt:TimerEvent ):void {
trace(”I am actor “+id);
}
}
}
November 20th, 2007 at 10:38 am
Looks like my App got cut off, here it is:
November 20th, 2007 at 10:38 am
Ok, so it doesn’t like the XML tag.
November 20th, 2007 at 10:40 am
Ok, this is working poorly. Make a new Flex Applicaiton. Put
creationComplete=”init()”
in the App tag.
Throw this is a Script tag:
import Actor;
public var actorCounter:uint;
public var timer:Timer;
public function init():void {
trace(”init()”);
timer = new Timer(100);
timer.addEventListener(TimerEvent.TIMER, onTimer);
timer.start();
}
public function createActor():void {
trace(”createActor()”);
var actor:Actor = new Actor(actorCounter++, timer);
}
public function onTimer( evt:TimerEvent ):void {
trace(”onTimer()”);
createActor();
txtArea.text += “Created Actor “+actorCounter+”\n”;
}
and put this on the stage of a new Application:
November 20th, 2007 at 11:11 am
If you were listening to Actor for an event to be dispatched then you would run into problems.
var actor:Actor = new Actor(actorCounter++, timer);
actor.addEventListener(’actorOnStage’, actorOnStage);
Since actor is in the local scope of the createActor method it would be over written every 100 milliseconds and therefore the listener would get squashed next garbage collection cycle. If the event actorOnStage was broadcast right away there is a very good chance it would never get squashed, however if there was some asynchronous activity (tweens, data/asset loading, etc) there is a good chance the listeners would lose reference while the class waits to dispatch the event.
November 20th, 2007 at 12:57 pm
Ok, I think I understand the root of the problem you’re getting at now. It’s a pretty simple issue.
The problem of not catching the events is not “garbage collection,” it is that the events you’re watching for are firing before you addEventListener() to catch them. There is no guarantee that you should be able to catch a Event.COMPLETE on a loader or a TweenEvent.MOTION_FINISH after setting the processor to create that event into action previously. If you really want to catch the events, the objects to be watched needs the event listeners added before you tell them “go!”
I’m firmly convinced your work around is just getting lucky catching the events, perhaps because it takes longer to process assignments/instantiation into class memory than local.
To really solve your Tween problem, you should have a handler for the clip ending on the listener argument object passed to the Tween on creation. That is
var t:Tween = new Tween(listener…)
in listener class:
public var onTweenEnd():void { // handle it! }
Check the livedocs on the Tween class for details.
It seems the solution is built right into the Flex architecture and is not being used. If all your programmatic Tweens are based on event catching, check an see if this solution doesn’t fix the issue.
November 20th, 2007 at 1:11 pm
I should go further and say the local memory space that is being “overwritten” in the next round of the loop is just a pointer. It points to where the real object lives out in heap memory. You are not overwriting something on the heap when the next round of the loop runs. That’s what the GC is there for to begin with.
Check out this variation on the last program.
class Actor
package {
import flash.events.EventDispatcher;
import flash.utils.Timer;
import flash.events.TimerEvent;
public class Actor extends EventDispatcher {
public var id:uint;
public var timer:Timer;
public function Actor( id:uint ):void {
this.id = id;
timer = new Timer(1000);
// Add strong reference eventListener
timer.addEventListener(TimerEvent.TIMER, onTimer);
timer.start();
}
public function onTimer( evt:TimerEvent ):void {
trace(”I am actor “+id);
dispatchEvent( evt );
}
}
}
Same as before: make a new Flex project. Add a creationComplete listener that calls init(), add a TextArea with id=”txtArea”, but this code in the script block.
import Actor;
public var actorCounter:uint;
public var timer:Timer;
public function init():void {
trace(”init()”);
timer = new Timer(1000);
timer.addEventListener(TimerEvent.TIMER, onTimer);
timer.start();
}
public function createActor():void {
trace(”createActor()”);
var actor:Actor = new Actor(actorCounter++);
actor.addEventListener(TimerEvent.TIMER, actorCallback);
}
public function onTimer( evt:TimerEvent ):void {
trace(”onTimer()”);
createActor();
txtArea.text += “Created Actor “+actorCounter+”\n”;
}
public function actorCallback( evt:TimerEvent ):void {
trace(”Actor “+evt.target.id+” still alive and well!”);
}
November 30th, 2007 at 1:17 pm
user411, I think you’re talking about mx.effects.Tween and the author was talking about fl.transitions.Tween for Flash CS3. They are different.
I tried your last example, but I think actors don’t die because they are referenced by their own timer instances, which in turn are referenced by something! Take a look at the sample below. Locally created Timer instance is never GCed even though there is no reference to it from my code.
package {
import flash.display.Sprite;
import flash.events.TimerEvent
import flash.system.System;
import flash.utils.Timer;
public class Main extends Sprite {
private var sprite:Sprite;
public function Main():void {
sprite = new Sprite();
sprite.graphics.beginFill(0xff0000);
sprite.graphics.drawRect(0, -5, 100, 5);
sprite.x = 150;
sprite.y = 150;
addChild(sprite);
var timer:Timer = new Timer(1000);
timer.addEventListener(TimerEvent.TIMER, onTimer, false, 0, true); // weak reference
timer.start();
timer = null; // clear local reference to timer
}
public function onTimer(event:TimerEvent):void {
sprite.rotation += 10;
tryGC();
}
public function tryGC():void {
trace(”before: ” + String(System.totalMemory));
var array:Array = new Array();
for (var i:uint = 0; i < 100000; i++) {
array.push(”abcdefghijklmnopqrstuvwxyz”);
}
trace(”after: ” + String(System.totalMemory));
}
}
}
November 30th, 2007 at 1:27 pm
Some guy reported the same problem (Tween ending early) at his blog:
http://www.imajuk.com/blog/archives/2007/11/as3fltransitionstween_1.html
(sorry in japanese)
According to him, Tween is a listener for enterFrame event. In AS2, Tween is a listener by a strong reference, but in AS3 by a weak reference. This is why Tween can be GCed in AS3, but not in AS2.
Can someone confirm this?
December 15th, 2007 at 2:26 pm
Josh: Thanks for clarifying that bit!
Jensa: I’d actually like to throw a punch for TweenLite. It’s very fast and lightweight, as well as incredibly easy to use. We’ve been using it for a while now.
January 26th, 2008 at 5:51 am
Thanks! This helped me out =)
February 13th, 2008 at 11:51 am
It’s not just Tweens either.
February 18th, 2008 at 3:58 am
This works for me. I also briefly checked the GC and it seems to work (memory doesn’t sky rocket).
My anti garbage collection solution to keep Tweens until they are finished:
//Must be global - keep me
var antiGC:Dictionary = new Dictionary(false);
…
//in a function or whatever (b is an object) - I will finish!
var t:Tween = new Tween(b,”y” , Elastic.easeOut, b.y, b.y+200, 50);
t.addEventListener(TweenEvent.MOTION_FINISH, tweenFinished);
//Store
antiGC[t] = t;
..
// Manually allow GC - save your memory
function tweenFinished(e:TweenEvent){
//Remove
antiGC[e.currentTarget] = null;
delete antiGC[e.currentTarget];
}
Comments welcome