Scott Morgan

Calgary Flash and Flex Developer

Nov

18

AS3 Garbage Collection, the reason your tweens are ending early.

By scott

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”.

42 Responses so far

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.

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?

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.

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.

I second Tweener.

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

Looks like my App got cut off, here it is:

Ok, so it doesn’t like the XML tag.

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:

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.

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.

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

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

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?

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.

Thanks! This helped me out =)

It’s not just Tweens either.

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

I know this is a bit after the fact, but thanks for posting this article. I had been banging my head against the keyboard for a while now trying to track down why my tweens were not finishing. And then I finally dug this article up.

This article ended up leading me to abandon Adobe’s tween engine and go with TweenLite instead, which I have to say - aside from solving my incomplete tween problem - is just easier to work with all around.

Thanks again.

I can confirm this (mis)behavior in Flex 3 using mx.effects.

It worked, after a week of head-banging over the walls, this helped me :) thanks!

[...] 之前的AS3开发中,我一直用的Adobe官方的fl.transitions.Tween,虽然不怎么好用但是基本功能能满足。但是最近做了一个项目中中,碰到了一个问题,很多tween同时执行时,偶尔会出现所有动画同时半途停止的现象。google了一下英文资料,发现问题出在“AS3的垃圾回收器把tween对象给回收了”。匪夷所思?看看这里的解释和解决办法。 [...]

Great post, I would’ve never fixed the way I did Tweens in AS2 and just assumed I was doing something wrong.

Mate! that saved my bacon your a STAR

I was thinking I had a scope issue when my tweens worked great locally but when loaded into my Flex application they would work spoty at best.

Thanks for the great post!

[...] 见AS3 Garbage Collection, the reason your tweens are ending early. [...]

Thanks Kevin (post #18). You solution fixed my tween problem.

Saeid

I didn’t get a chance to read all the responses to this post, but I was having the same issue with tweens getting cut off and I chalked it up to Garbage Collection as well. The solution I found was to create your variables for the tweens outside of the function, so they are in the global scope, then add the tween after when needed.

var movement:Tween;

function yourFunction(event:Event):void
{
movement = new Tween(yourMC, ….
}

This way when the garbage is taken out, it only takes the actual tween, and not the variable.

[...] I used a method found here to force the Tween to fully [...]

Thanks for the post, it solved a problem for me. If your ever in the philly area , drinks on me boss

Using this post as a guide, I created an object to hold all of my tweens, and I keyed everything as such: instanceName_property_finalValue. This way, I am able to store unique references to all of my tweens, and in case a tween was called twice, I am not storing multiple references (as you might with push()). Can anybody offer any cons to the method I have chosen… aside from the fact that I am going to have a rather large object after 1000+ unique tweens. Thanks.

[...] I used a method found here to force the Tween to fully [...]

Didn’t read all the comments, but isn’t it a lot easier just to declare the tween variable outside the function!?
Works fine for me:
http://www.actionscript.org/forums/showpost.php3?p=830593&postcount=5

I know this is the solution to my problem, a class variable should stop overwriting the loader. What I don’t understand is how to pass the right part of the class variable based on the event down in the finishLoadImage function. (Hoping this little extraction makes sense.)

The problem I have is that if I roll in and out too fast the movieclip with the image is getting deleted before it is finished loading. I sure could use a little advice on how to proceed. Thanks! And thanks for this great explanation.

function rollover(e:Mousevent)
{
loadImage(ID,…);
}
function loadImage(ID,…)
{
var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE,finishLoadImage);
loader.load(new URLRequest(path));
//etc
}

function finishLoadImage(e:Event)
{
var IDhere = event.target.loader.parent.name;
//positions image
}

thanks. u helped me a lot! b the way: great written article

Great thanks to you sir. Was stumped as to why my tweens were finishing early. Eliminating the garbage collection issue has eradicated the problem :)

Thanks Scott for your idea.
Thanks Kevin for your solution.

I repackaged as a class with static methods so you can just call

TweenKeeper.keep(new Tween(…))

from everywhere, without worries. I still wonder why AS3 isn’t taking care of this, or why AS3 help/examples on the Tween class, all use it the wrong way.

// Usage: TweenKeeper.keep(new Tween(this,”shownPerc”,Regular.easeInOut,this.shownPerc,1.0,40,false));

package {
import fl.transitions.Tween;
import fl.transitions.TweenEvent;
import flash.utils.Dictionary;

public class TweenKeeper {
protected static var antiGC:Dictionary = new Dictionary(false);

public static function keep(t:Tween) {
t.addEventListener(TweenEvent.MOTION_FINISH, tweenFinished);
antiGC[t] = t;
}
static function tweenFinished(e:TweenEvent){
antiGC[e.currentTarget] = null;
delete antiGC[e.currentTarget];
}
}
}

Just forgot to say I still like other cleaner solutions better than keeping track of all tweens, with this class.

For instance saving the tweens inside the instances you are moving.
Just as an example (untested code):

public class MoveMe extends MovieClip {
public var myTween1;

}
var aMoveMeInst:MoveMe=new MoveMe()

aMoveMeInst.myTween1=new Tween(aMoveMeInst,…

This way, if you have 100 sprites, each will keep its own Tween (or tweens if more) untill the sprite itself gets GC.

Thanks again,

Francesco

i think you should update your post. ’cause Matty’s right. You just need to create an var foo:Tween; global. Much quicker than your posted Workaround. And not everybody is going to read all the comments.

Thanks! This tween thing nearly drove me crazy.

Thank you.

Leave a comment