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

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件のコメント

アクティベーションオブジェクトとスコープチェーン

3回シリーズの2回目です

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

参照の解決

以下のようなコードがあるとします。

package
{
    public class MyClass extends Sprite
    {
         private function methodA():void
        {
        }

        private function methodB():void
        {
                    var f:Function = function()
                                    {
                                        trace(methodA);
                                    }
                    f();
        }
    }
}

methodBを実行すると、メソッド’methodA’が出力されます。

// output
// function Function() {}

ごく普通で当たり前の光景ですよね。
でも、よく考えると不思議です。
関数クロージャ(匿名関数)’f'はFunctionインスタンスで、MyClassインスタンスとは独立した別のオブジェクトです。
なぜ、関数クロージャ’f'はMyClassのprivateなメソッドにアクセスできるのでしょうか?

僕は、「methodAの前にthisが省略されていて、そのthisはMyClassインスタンスを指しているから」だと勝手に理解していたのですが、調べてみると全然違いました。

前回のエントリーで書いたように、
関数クロージャ’f'の中でtrace(this)すると、[object global]が出力されます。
つまり、仮にthisが省略されていたとしても、関数クロージャ’f'内でのthisはMyClassインスタンスを指しません。

実は、このmethodAはスコープチェーンによって解決されています。

スコープチェーン

僕は、つい頭の中で「スコープチューン」と読んでしまうのですが、「チェーン」です。鎖です。
参照を解決するオブジェクトが連なった鎖をイメージするとよいかと思います。

