アクティベーションオブジェクトによるメモリリーク

3回シリーズの最後です。
ようやく本題に入ります。

  1. AS3のクロージャ
  2. アクティベーションオブジェクトとスコープチェーン
  3. アクティベーションオブジェクトによるメモリリーク

エントリーの初回で、クロージャには、「メソッドクロージャ」と「関数クロージャ」があると書きましたが、
ちまたで(特にAS以外の言語で)クロージャと言えば、ASで言うところの関数クロージャで、メソッドクロージャと言うのはほぼAS独自の概念のようです。便利ですけどね。

今回はまず、関数クロージャの使いどころを例に挙げ、
その後に、関数クロージャの罠へと話を持っていきたいと思います。

関数クロージャによるローカル変数のカプセル化

よく関数クロージャの使用例として出てくるのが、関数クロージャを利用した「ローカル変数のカプセル化」です。
関数クロージャの実行前後に生成される、アクティベーションオブジェクトとスコープチューンを利用すると、
関数クロージャにローカル変数をカプセル化することができます。

例を挙げます。

package
{
    public class ClosureTest extends Sprite
    {
        public function ClosureTest()
        {
            //関数クロージャを取得
            var closure:Function = getClosure();
            //関数クロージャを実行
            trace(closure());
            trace(closure());
            trace(closure());    
        }
        private function getClosure():Function
        {
            var n:int = 0;
            //関数クロージャを生成して返す
            return  function() : int
                    {
                        return ++ n;
                    }
        }
    }
}

実行結果は

// output
// 1
// 2
// 3

メソッド’getClosure’のローカル変数’n’に注目します。
通常この変数’n’はローカル変数ですので、このメソッド内でしかアクセスできません。
また、メソッドを抜けると破棄されるはずです。
しかし、実行結果を見ると破棄されている様子はなくしっかりと出力されています。

何が起こっているかみてみます。
前回のエントリーと同じ表現で、コード中にスコープチェーンを書き入れてみると、以下のようになります。

package
{
    public class ClosureTest extends Sprite
    {
        public function ClosureTest()
        {
            //関数クロージャを取得
            var closure:Function = getClosure();
            //関数クロージャを実行
            trace(closure());
            trace(closure());
            trace(closure());    
        }
        private function getClosure():Function
        {
            /**
             * メソッド'getClosure'のスコープチェーン
             * [@ getClosure{n:0} - #ClosureTest]
             */
            var n:int = 0;
            //関数クロージャを生成して返す
            return  function() : int
                    {
                        /**
                          * 関数クロージャのスコープチェーン
                         * [@anonymous{} - @ getClosure{n:0} - #ClosureTest]
                         */
                        return ++ n;
                    }
        }
    }
}

関数クロージャは自身のスコープチェーンで、識別子’n’を解決します。
スコープチェーンの先頭は、自身のアクティベーションオブジェクトです。
この中にはプロパティnはありません。
なので、スコープチェーンの次のオブジェクトを探します。
次のオブジェクトは@ getClosure{n:0} で、メソッド’getClosure’のアクティベーションオブジェクトです。
このアクティベーションオブジェクトは、関数クロージャの実行時に関数クロージャのスコープチェーンに追加されたものです。(前回のエントリー参照)
このオブジェクトのプロパティにnが存在するので関数クロージャはこのnの値を使用します。

こうやって、呼び出しもとの親関数のローカル変数を、関数クロージャはまるで自分の持ち物のように扱えます。
しかもこの関数クロージャのスコープチェーンに保存された’n’は、もとの持ち主である親関数ですらアクセスする手段を持ちません。
なぜなら、メソッド’getClosure’のスコープチェーン はメソッドを抜けると破棄されるからです。

この、アクティベーションオブジェクトとスコープチェーンを利用したオブジェクトの保存方法を、いわゆる「クロージャによる変数のカプセル化」などと言います。

その他のクロージャの使いどころ

他には、「遅延評価」というものがあるようですが、今回は触れません。
興味のある方は以下を。ASでも実装できます。
参考:http://blog.livedoor.jp/dankogai/archives/50996734.html

イベントリスナに関数クロージャを利用する

