assertGC()

2008.7.5

ActionScript3にて特定のオブジェクトがGCされたかを確認する話です。既出だったらどうしよう。。

http://d.hatena.ne.jp/kkanda/20080406/p1

上記の記事は監視オブジェクトの生存数の変化を通知してくれるものみたいですが、一つのオブジェクトにつき一つのディクショナリを作れば特定オブジェクトのGCをフックできそうです。以下のような感じで:

GC.watch({}, function():void {trace("gc!");});

さらにGC強制発動と併用すると、参照がどこにもなくなってGCされるべきオブジェクトを表明する関数が考えられます。表明をパスすれば安心して作業が進められそうですね。使用例は以下のような感じに:

package {
  import flash.display.Sprite;
  import flash.events.MouseEvent;

  public class Main extends Sprite {
    public var o:Object;

    public function Main() {
      o = {};
      var item:GCItem = GC.watch(o);
      stage.addEventListener(MouseEvent.CLICK, function(e:MouseEvent):void {
        o = null; //ここをコメントアウトすれば表明に違反してエラー
        item.assertGC(); //ここでエラーが起きなければGCされたことになる
      });
    }
  }
}

GC.watch()の戻り値を取っておき、参照がなくなったと思われる時点でassertGCを呼んでやれば、メソッド内部で強制GCを試みて、もし回収されていなかった場合にエラーを送出します。もっとも、強制GCの手法が非公式らしいのでいつまで機能するかは不明です。

ソースは以下のような感じになります:

package {
  import flash.display.Sprite;
  import flash.events.Event;
  import flash.net.LocalConnection;

  public class GC {
    private static var sprite:Sprite = new Sprite();

    public static function start():void {
      try {
        new LocalConnection().connect("foo");
        new LocalConnection().connect("foo");
      } catch (e:*) {}
    }

    public static function watch(value:Object, callback:Function = null):GCItem {
      var item:GCItem = new GCItem(value);
      value = null;
      sprite.addEventListener(Event.ENTER_FRAME, function(e:Event):void {
        if (!item.target) {
          callback && callback();
          e.target.removeEventListener(e.type, arguments.callee);
        }
      });
      return item;
    }
  }
}
package {
  import flash.utils.Dictionary;	

  public class GCItem {
    private var dict:Dictionary;

    public function GCItem(value:Object) {
      dict = new Dictionary(true);
      dict[value] = true;
    }

    public function get target():Object {
      for (var key:Object in dict) return key;
      return null;
    }

    public function assertGC():void {
      GC.start();
      if (target) {
        throw new Error("the object is not collected: " + target);
      }
    }
  }
}

一つ問題があって、最後の参照がなくなってから最低1フレーム経ってからGC.startしないと、GCの検出がうまかいかないような雰囲気です。従って、以下の例は期待に反して、表明に違反してしまいます。

var o:Object = {};
var item:GCItem = GC.watch(o);
o = null;
item.assertGC();

一方、以下のようにすれば期待通りに行きます。

var o:Object = {};
var item:GCItem = GC.watch(o);
o = null;
stage.addEventListener(Event.ENTER_FRAME, function(e:Event):void {
  item.assertGC();
  e.target.removeEventListener(e.type, arguments.callee);
});