関数が実行されるたびにスコープチェーンが生成されます。

  1. まず、methodBが実行されるとき、MyClassインスタンスが持つ全てのプロパティが参照できるオブジェクトが生成され、methodBのスコープチェーンに格納されます。(多分。。)
  2. つぎに、methodBのなかで関数クロージャ’f'が実行されるときに、そのスコープチェーンが渡されます。(多分。。)
  3. そして、tarce(methodA)でmethodAを解決する際に、このスコープチェーンを検索します。
  4. 最後に、スコープチェーンにある、1で出来たオブジェクトが検索にヒットし、めでたくmethodAにアクセスすることができます。

    (この1〜4の行程は、十分な裏が取れず推測が混じっているので正しくないかもしれない

スコープチェーンが生成されるときに作られるオブジェクトについては、リファレンスによると

Second, a scope chain is created that contains an ordered list of objects that Flash Player checks for identifier declarations.
次に、”スコープチェーン” が作成され、そこには Flash Player によって識別子宣言の有無がチェックされるオブジェクトが列挙されたリストが含まれます。

とありますが、正直この説明は漠然としすぎていてよくわかりません。
が、AS3のスコープチェーンについてのこれ以上詳しいリソースを見つけられませんでした。

知りたいのは、その「 Flash Player によって識別子宣言の有無がチェックされるオブジェクト」がどういうルールで生成されるのかなんですが。。
詳細をご存知の方はコメントください!

アクティベーションオブジェクト

さて、さっきの説明でははしょったんですが、
関数の実行時には、毎回アクティベーションオブジェクトと呼ばれるオブジェクトが生成されて、関数のスコープチェーンの先頭に加えられます。

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. You cannot access the activation object directly, because it is an internal mechanism.
関数が実行を開始するたびに、多数のオブジェクトおよびプロパティが作成されます。最初に、”アクティベーションオブジェクト” という特別なオブジェクトが作成され、そこには関数本体で宣言されるパラメータとローカル変数または関数が格納されます。アクティベーションオブジェクトは内部メカニズムであるため、そこに直接アクセスすることはできません。
(ActionScript 3.0 のプログラミング > ActionScript 言語とシンタックス > 関数 > 関数のスコープ)

また、関数クロージャが実行される場合は、関数クロージャを呼び出した親関数のアクティベーションオブジェクトが関数クロージャのアクティベーションオブジェクトの後に追加されます。メソッドクロージャの実行時にはこのアクティベーションオブジェクトの受け渡しは行われません。

For a nested function, the scope chain starts with its own activation object, followed by its parent function’s activation object.
ネストされた関数の場合は、スコープチェーンはそのアクティベーションオブジェクトから始まり、その後に親関数のアクティベーションオブジェクトが続きます。

ちょっと例を変えて、説明を試みてみます。

public class ScopeTest extends Sprite
{
    private var myNum:int;
    public function ScopeTest()
    {
        methodA();
    }
    private function methodA():void
    {
        var myNum:int = 2;
        methodB();
    }
    private function methodB():void
    {
        var myNum:int = 3;
        var f:Function = function():void
                    {
                        var myNum:int = 4;
                        trace(myNum);
                    };
        f();
    }
}

このコードではmyNumという識別子を持つ変数が4カ所で定義されています。

  • ScopeTestクラスのプロパティ
  • methodAのローカル変数
  • methodBのローカル変数
  • 関数クロージャ’f”のローカル変数

関数クロージャ内でこのmyNumをtraceしたときに出力される値は何でしょうか?

// output
// 4

では、関数クロージャのローカル変数であるmyNumをコメントアウトしてみます。

public class ScopeTest extends Sprite
{
    private var myNum:int;
    public function ScopeTest()
    {
        methodA();
    }
    private function methodA():void
    {
        var myNum:int = 2;
        methodB();
    }
    private function methodB():void
    {
        var myNum:int = 3;
        var f:Function = function():void
                    {
                        //var myNum:int = 4;
                        trace(myNum);
                    };
        f();
    }
}

こんどは出力結果は3になり、methodBのmyNumを参照するようになりました。

//output
//3

さらに、methodBのローカル変数であるmyNumをコメントアウトしてみます。

public class ScopeTest extends Sprite
{
    private var myNum:int;
    public function ScopeTest()
    {
        methodA();
    }
    private function methodA():void
    {
        var myNum:int = 2;
        methodB();
    }
    private function methodB():void
    {
        //var myNum:int = 3;
        var f:Function = function():void
                    {
                        //var myNum:int = 4;
                        trace(myNum);
                    };
        f();
    }
}

結果は以下の通りで、ScopeTestインスタンスのmyNumプロパティが参照されます。

// output
// 0

この結果から、methodAのローカル変数myNumにはアクセスしていないことがわかります。
関数クロージャ’f'はmethodAが見えていません。
つまり、「methodAのローカル変数myNumは、関数クロージャ’f'のスコープチェーンのなかには存在しない」ということができます。

スコープチェーンが作られていく様子を詳細に追いかけてみます。
注意するべきポイントは、「関数クロージャが実行されるときに行われるアクティベーションオブジェクトの受け渡し」です。(下のリストの中の9の後半です)

  1. methodAが実行されるときにアクティベーションオブジェクトが生成されます。
    このアクティベーションオブジェクトには、プロパティ’myNum’を持ち、methodAのローカル変数’myNum’の値が格納されます。
    便宜上、アクティベーションオブジェクトを@methodA{myNum:2}と表現することにします。

  2. 次にmethodAのスコープチェーンが生成されます。
    このスコープチェーンにはScopeTestインスタンスのprivateな変数であるmyNumの参照をもつオブジェクトが含まれます。
    便宜上、このオブジェクトを#ScopeTest{myNum:0}と表現することにします。

  3. 2で作成されたスコープチェーンの先頭にアクティベーションオブジェクトが追加されます。
    methodAのスコープチェーンは以下のような順番になっています。
    [@methodA{myNum:2} - #ScopeTest{myNum:0}]
    便宜上、スコープチェーンをこのように表現します。

  4. methodBが呼び出されます。
  5. methodBが実行されるときにアクティベーションオブジェクト @methodB{myNum:3, f:関数クロージャ} が生成されます。
    このアクティベーションオブジェクトはmethodBのローカル変数をプロパティとして持ちます。

  6. methodBのスコープチェーンが生成され、アクティベーションオブジェクトが先頭に追加されます。
    methodBのスコープチェーンは以下のような順番になっています。
    [@methodB{myNum:3, f:関数クロージャ} - #ScopeTest{myNum:0}]

  7. 関数クロージャ’f'が呼び出されます。
  8. 関数クロージャ’f'が実行されるときにアクティベーションオブジェクト @f{myNum:4} が生成されます。
    このアクティベーションオブジェクトは関数クロージャ’f'のローカル変数をプロパティとして持ちます。

  9. 関数クロージャ’f'のスコープチェーンが生成され、アクティベーションオブジェクトが先頭に追加されます。
    さらに、このとき、呼び出しもとの親関数のアクティベーションオブジェクト(5で作られた @methodB{myNum:3, f:関数クロージャ} ) がその後に追加されます。
    [@f{myNum:4} - @methodB{myNum:3, f:関数クロージャ} - #ScopeTest{myNum:0}]

[@f{myNum:4} - @methodB{myNum:3, f:関数クロージャ} - #ScopeTest{myNum:0}]
最終的に、これが、関数クロージャ’f'のスコープチェーンになります。

左が先頭のオブジェクトで、右にオブジェクトが続きます。
関数が識別子を解決する場合は、先頭のオブジェクトから探していきます。

今回の例では、myNumの検索は先頭のアクティベーションオブジェクトでヒットし、関数クロージャ’f'のmyNumが参照されます。myNumを1つづつコメントアウトすると、検索にヒットするオブジェクトがひとつずつ右に移動します。
これは先ほどの出力結果と一致し、methodAがスコープチェーンに含まれていないことも確認できます。


参考サイト:http://www2u.biglobe.ne.jp/~oz-07ams/prog/js-notes/scope.html


次回のエントリーに続く

カテゴリー: ActionScript3 | コメントをどうぞ

AS3のクロージャ

どうしても解消しないメモリリークを追いかけていったら、アクティベーションオブジェクトにたどりついたので、
自分の理解を深めるためにまとめてみます。
長くなりそうなので、3回のエントリーに分けます。

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

関数

リファレンスによると、AS3の関数は「メソッド」と「関数クロージャ」の 2種類に分類されるようです。

There are two types of functions in ActionScript 3.0: methods and function closures.
ActionScript 3.0 の関数には「メソッド」と「関数クロージャ」の 2 種類があります。
(ActionScript 3.0 のプログラミング / ActionScript 言語とシンタックス / 関数)

関数クロージャとは何かというと、メソッド以外の全ての関数のことです。

Whether a function is a called a method or a function closure depends on the context in which the function is defined. A function is called a method if you define it as part of a class definition or attach it to an instance of an object. A function is called a function closure if it is defined in any other way.
関数をメソッドと呼ぶか関数クロージャと呼ぶかは、関数が定義されたコンテキストによって決まります。関数をクラス定義の一部として定義した場合、またはオブジェクトのインスタンスに関連付けた場合は、メソッドと呼びます。関数がその他の方法で定義された場合は、関数クロージャと呼びます。

さらに、メソッドは特にメソッドクロージャ(バインドメソッド)と呼ばれる使われ方をすることがあります。

A bound method, sometimes called a method closure, is simply a method that is extracted from its instance. Examples of bound methods include methods that are passed as arguments to a function or returned as values from a function.
バインドメソッドは、単にインスタンスから抽出されるメソッドで、「メソッドクロージャ」とも呼ばれます。バインドメソッドの例には、関数にパラメータとして渡され、関数から値として返されるメソッドがあります。
(ActionScript 3.0 のプログラミング / ActionScript のオブジェクト指向プログラミング / クラス)

クロージャ

つまり、AS3のクロージャには「メソッドクロージャ」と「関数クロージャ」があるということですね。
関数の分類を図にするとこんな感じです。
function.jpg


もうちょっと詳しくみていきます。
例えば以下のようなコードがあったとき、

package
{
    import flash.display.Sprite;
    public class FunctionTest extends Sprite
    {
        public function FunctionTest()
        {
        }

        private function onClick(e:Event=null):void
        {
            trace(this);
        }

        private function aboutMethodClosure():void
        {
            stage.addEventListener(MouseEvent.CLICK, onClick);

            var o:Object = {};
            o.onClick = onClick;
            o.onClick();
        }

        private function aboutFunctionClosure():void
        {
            var f:Function = function():void
                             {
                                trace(this);
                                trace(this is Function);
                             };
            f();
            var o:Object = {};
            o.f = f;
            o.f();
        }
    }
}

各関数は以下のように分類できます。

コンストラクタ

FunctionTestは通常コンストラクタといいますが、正確にはコンストラクタメソッドといってメソッドの一部です。

メソッドクロージャ

コード中のメソッド’aboutMethodClosure’の中で、
Stageのリスナとして’onClick’メソッドを渡しています。
このとき、onClickはクラスから抽出されています。
これがメソッドクロージャ。

また、その後にObjectインスタンスを生成して
そのインスタンスにonClickというメンバを定義し、onClickメソッドを代入しています。
ここで代入したonClickメソッドもクラスから抽出されたメソッドなのでメソッドクロージャ。

関数クロージャ

関数クロージャは、メソッド以外の全ての関数ですが、
ほとんどの場合メソッド内で新しく定義された関数が関数クロージャになります。
関数内で新しく定義された関数であることから「ネストされた関数」という言い方もするようです。

この例では匿名関数を生成し、変数’f'に代入していますが、以下のような書き方も出来ます。

package
{
    class myClass
    {
        private function myFuncA() : void
        {
            function myFuncB() : void
            {
            }
        }
    }
}

この場合、
myFuncAはメソッド、
myFuncBは関数クロージャ。

メソッドクロージャと関数クロージャの違い

メソッドクロージャ’onClick’と、関数クロージャ’f'は
実行されるとどちらもtrace(this)します。
メソッドクロージャと関数クロージャでは、このthisが指す参照が大きく違います。

実行結果は以下のようになります。

//メソッドクロージャ
[object FunctionTest]
[object FunctionTest]
//関数クロージャ
[object global]
[object Object]

メソッドクロージャの方は、どのような呼び出し方をされても
thisの参照先は「そのメソッドが定義されたインスタンス」を、
つまりFunctionTestインスタンスを指します。
このようにthisの参照先が常に束縛されているので「バインドメソッド」とも呼ぶようです。

それに対して、関数クロージャは
呼び出し方によって参照先が変わります。
最初のf()での呼び出しですが、このfはどのオブジェクトの持ち物でもありません。
が、このときアクティベーションオブジェクトと呼ばれるObjectが内部的に生成されていて、
fはこのアクティベーションオブジェクトのスコープで実行されます。

thisの参照を解決するためにスコープチェーンをたどりこのアクティベーションオブジェクトに到達し、
thisの参照先がアクティベーションオブジェクトになっています。
(ここの理解は自信なし。スコープチェーンをたどってGlobalに達しているのかもしれん)

(thisの値はスコープチェーンと関係なかった。たんにこの関数が誰に関連づけられているかでthisが指すものは決まる。誰の持ち物でもない場合はglobalオブジェクトが返る)
それに対して、次のo.f()の呼び出しでは、このfはoに含まれた関数なので
thisはObjectインスタンス’o'を指しています。

AS2の頃のクロージャ

AS2では「メソッドクロージャ」はなく、全て「関数クロージャ」でした。
なので、以下のようにイベントのリスナに登録すると
クロージャはMouseのスコープで実行されてしまい、thisはMouseを指していました。

class MyClass
{
    function MyClass()
    {
        Mouse.addListener(onClick);
    }
    private function onClick():void
    {
        trace(this);
    }
}

これを回避するために、Delegate.create()していたのはいい思い出ですよね。
AS3でのメソッドクロージャの導入は、この面倒を解決するためのものだと勝手に想像しています。


次回のエントリーへつづく

カテゴリー: ActionScript3 | 2件のコメント

ASのガベージコレクトは参照カウント方式じゃなかった

かなり今更ですが、
「そのオブジェクト、本当に消えていますか?」で、ガベージコレクトは参照カウント方式で行われると書きましたが、
どうやらマーク・アンド・スウィープ方式と呼ばれる方法で行われているようです。
参考:akihiro kamijo: Flash Player 9 のガーベジコレクション


参照カウント方式

参照カウント方式の場合、循環参照しているオブジェクトは永久に破棄されません。
gc_a0.jpg
矢印は参照方向、数字は参照カウントを表しています。
最下段の二つのインスタンスは、お互いに参照し合っています。(循環参照)
gc_a1.jpg
ここで、図のように参照を切ったとしても、、
gc_a2.jpg
依然として、最下段のインスタンスの参照カウントは0にはならず、
いつまでたってもガベージコレクトの対象にはなりません。

マーク・アンド・スウィープ方式

マーク・アンド・スウィープ方式では、循環参照の問題は起きません。
いままで僕はオブジェクト同士が循環参照にならないように気を使っていましたが、
徒労だったというわけです。
マークアンドスウィープ方式は以下のような感じです。
gc_b0.jpg
ルート(おそらく、StageやGlobal)から参照をたどれるオブジェクトにマークをつけていきます。
参照をたどれるオブジェクトがなくなった時点で、まだマークされていないオブジェクトがあったら、
ガベージコレクトの対象となります。
たとえば、以下のように参照を切った場合、
gc_b1.jpg
最下段の二つのインスタンスはルートから参照をたどれなくなるので、
ガベージコレクトの対象となり破棄されます。
gc_b2.jpg

FlashPlayerが参照カウント方式でガベージコレクトして「いない」ことを確かめるサンプルです。
循環参照しているインスタンスを意図的に生成しています。
FlashPlayerが参照カウント方式でガベージコレクトしていれば、
メモリ使用量が増え続けるはずですが、
実行するとある程度増えたところでお掃除される様子が確認できます。
(Flashのガベージコレクトはそんなに頻繁には行われないようです。
僕の環境だと600Mbほどメモリを使用したところで、ガベージコレクトされました。
ガベージコレクトされる頻度は、おそらく実行環境に依存すると思います。)

package
{
    import flash.system.System;
    import flash.events.Event;
    import flash.display.BitmapData;
    import flash.text.TextField;
    import flash.display.Sprite;
    import flash.utils.setInterval;

    public class Main extends Sprite
    {
        private var _memoryViewer : TextField;
        public function Main ()
        {
            //相互参照なオブジェクトの生成
            setInterval(createEachReferencedObj, 1000);

            //メモリ表示用テキストエリア
            _memoryViewer = new TextField();
            addChild(_memoryViewer);
            addEventListener(Event.ENTER_FRAME, checkMemory);
        }

        /**
         * 相互参照なオブジェクトを生成
         */
        private function createEachReferencedObj () : void
        {
            var objA : Object = {bmp:new BitmapData(2000, 2000, true)};
            var objB : Object = {ref:objA};
            objA.ref = objB;
        }

        /**
         * メモリ使用量を表示
         */
        private function checkMemory (event : Event) : void
        {
            var bytes : int = System.totalMemory;
            _memoryViewer.text = String(bytes >> 20) + " Mb";
        }
    }
}
カテゴリー: ActionScript3 | コメントをどうぞ

AS3におけるroot参照

AS2はもとより、AS3でもrootを参照することはあまりないと思うけど、AS3でのルート参照を調べてみたら、かなり直感的ではないことがわかったのでレポートします。
いざというときに役に立つかも。

●そもそもFlashにおけるルートとはなんぞや

画面上に表示されるオブジェクトを「表示オブジェクト」とし、表示オブジェクトのコンポジット構造を「表示オブジェクトツリー」とすると、「ルート」とは「表示オブジェクトツリー」のルートノードである
という定義でいいと思う。多分。

問題は、AS2ではすべての表示オブジェクトは、一本の「表示オブジェクトツリー」で表現されていたが、(例外として、loadMovieNum()を使用した場合は、そこから新しく表示オブジェクトツリーが生成されていた。もうあんまり覚えてないけど。あと、MovieClip._lockrootとか。。使ってなかったけど。)AS3の場合は、どうもそう単純な話ではない様子。

●AS3におけるroot参照

たとえば、以下のような表示オブジェクトツリーがあるとする。(クリックで拡大します)
a image of display object tree
Stage直下にLoaderやDisplayObjectが存在するのに違和感を感じるかもしれないが、
Stage.addChild()が可能である以上、これは普通に起こりえる状況。
さて、それぞれのオブジェクトのrootプロパティはいったい何を指すでしょうか。

答えは ↓
一見するとかなり複雑に見える。(クリックで拡大します)
a image of pointing root

で、がんばって、一般化してみた。
あるオブジェクトのrootプロパティが何を指すかは、そのオブジェクトの親をたどっていき、

1・Loaderにぶちあたったら、そのLoaderに読み込まれたDisplayObjectがroot。
2・DocumentClassにぶちあたったら、それがroot。
3・Stageにぶちあたったら、それがroot。

言い方を変えると、
Stageから下におりていき、DocumentClassかLoaderに読み込まれたDisplayObjectにぶちあたったら
そのオブジェクトをrootとしてあたらしい表示オブジェクトツリーが開始される。

ちなみに、表示オブジェクトツリーを色分けしてみるとこんな感じ。
a image of pointing to root

最後に検証したコードと出力結果です。

package
{
    import flash.display.*;
    import flash.net.*;
    import flash.events.*;
    public class Main extends Sprite
    {
        private static  var cnt : int = 0;

        public function Main ()
        {
            if (stage)
            {
                //Test1
                test(this);
            }
        }

        public override function toString () : String
        {
            return (parent is Loader) ? "Loader.content" : "DocumentClass";
        }

        private function test (target : DisplayObjectContainer) : void
        {
            cnt ++;
            trace("¥n¥n/************************************");
            trace(" *" + cnt + "¥tTest about " + target + "'s root");
            trace(" ****************************/");
            var stageChildren : int = stage.numChildren;
            trace(" * now, Stage has " + stageChildren + "children");
            for (var i : int;i < stageChildren; i ++) trace("¥t¥t" + stage.getChildAt(i));

            //check parent
            trace(target + "'s parent is " + target.parent);

            //check root
            trace(target + "'s root is " + target.root);

            //check Stage's root
            trace("Stage's root is always Stage : " + (target.stage.root is Stage));

            if (cnt == 1)
            {
                //check DocumentClass'children
                var spr : Sprite = new Sprite();
                var spr2 : Sprite = new Sprite();
                spr.addChild(spr2);
                target.addChild(spr);
                trace("root of DocumentClass's child  is DocumentClass : " + (spr.root == target && spr2.root == target));

                var anortherChildOfStage : Sprite = new Sprite();
                var grandChildOfStage : Sprite = new Sprite();
                anortherChildOfStage.addChild(grandChildOfStage);
                target.stage.addChild(anortherChildOfStage);
                //Test2
                test(anortherChildOfStage);
            } else if (cnt == 2)
            {
                //Test3
                test(target.getChildAt(0) as DisplayObjectContainer);
            } else if (cnt == 3)
            {
                var loader : Loader = new Loader();
                loader.load(new URLRequest("root.swf"));
                loader.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadedLoader);
            }
            else if(cnt == 5)
            {
                var loader2 : Loader = new Loader();
                loader2.load(new URLRequest("root.swf"));
                loader2.contentLoaderInfo.addEventListener(Event.COMPLETE, onLoadedLoader2);
            }
        }

        private function onLoadedLoader (event : Event) : void
        {
            var loader : Loader = LoaderInfo(event.target).loader;
            stage.addChild(loader);
            //Test4
            test(loader);
            //Test5
            test(loader.content as DisplayObjectContainer);
        }

        private function onLoadedLoader2 (event : Event) : void
        {
            var loader : Loader = LoaderInfo(event.target).loader;
            this.addChild(loader);
            //Test6
            test(loader);
            //Test7
            test(loader.content as DisplayObjectContainer);
        }
    }
}

//結果

/************************************
*1	Test about DocumentClass's root
****************************/
* now, Stage has 1children
DocumentClass
DocumentClass's parent is [object Stage]
DocumentClass's root is DocumentClass
Stage's root is always Stage : true
root of DocumentClass's child  is DocumentClass : true
/************************************
*2	Test about [object Sprite]'s root
****************************/
* now, Stage has 2children
DocumentClass
[object Sprite]
[object Sprite]'s parent is [object Stage]
[object Sprite]'s root is [object Stage]
Stage's root is always Stage : true
/************************************
*3	Test about [object Sprite]'s root
****************************/
* now, Stage has 2children
DocumentClass
[object Sprite]
[object Sprite]'s parent is [object Sprite]
[object Sprite]'s root is [object Stage]
Stage's root is always Stage : true
/************************************
*4	Test about [object Loader]'s root
****************************/
* now, Stage has 3children
DocumentClass
[object Sprite]
[object Loader]
[object Loader]'s parent is [object Stage]
[object Loader]'s root is [object Stage]
Stage's root is always Stage : true
/************************************
*5	Test about Loader.content's root
****************************/
* now, Stage has 3children
DocumentClass
[object Sprite]
[object Loader]
Loader.content's parent is [object Loader]
Loader.content's root is Loader.content
Stage's root is always Stage : true
/************************************
*6	Test about [object Loader]'s root
****************************/
* now, Stage has 3children
DocumentClass
[object Sprite]
[object Loader]
[object Loader]'s parent is DocumentClass
[object Loader]'s root is DocumentClass
Stage's root is always Stage : true
/************************************
*7	Test about Loader.content's root
****************************/
* now, Stage has 3children
DocumentClass
[object Sprite]
[object Loader]
Loader.content's parent is [object Loader]
Loader.content's root is Loader.content
Stage's root is always Stage : true

出力の1〜7の番号は、以下のオブジェクトに対応します。

●最後に

検証コード自体複雑になってしまって、もしかしたらまちがってるかもしれん。
あと、すべてのDisplayObjectについて検証したわけじゃないので、
Loaderみたいな特別なやつが他にもいるかもしれん。

ひとついえるのは、AS2の感覚でrootを使うと間違いなくはまる。
自分が何をしようとしているか、はっきりと確信を持っている時じゃなければ、
root参照は使わない方がいいんじゃないでしょうか。

カテゴリー: ActionScript3, AS3fromAS2 | 2件のコメント

privateが自分が思っていたほどprivateじゃなかった件について

今更、自分が凄い勘違いをしていたことに気づいてしまった。

privateキーワードは、可視性を当事者であるインスタンスのみに制限するものだと思ってた。
違うんだね。
privateが定義されているクラスのインスタンスからなら誰からでもアクセスできるんだね。
こんな凄い勘違い、今までよく問題にならなかったなと。。
思い込みが強すぎて、アクセスを試そうとすらしたことがなかった。
自分への戒めをこめてカミングアウトします。

package {
    public class MyClass {
        private var _privateValue : int;
        public function MyClass(v:int) {
            _privateValue = v;
        }

        public function printPrivateValue(c:MyClass) {
            /**
             * よそのインスタンスのprivateな変数にはアクセスできないと思ってた。
             *でもクラスが同じならアクセスできるんだね。
            */
            return c._privateValue;
        }
    }
}
//Client code
var c1 : MyClass = new MyClass(1);
var c2 : MyClass = new MyClass(2);
trace(c1.printPrivateValue(c2));
//output
2
カテゴリー: ActionScript3 | コメントをどうぞ

flash.utils.Proxyの使いどころがわからん

flash.utils.Proxyって使えなくないですか?
あるオブジェクト、例えばSpriteのラッパークラスがあるとして、
このクラスを外から見るとSpriteとしてみせたい場合、
Spriteを継承するわけだけど、
ASは多重継承が出来ないので、いつも継承が使えるわけじゃない。
まぁ、「継承よりコンポジット」というOOPの指針もあることだし、
Spriteをコンポジットしてラップするとすると、
今度は、Spriteが持っている全てのプロパティやメソッドを
コンポジットしているSpriteに委譲するコードを書くのが超めんどくさい。
そこでProxyが使えるのかなーって思ったんだけど、
たとえばSpriteのように振る舞うProxySpriteは必ずProxyを継承しなければいけない仕様。。
これ外からみてもSpriteに見えないよ。
もちろん、ProxySpriteをSpriteに強制型変換も出来ない。
実に使えない。というか使いどころが想像できない。
ヘルプにあるサンプルは、Arrayの代理オブジェクトを例としてあげてるけど、
このサンプルからはProxy使っている意味が全く読み取れない。
Proxyをうまく使っている例ってみたことあります?

カテゴリー: ActionScript3 | コメントをどうぞ

計算機のくせに

コンピュータの計算は、事情により結構いい加減なのは知っていたけど、
バグを追いかけた結果、目の当たりにするとなんともはや。
こういうのって個別に対応するしかないんでしょうか。


trace(1.005 * 100);
//期待する結果    100.5
//実際の結果    100.49999999999999

特にAS3から、各種プロパティが0-1の値を取るようになったので、
この問題にぶちあたりやすい気がする。

そもそも、何でこれに気づいたかというと、
1.005を小数点以下2桁の数値になるように四捨五入したくて、
100倍して、整数に丸めて、0.01倍してて気づいたんだよね。

ところで!
その後、調べてみたら、まさにそのためのメソッド
Number.toFixed()があった!

しか〜し


var num:Number = 1.005
trace(num.toFixed(2));

//結果 1.00

だめじゃん。。
これも同じことが起こってるんだろうね。

ちょっとテストしてみた。


trace(Number(0.005).toFixed(2));
trace(Number(1.005).toFixed(2));
trace(Number(2.005).toFixed(2));
trace(Number(3.005).toFixed(2));
trace(Number(4.005).toFixed(2));
trace(Number(5.005).toFixed(2));
trace(Number(6.005).toFixed(2));
trace(Number(7.005).toFixed(2));
trace(Number(8.005).toFixed(2));
trace(Number(9.005).toFixed(2));

// 結果
0.01
1.00
2.00
3.00
4.00
5.00
6.00
7.00
8.01
9.01

あわあわ。

カテゴリー: ActionScript3 | コメントをどうぞ

外部swfにリンケージされているクラスをnewしたい

ちょっとはまったのでメモ。

ミッション

メインswfから外部swf(アセット用swf)を読み込み、読み込んだアセットswにあるクラス(MyClass)をnewしたい。
ただし、MyClassはライブラリにあるMovieClipシンボルとリンケージされている。

図にするとこんな感じ
mission.gif
で、Main.asからこんな感じにnewしたい、と。

var myClass:MyClass = new MyClass();

その場合、何が問題なのか?

アセットswfにコンパイルされているMyClassが、ライブラリにあるシンボルとリンケージされている場合、
読み込み元に記述されているMyClassが競合する。

なぜか?

アセットにコンパイルされているMyClassと、読み込み元にコンパイルされているMyClassは同じ名前空間の同じクラス。
この場合、アセットswfを読み込んでも、読み込み元のMyClassはアセットのMyClassで上書きされない。
つまり、newできない。(リンケージしなければ可能)

思いついた3つの解決策

1・読み込み元にはクラスのimport記述をせず、型指定もしない。
ようするに読み込み元のswfにはMyClassをコンパイルしない。

var classRef : Class = getDefinitionByName("MyClass") as Class;
myClass = new classRef() as MovieClip;

2・読み込み元とは別のApplicationDomainにアセットを読み込む
この場合、main.swfにコンパイルされたMyClassと、読み込み元にコンパイルされているMyClassは、別のApplicationDomainになるので競合しない。

3・アセットswfではダミーのクラスをリンケージしておく
たとえば、dammyMyClassなどという識別子でリンケージし、クラスファイル(dammyMyClass.as)は作成しない。
(Flashはasset.swfコンパイル時に、自動的にダミーのクラスを作ってくれる。)
で、MyClassクラス内で、dammyMyClassをnewする

結論

1は、コンパイル時のエラーチェックが出来ない。
2は、アセットが関連するクラスはすべて、別のApplicationDomainにも存在する必要があるので、現実的じゃない。
いまのとこ、3がいいと思う。
ただし、この場合、MyClassはMovieClipを継承したクラスではなくMovieClipのラッパーとなる。
まぁ、MovieClipをどうしても継承したい局面が思い浮かばないので問題なし。

いろいろ書いたけど、個人的には
リンケージはいっさい使用しない!
これが最強。
AS2の頃からリンケージを使ってMovieClipをnewするのはすかんかった。

追記(2008/6/3):
と書いたものの、AS3のイベントモデルを堪能するにはMovieClipを継承しなければならない。最近は表示オブジェクトはコンポジットではなく継承するようにしています。なので、方法は3番だけど、MyClassはMovieClipを継承し、dammyMyClassをコンポジットするようにする。
これが今回のエントリについてのベストプラクティスだと思います。

カテゴリー: ActionScript3, AS3fromAS2 | コメントをどうぞ

as演算子の使いどころ

キャスト(型変換)は、オブジェクト指向プログラミングには欠かせない。
AS3にはキャストの方法が2種類ある。

()を使ったキャスト

変換したいインスタンスを、変換したい型のあと()でくくる。
キャストに失敗すると例外(Error)を投げる。
例外をキャッチしないとランタイムエラーになる。

trace(MovieClip(new Sprite()));
//output
TypeError: Error #1034: 強制型変換に失敗しました。flash.display::Sprite@4c38ee49 を flash.display.MovieClip に変換できません。

as 演算子を使ったキャスト

asの前に、「変換したいインスタンス」、後に「変換したい型」で記述する。
キャストに失敗するとnullを返す。

trace(new Sprite() as MovieClip);
//output
null

ところで、この2つはどう使い分けるべきなんだろう?

()を使ったキャストを使う局面は、「キャストできることが前提の処理を書くとき」
つまり、アプリケーションの実行上、キャストが失敗するのは何らかの異常事態が起こっているときの場合で、
as演算子を使う局面は、キャストに成功するか失敗するかコンパイル時には予想できない場合なのかな?

例えば、コレクション内に、いろいろな動物オブジェクトがあり、
その中のTigerに対して処理を実行したい場合、

()を使ったキャスト

function giveFoodToTiger(zoo:Array)
{
    for(var i:int = 0; i < zoo.length; i ++)
    {
        try
        {
            var tiger:Tiger = Tiger(zoo[i]);
        }
        catch(e)
        {
            //クリティカルなエラーが発生!エラー処理を実行
            tiger = new Tiger();
        }
        finally
        {
            //トラにだけ餌をあげる
            tiger.giveFood();
        }

    }
}

as演算子を使ったキャスト

function giveFoodToTiger(zoo:Array)
{
    for(var i:int = 0; i < zoo.length; i ++)
    {
        //トラにだけ餌をあげる
        var tiger:Tiger = zoo[i] as Tiger;
        if(tiger) tiger.giveFood();
    }
}

()を使ったキャストは、コレクション内の動物オブジェクトが「トラだけ」なのを想定していて、Tigerクラスでなければ困る場合に使える。
as演算子を使ったキャストは、コレクション内の動物オブジェクトが「いろんな動物」なのを想定していて、その中の特定オブジェクトだけに何かしたい場合に使える。

あと、未検証だけど、
as演算子を使ったキャストの方が高速らしい。

カテゴリー: ActionScript3, AS3fromAS2 | 1件のコメント