イベントリスナを関数クロージャにすると、以下のようなことができます。

package
{
    public class ClosureTest2 extends Sprite
    {
        public function ClosureTest2()
        {
            initialize("DEFAULT");
        }
        private function initialize(execMode:String)
        {
            var m:String = execMode;
            stage.addEventListener( MouseEvent.CLICK, 
                                    function(e:MouseEvent)
                                    {
                                        trace(m);
                                    });
        }
        
    }
}

メソッド’initialze’に文字列”DEFAULT”を引数として渡しています。
(テストのためコンストラクタで呼び出していますが、実際は他のクラスから呼び出されると思って下さい。)
ステージがクリックされた時、この渡した文字列を出力したいとします。

通常このような非同期処理をする場合は、インスタンス変数として”DEFAULT”を保存すると思います。
しかし、この”DEFAULT”が、ステージをクリックされた時だけ使用される場合、
わざわざそのためにインスタンス変数を用意するのもためらわれます。
(この考えは間違っているのかもしれませんが、多すぎるインスタンス変数が僕は好きじゃありません。)
そこで、上記のように参照をカプセル化し、必要となる時までお手軽に保存することができます。

ちなみに、アクティベーションオブジェクトは、ローカル変数だけじゃなく「関数に渡された引数」も含まれるので、
実は、引数をローカル変数に一度保存する必要はありません。

package
{
    public class ClosureTest2 extends Sprite
    {
        public function ClosureTest2()
        {
            initialize("DEFAULT");
        }
        private function initialize(execMode:String)
        {
            stage.addEventListener( MouseEvent.CLICK, 
                                    function(e:MouseEvent)
                                    {
                                        /**
                                         * スコープチェーン
                                         * [@anonymous{} - @initialize{execMode:"DEFAULT"}]
                                         */
                                        trace(execMode);
                                    });
        }
        
    }
}

これでも動作します。
このテクニックは、結構応用しがいがあると思いませんか?

ただ、気をつけなければいけないことがあります。
今回のケースでは、関数クロージャはStageからリスナとして参照されます。
なので、関数クロージャと関数クロージャにカプセル化されているString”DEFAULT”を破棄する(ガベージコレクトさせる)ためには、
removeEventListener()する必要があります。
(addEventListener時に弱い参照を使う方法は、基本的に無効です。弱い参照だと、関数クロージャはどこからも参照されず、すぐにガベージコレクトされるからです。
それを回避するために、インスタンス変数に関数クロージャを保存するくらいなら、素直にメソッドクロージャを使った方がいいと思います。)

以下の例では、trace()を実行した後、自分自身(関数クロージャ)をStageのリスナーから削除します。
(arguments.calleは実行中の関数を返します)

package
{
    public class ClosureTest2 extends Sprite
    {
        public function ClosureTest2()
        {
            initialize("DEFAULT");
        }
        private function initialize(execMode:String)
        {
            stage.addEventListener( MouseEvent.CLICK, 
                                    function(e:MouseEvent)
                                    {
                                        trace(execMode);
                                        stage.removeEventListener(MouseEvent.CLICK, arguments.callee);
                                    });
        }
        
    }
}

このケースはごく簡単なサンプルなので、これで問題ないように見えますが、
事実上、イベントが発生したときにしかremoveListenerするチャンスがない以上、一定のリスクが存在します。
カプセル化するオブジェクトの破棄を、シビアにとらえなければいけない局面ではこの方法はお勧めしません。
何らかの理由でイベントのディスパッチが失敗すると、カプセル化したオブジェクトを破棄する手段(とアクセスする手段)がなくなるかもしれないからです。

アクティベーションオブジェクトによる意図しないメモリリーク

3回に分けて長々とエントリーを書きましたが、やっと佳境に入ってきました。
基本的な仕組みの説明が全て終わったので、実際にやってしまいがちなメモリリークするコード例を紹介します。

わりと複雑なアプリケーションを作っているとします。
MyApplicationは、クライアントクラスから初期化されるとします。

