不要になったオブジェクトへの参照が残っていると、そのオブジェクトの使用しているメモリを開放することができません。特に複数の参照を持つオブジェクトに対しては、参照の消し忘れによるメモリリークが発生しないよう注意が必要です。
イベントリスナを登録すると、イベントのターゲットになるオブジェクトとイベントリスナを持つオブジェクトの間で参照ができます。AS3 では下のような記述になります。
eventTarget.addEventLisener("type", eventHandler);
このコードを実行すると eventTarget と this の間に参照がつくられます。(eventHandler は this オブジェクトのメソッド)
ところが、これは明示的な参照の追加ではありません。そのため、参照の削除が必要な場合でも見落としてしまいそうですよね。
というわけで、今回はイベントリスナ追加時の参照の扱い方についてです。
参照の方向
参照には方向性があります。つまりオブジェクト間の参照は一方向のみ可能ということです。例えば以下のようなコードがあったとします。
var foo = New Foo(); foo.bar = this; foo = null;
一行目で作られている参照は "このオブジェクト (this)" → "New Foo() で作られたオブジェクト" という方向の参照です。二行目で作られている参照はこの逆で "New Foo() で作られたオブジェクト" → "this" という方向です。三行目では一行目で作成した参照を削除しています。そのため "this" から "New Foo() で作られたオブジェクト" へと辿ることはもうできません。
前の記事にあるように、ガーベジコレクタはオブジェクトツリーのルートから参照を辿ります。まず、ルートの子オブジェクトを見つけて、次にその子オブジェクトの子オブジェクトを見つけるという動作を繰り返します。
とすると、上のサンプルが実行された場合、"this" が親オブジェクトであれば、ガーベジコレクタは "New Foo() で作られたオブジェクト" を見つけることができません。つまり二行目で作成した参照は残っていたとしても、三行目が実行されていればメモリリークの心配は無いことになります。
メモリリークの原因になり得るのは、親から子への参照のみであるということですね。
子オブジェクトへのイベントリスナ追加
では、イベントリスナ追加時にはどの方向の参照ができるのでしょうか。
var foo = New Foo(); addChild(foo); foo.addEventListener("click", clickHandler);
上の例では foo という子オブジェクトを作成して、その子オブジェクトである foo に clickHandler というイベントリスナを登録しています。clickHandler は親オブジェクトのメソッドです。
この場合には、子オブジェクトから親オブジェクトの方向の参照が作られます。 そのためイベントリスナはそのまま放置しておいてもメモリリークを引き起こすことはありません。親オブジェクトは不要になったら foo への明示的な参照を全て削除すれば十分です。
つまり、イベントリスナの追加時には、イベントリスナが追加されるオブジェクト → イベントリスナを持つオブジェクトの方向に参照が作成されているということです。(より正確には、イベントリスナが追加されるオブジェクト → イベントリスナ → イベントリスナを持つオブジェクト)
Flex ではイベントリスナの設定に以下のような記述が良く使われます。
<my:Foo id="foo" click="clickHander(event)" />
このケースでも、foo を後で削除する場合、イベントリスナが登録されているかどうかは気にしなくてもよいわけです。Flex 書いた事のある方は分かりますよね?
親オブジェクトへのイベントリスナ追加
次は下のようなケースを考えて見ます。
var foo = New Foo(); addChild(foo); addEventListener("click", foo.clickHandler);
今度は親オブジェクトが子オブジェクトのメソッドを登録しています。ということは、親から子への参照が作成されているということです。
このような場合は、子オブジェクトへの参照を削除する際にイベントリスナも削除しないとメモリリークの原因になります。
下のように子オブジェクトから親オブジェクトにイベントリスナを設定する場合も同様です。
parent.addEventListener("click", clickHandler);
イベントリスナの削除には removeEventListener() を使います。
parent.removeEventListener("click", clickHandler);
また Flex に話を移しますが、特に Flex では親子関係が分かり難い場合があります。例えばポップアップ表示されるウインドウオブジェクトがあったとして、その中でクリックイベントのリスナを SystemManager に登録した場合、
SystemManager.addEventListener("click", clickHandler);
SystemManager からポップアップウインドウへの参照が作られます。
ところで SystemManager はポップアップウインドウを管理する親オブジェクトでもあります。そのため、SystemManager からポップアップウインドウへの参照が 1. popupChildren の配列からと、2. イベントリスナ経由、の 2 つ存在することになります。従って、この場合は、ポップアップウインドウを削除する時にイベントリスナも削除しないとメモリリークが発生します。
弱い参照の利用
弱い参照とは、たとえ存在していても、ガーベジコレクションの際に参照としてみなされない参照です。
これは大変便利で、イベントリスナにより生成される参照が全て弱い参照であれば、参照の方向とか気にせず登録したリスナを放置しておいて良いことになるからです。
都合の良いことに、イベントリスナの登録時に弱い参照を使うかどうかを指定することができます。addEventListener() の5 つ目の引数を true にすれば弱い参照が使われます。
addEventListener("type", listener, false, 0, true)
3 つ目と 4 つ目の引数は false と 0 を指定しておけば大抵問題ありませんので、上の形を覚えておけば大丈夫です (たぶん)。今後イベントリスナを登録する際はこれを使うように決めてしまうというのも結構お勧めかもしれません。
なるほどそういうことだったんですね、大きなアプリケーションを使用したときは気をつけなければなりませんね。
以前にメモリが延々とたまってしまう現象に遭遇したときに苦労しました。。