AS3で陥りがちなメモリーリーク
AS3ではガベージコレクションによるメモリ管理が導入されています。
ガベージコレクションとは、簡単に言えば「どこからも参照されなくなったオブジェクトがころあいを見計らって勝手にメモリから消去される」仕組みです。
この「どこからも参照されなくなった」という条件がクセモノで、気をつけないとすぐにメモリーリークの原因になります。
メモリーリークの例
まず、キーボードが押されるとTESTというイベントを発行するSampleクラスを定義します。
import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import org.as3s.Document;
//Sample Class Ver.1
public class Sample extends Sprite {
public static const TEST:String = "test";
public function Sample() {
Document.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
}
private function onKeyDown(event:KeyboardEvent):void {
dispatchEvent(new Event(Sample.TEST));
}
}
}
次に、クリックする度にSampleクラスのインスタンスを作り直すMemoryLeakTestクラスを定義します。
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import org.as3s.Document;
//MemoryLeakTest Class Ver.1
public class MemoryLeakTest extends Document {
private var sample:Sample;
public function MemoryLeakTest() {
Document.addEventListener(MouseEvent.CLICK, onClickStage);
}
public function onClickStage(event:Event):void {
if (sample!=null) removeChild(sample);
sample = new Sample();
addChild(sample);
sample.addEventListener(Sample.TEST, test);
}
public function test(event:Event):void {
trace("test: "+event.target);
}
}
}
AS2的な考え方では一見問題ないように見えますが、このまま実行するとクリックする度にtestイベントが
どんどん重複されてしまいます。removeChildされたオブジェクトからも引き続きイベントが発行されている状態です。
では、removeChildする前にremoveEventListenerするように修正してみます。
import flash.display.Sprite;
import flash.events.Event;
import flash.events.MouseEvent;
import org.as3s.Document;
//MemoryLeakTest Class Ver.2 (removeEventListener)
public class MemoryLeakTest extends Document {
private var sample:Sample;
public function MemoryLeakTest() {
Document.addEventListener(MouseEvent.CLICK, onClickStage);
}
public function onClickStage(event:Event):void {
if (sample!=null) {
sample.removeEventListener(Sample.TEST, test);
removeChild(sample);
}
sample = new Sample();
addChild(sample);
sample.addEventListener(Sample.TEST, test);
}
public function test(event:Event):void {
trace("test: "+event.target);
}
}
}
これでイベントが重複することはなくなり一件落着に見えますが、このままではメモリーリークは解消されません。
なぜなら、Sampleクラスでキーボードイベントを受け取るためにstage(Documentクラスを利用しています)に対してaddEventListenerしているので、stageがこのオブジェクトの参照を持ち続けているからです。
そこでSampleクラスを次のように修正します。
自分自身がstageから削除されたらキーボードイベントもremoveEventListenerしています。
import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import org.as3s.Document;
//Sample Class Ver.2 (removeEventListener onRemovedFromStage)
public class Sample extends Sprite {
public static const TEST:String = "test";
public function Sample() {
Document.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
addEventListener(Event.REMOVED_FROM_STAGE, onRemovedFromStage);
}
private function onRemovedFromStage(event:Event):void {
Document.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
}
private function onKeyDown(event:KeyboardEvent):void {
dispatchEvent(new Event(Sample.TEST));
}
}
}
これでようやくメモリーリークは発生しなくなります。
実は、メモリーリークを防ぐだけであればWeak Reference(弱い参照)を用いた方法もあります。
「弱い参照」で参照されている場合は、ガベージコレクタはこれを参照とはカウントしなくなります。
具体的には次のように、addEventListenerの第5引数useWeakReferenceをtrueにします。
import flash.display.Sprite;
import flash.events.Event;
import flash.events.KeyboardEvent;
import org.as3s.Document;
//Sample Class Ver.3 (use Weak Reference)
public class Sample extends Sprite {
public static const TEST:String = "test";
public function Sample() {
Document.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown, false, 0, true);
}
private function onKeyDown(event:KeyboardEvent):void {
dispatchEvent(new Event(Sample.TEST));
}
}
}
この方法でも前述した明示的にremoveEventListenerする方法と同様、メモリーリークを防ぐことはできます。
しかし、この方法は「参照はされているのにガベージコレクションの対象になる」という状況をつくることになります。
実際に、MemoryLeakTestクラスを最初の状態(Ver.1)に戻してみると、
ガベージコレクションが実行されるまで、testイベントが重複されていくことがわかります。
このように、Weak Referenceを用いると、(いつ起こるかわからない)ガベージコレクションのタイミングで挙動が変わる可能性が生まれるため注意が必要です。
強制的にガベージコレクションを実行するハックも存在するようですが、将来のFlashPlayerで動作する保証はありませんのであまりオススメしません。
個人的には、addEventListenerしたものは明示的にremoveEventListenerすることを心がけた方がよいと思います。