package
{
    public class MyApplication extends Sprite
    {
        public function MyApplication()
        {
        }
        
        public function initialize(mode:String) : void
        {
            /**
             * このアプリケーションは、ステージのクリックを検知し、開始する。
             */ 
            stage.addEventListener( MouseEvent.CLICK,
                                    function(e:Event)
                                     {
                                         start( mode );    
                                     });
        }        
        
        /**
         * アプリケーションをスタート
         */ 
        private function start(mode:String):void
        {
            trace("Application start", mode);
        }
    }
}

メソッド’initialize’が呼ばれ初期化処理された後、ステージをクリックするとアプリケーションが開始されます。
さっきのサンプルと同じように、関数クロージャに、initializeメソッドにわたされた引数をカプセル化しています。
ここまでは問題ありません。

ただ実際の実装はこんなにシンプルな物ではなく、
初期化にはそれなりの命令文が続くはずです。

package
{
    public class MyApplication extends Sprite
    {
        public function MyApplication()
        {
        }
        
        public function initialize(mode:String) : void
        {
            /**
             * めっちゃ重いオブジェクトを取得
             * -------------------------------------------
             * ローカル変数に格納することにより、
             * 一連の初期化処理が終わりこの関数を抜けた後、
             * このオブジェクトの参照は破棄されることを期待している。
             */ 
            var renderingData:Array = RenderSourceServise.getRenderingSource();
            
            /**
             * このアプリケーションは、ステージのクリックを検知し、開始する。
             */ 
            stage.addEventListener( MouseEvent.CLICK,
                                    function(e:Event)
                                     {
                                         start( mode );    
                                     });
        }        
        
        /**
         * アプリケーションをスタート
         */ 
        private function start(mode:String):void
        {
            trace("Application start", mode);
        }
    }
}

たとえば、ビューをレンダリングするために、かなり大きなビットマップ画像の配列を取得したとします。
察しのいい方ならお分かりでしょうが、この時点で結構深刻な問題が生まれています。

そうです。
巨大な配列、renderingDataが関数クロージャにカプセル化されてしまうのです。

関数クロージャ内で、renderingDataへの参照はありませんが、そんなことは関係ありません。
関数クロージャ内で使われようがそうでなかろうが、アクティベーションオブジェクトにしっかり保存されてしまうのです。
前回からのエントリーをみていただければ当然のことだとわかると思います。
そんなばかな!とにわかに信じがたいかもしれませんが、本当です。怖いですねー。
僕も最初にこれに気づいたときには愕然としました。ぶるぶる。

さらに、実際には、このinitializeメソッドには他にも多くのローカル変数が使われるはずです。
それらが「すべて!」、そう「すべて!」カプセル化されます。
もちろん、問題は、カプセル化されること自体ではありません。
リスナとして参照され続けるということが本当の問題です。
ローカル変数だから関数を抜けると破棄されると思っていたのに、こんな罠が!ですよね。
関数クロージャとイベントリスナの素敵なコラボレーションですね。

関数クロージャを他の人に渡す時は、常にこの問題に注意を払う必要があります。
他にどんなケースがあるでしょうか?
ありがちな例をいくつか挙げてみます。

package
{
    public class MyApplication extends Sprite
    {
        public function MyApplication()
        {
        }
        
        public function initialize(mode:String) : void
        {
            /**
             * アプリケーションマネージャを生成
             * アプリケーションマネージャを初期化し、
             * 非同期の初期化処理が終わったらアプリケーションを開始する。
             */ 
            _myManager = new Manager();
            _myManager.addEventListenert( Event.Complete,
                                        function(e:Event)
                                         {
                                             start( mode );    
                                         });
            
            /**
             * めっちゃ重いオブジェクトを取得
             * -------------------------------------------
             * ローカル変数に格納することにより、
             * 一連の初期化処理が終わりこの関数を抜けた後、
             * このオブジェクトの参照は破棄されることを期待している。
             */ 
            var renderingData:Array = RenderSourceServise.getRenderingSource();
        }
        
        
        /**
         * アプリケーションをスタート
         */ 
        private function start(mode:String):void
        {
            trace("Application start", mode);
        }
    }
}

これも、リスナ&関数クロージャです。

