Creating inline function after loading an image causes memory leak

170 Views Asked by At

I was analyzing an unexpected memory leak in our game project and found some strange results. I am profiling using Adobe Scout and eliminated all other factors like starling, texture or our loading library. I reduced the code to simply load a png and immediately allocate an empty inline function on its complete event.

Loading a png allocates image on default and if you do nothing after loading gc clears that image. But creating an inline function seems to prevent that image to be garbage collected somehow. My test code is;

public class Main extends Sprite 
{
    private var _callbacks:Array = new Array();

    public function Main() 
    {
        load("map.png", onPngLoaded);
    }

    private function onPngLoaded(bitmap:Bitmap):void 
    {
        _callbacks.push(function():void { });
    }

    public function load(url:String, onLoaded:Function):void 
    {
        var loader:Loader = new Loader;

        var completeHandler:Function = function(e:Event):void {
            loader.contentLoaderInfo.removeEventListener(Event.COMPLETE, completeHandler);
            onLoaded(loader.content);
        }

        loader.contentLoaderInfo.addEventListener(Event.COMPLETE, completeHandler);

        loader.load(new URLRequest(url));   
    }
}

If you remove the code which creates an inline function;

    private function onPngLoaded(bitmap:Bitmap):void 
    {
        // removed the code here!
    }

gc works and clears the image from memory.

Since having no logical explanation for this, I suspect of a flash / as3 bug. I will be glad to hear any comments who tests my code and gets the same results.

Note: To test, replace the main class of an empty as3 project with my code and import packages. You can load any png. I am using flashdevelop, flex-sdk 4.6.0 and flash player 14.

2

There are 2 best solutions below

3
On BEST ANSWER

When you create an inline function, all local variables get stored with it in the global scope. So in this case, that would include the bitmap parameter.

For more information, see this: http://help.adobe.com/en_US/ActionScript/3.0_ProgrammingAS3/WS5b3ccc516d4fbf351e63e3d118a9b90204-7f54.html

Here is the relevant part:

Any time a function begins execution, a number of objects and properties are created. First, a special object called an activation object is created that stores the parameters and any local variables or functions declared in the function body....Second, a scope chain is created that contains an ordered list of objects that Flash Player or Adobe AIR checks for identifier declarations. Every function that executes has a scope chain that is stored in an internal property. For a nested function, the scope chain starts with its own activation object, followed by its parent function’s activation object. The chain continues in this manner until it reaches the global object.

This is another reason why inline/anonymous functions are best avoided in most situations.

1
On

So using asc2, Flash/Air 19 : Yes I get the same results that you are seeing, but due to the anonymous function holding global references I expected that (like my original comment stated).

I rewrote it in my style based upon Adobe's GC technical articles and bulletins and no leaks are seen as all the global references are removed.

A cut/paste AIR example:

package {

    import flash.events.MouseEvent;
    import flash.text.TextField;
    import flash.display.Sprite;
    import flash.display.Bitmap;
    import flash.display.Loader;
    import flash.events.Event;
    import flash.net.URLRequest;
    import flash.system.System;
    import flash.utils.Timer;
    import flash.events.TimerEvent;

    public class Main extends Sprite {
        var timer:Timer;
        var button:CustomSimpleButton;
        var currentMemory:TextField;
        var highMemory:TextField;
        var hi:Number;

        var _callbacks:Array = new Array();

        public function Main() {
            button = new CustomSimpleButton();
            button.addEventListener(MouseEvent.CLICK, onClickButton);
            addChild(button);
            currentMemory = new TextField();
            hi = System.privateMemory;
            currentMemory.text = "c: " + hi.toString();
            currentMemory.x = 100;
            addChild(currentMemory);
            highMemory = new TextField();
            highMemory.text = "h: " + hi.toString();
            highMemory.x = 200;
            addChild(highMemory);
            timer = new Timer(100, 1);
            timer.addEventListener(TimerEvent.TIMER_COMPLETE, timerHandler);
            timer.start();
        }

        function timerHandler(e:TimerEvent):void{
            System.pauseForGCIfCollectionImminent(.25);
            currentMemory.text = "c: " + System.privateMemory.toString();
            hi = System.privateMemory > hi ? System.privateMemory : hi;
            highMemory.text = "h: " + hi.toString();
            timer.start();
        }

        function onClickButton(event:MouseEvent):void {
            for (var i:uint = 0; i<100; i++) {
                //load("foobar.png", onPngLoaded);
                load2("foobar.png");
            }
        }

        private function onPngLoaded2(bitmap:Bitmap):void {
            var foobarBitMap:Bitmap = bitmap; // assuming you are doing something
            foobarBitMap.smoothing = false;   // with the bitmap...
            callBacks(); // not sure what you are actually doing with this
        }
        private function callBacks():void {
            _callbacks.push(function ():void {
            });
        }

        public function completeHandler2(e:Event):void {
            var target:Loader = e.currentTarget.loader as Loader;
            // create a new bitmap based what is in the loader so the loader has not refs after method exits
            var localBitmap:Bitmap = new Bitmap((target.content as Bitmap).bitmapData);
            onPngLoaded2(localBitmap);
        }

        public function load2(url:String):void {
            var loader2:Loader = new Loader;
            loader2.contentLoaderInfo.addEventListener(Event.COMPLETE, completeHandler2, false, 0, true);
            loader2.load(new URLRequest(url));
        }
    }
}

import flash.display.Shape;
import flash.display.SimpleButton;

class CustomSimpleButton extends SimpleButton {
    private var upColor:uint   = 0xFFCC00;
    private var overColor:uint = 0xCCFF00;
    private var downColor:uint = 0x00CCFF;
    private var size:uint      = 80;

    public function CustomSimpleButton() {
        downState      = new ButtonDisplayState(downColor, size);
        overState      = new ButtonDisplayState(overColor, size);
        upState        = new ButtonDisplayState(upColor, size);
        hitTestState   = new ButtonDisplayState(upColor, size * 2);
        hitTestState.x = -(size / 4);
        hitTestState.y = hitTestState.x;
        useHandCursor  = true;
    }
}

class ButtonDisplayState extends Shape {
    private var bgColor:uint;
    private var size:uint;

    public function ButtonDisplayState(bgColor:uint, size:uint) {
        this.bgColor = bgColor;
        this.size    = size;
        draw();
    }

    private function draw():void {
        graphics.beginFill(bgColor);
        graphics.drawRect(0, 0, size, size);
        graphics.endFill();
    }
}