こんな風に上下逆になるだけで、普通にスルーしてしまいそうです。
removeEventListener()するまではManagerインスタンスから参照され続けます。。
関数クロージャとカプセル化された巨大なオブジェクトが。。
頼んでもないのにです。

気をつけないといけないのは、イベントリスナだけではないです。

package
{
    public class MyApplication extends Sprite
    {
        public function MyApplication()
        {
        }
        
        public function initialize(mode:String) : void
        {
            /**
             * めっちゃ重いオブジェクトを取得
             * -------------------------------------------
             * ローカル変数に格納することにより、
             * 一連の初期化処理が終わりこの関数を抜けた後、
             * このオブジェクトの参照は破棄されることを期待している。
             */ 
            var renderingData:Array = RenderSourceServise.getRenderingSource();
            
             /**
             * 何らかの理由で、1秒後にアプリケーションを開始したい。
             */ 
            setTimeout( function()
                        {
                            start( mode );
                        }, 1000);
        }
        
        
        /**
         * アプリケーションをスタート
         */ 
        private function start(mode:String):void
        {
            trace("Application start", mode);
        }
    }
}

こんなのも、よくやってしまいます。
かならずclearInterval()しましょう。
clearInterval()するまではグローバルから参照され続けます。。
関数クロージャとカプセル化された巨大なオブジェクトが。。
頼んでもないのにです。

ほかには、コールバック関数として関数クロージャを他のオブジェクトに渡したりとかでしょうか。
AS2スタイルでイベントハンドラが書けるラッパーとか使っている人(そんなラッパーがあるかどうか知らないが)は注意しましょう。

btn.onRelease = function(){};

みたいなやつ。
これも立派な関数クロージャ。
dynamicクラス大好きな人はばんばんやってそうな予感もします。

まとめ

じゃぁ、どうすればいいの?って話になると思うのですが、
メモリリークのリスクが発生するのは、関数クロージャを他のオブジェクトに渡すときです。
なので、ブラックボックスなオブジェクトには渡さない方がいいんじゃないでしょうか。
すくなくとも、EventDispatcherやsetTimeoutなど、参照を断ち切るインターフェイスを持ったクラスか、
仕事が終わったら参照を破棄することを保証してくれるオブジェクトにだけ渡しましょう。

また、関数クロージャは使うべきときに使うようにしましょう。
使うべき時とはローカル変数のカプセル化や遅延評価を利用したいときです。
「メソッドを定義するのがめんどくさくて匿名関数ベタ書きしちゃいましたー。てへ」というのはよくないです。
自戒を込めて。
さらに、カプセル化のコード例の様に、関数クロージャを生成する際は生成する専用のメソッドを用意してあげると、リスクを最小限に出来ると思います。

ながなが読ませやがって、そんな落ちかよ!知ってたよ!ってひとはごめんなさい。
だって自分にとっては衝撃だったんだもん。

最後に、
「FlexBuilder3のプロファイラはパンドラの箱かもしれん」

カテゴリー: ActionScript3 パーマリンク

アクティベーションオブジェクトによるメモリリーク への3件のフィードバック

  1. YO のコメント:

    いつも楽しく拝見させていただいております。
    とても参考になりました。
    >dynamicクラス大好きな人はばんばんやってそうな予感もします。
    間違いなく私のことです(笑
    過去のコードを読み返してみたら確かに大変なことになっておりました。

  2. Webのプルタブ のコメント:

    Flex3 関数クロージャでメモリリークしてしまう件

    ActionScript3で、以下の様にクロージャでインスタンスを生成した場合。…

  3. yamaharu のコメント:

    YOさん、こんにちは。
    btn.onRelease=function(){}
    は、as2の頃にはヘルプにも記載されている標準的なやり方で、僕も何も考えずにやってましたw
    as2はランタイムエラーも殆どはかないし、基本的に緩い制約でのびのびコードが書けた時代でしたよね。そんなに複雑なアプリケーションも要求されず、メモリリークは起こりまくってたのでしょうがあまり気にする人もいなかったんじゃないでしょうか。
    as3になって、ライブラリ開発などを始める人が増えて、この辺の話題を参照する人が多くなった気がします。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です