Extension Managerのバージョンが古くて嵌まった

Mac版 Flash8からCS3にアップグレードした際にExtension Managerが1.8にバージョンアップされないという問題で嵌まりました。

CS3にアップグレードしてから、mxpファイルをインストールしようとするとFlash8以降のみインストールできますというようなエラーがでて、インストールが完了しませんでした。

原因がよくわからなくてそのまま放置していたのですが、実はCS3の登場とともにExtension Managerのバージョンが1.8になっていてCS3では旧バージョンのExtension Manager(1.7ぐらいだったかな?)を使えないということに気がつきました。

Adobeのページからダウンロードしてインストールしたら、正常にmxpファイルをインストールできるようになりました。

私の環境の問題なのかもしれないですが、念のためメモっておきます。

プロパティへのアクセス速度の比較

id:nishiohirokazuさんの少し前のエントリー「PointとArrayで速度比較」の、

ActionScriptの配列はC的な配列じゃないからかも。逆にクラスのメンバ変数はすでに配列として確保されていそう。xとかも整数にマップされてそう。

という推測が正しいのかを調べてみました。

まずは私もプロパティへ定数を代入する式の実行速度を比較してみました。

以下は、評価対象の式の一覧になります。各式を10,000,000回ループで実行した時の実行時間を//の後ろに記載しておきます。
また、pがPoint型、aがArray型、oがObject型となります。

p.x = 1; //216ms   (1)  
p["x"] =1; //1631ms   (2) 

a[0] = 1; //567ms   (3) 
a["0"] = 1; //2681ms   (4) 

o.x = 1; //2028ms   (5) 
o["x"] = 1; //2201ms (6)
o[0] =1; //1105ms (7)

メンバ変数へのアクセスが最速でした(1)。しかし、添字を使ってアクセスするとかなり遅くなります(2)。

2番目に速かったのが数値の添字でArrayにアクセスした場合でした(3)。しかし、文字列の添字でアクセスすると遅くなりました(4)。

一番遅かったのが文字列の添字でObjectのプロパティにアクセスした場合です(6)。これに対しObject型へのアクセスでもuint型の添字でアクセスすると速くなりました(7)。

何故このような結果になったのかを理解するために、avm2overview.pdfとtamarinソースコードを読んでみました。以下では、まずどうしてこのような結果になったのかという疑問に対する私の回答を述べます。その後、その根拠となるコードをtamarinから一部抜粋して説明します。

メンバ変数へのアクセスが速いのはなぜか?

逆に遅いのはなぜかを考えると話は簡単になります。遅いのは、すべてのアクセスはハッシュテーブルを基本として設計されているからです。

幸いなことにメンバ変数のアクセスでは、ハッシュテーブルのインターフェースを使わないで直接変数にアクセスする方法を使えるので高速になります。この仕組みは「slot」と呼ばれます。

このslotを通してメンバ変数へアクセスされるのは、添字を使わなかった場合のみです。

p.x =1;

はslotを使われますが、

p["x"] =1;

という式では、使われません。何故かというとこの式は、

var propName:String = "x";
p[propName] =1;

と書き直す余地があり、構文上propNameには任意の文字列を設定出来ます。したがって、添字を使ったプロパティへのアクセスは実行時に変更される可能性があるわけです。

それに対し、

p.x =1;

という式は、実行時にxの部分を書き換える「構文が存在しません」。そのため、コンパイルされたコードは必ずメンバ変数xを参照することが保証されます。

以上の仕組みから、同じメンバ変数であっても添字でアクセスすると実行時に変更される可能性があるとみなされslotで処理されなくなります。

この話は、AVM2のマルチネーム(Multiname)という仕様に密接に関わっており少し複雑な話なのであとで別エントリーにまとめようと思います。

クラスで定義されていないプロパティへのアクセスは辞書へのアクセスとして扱われる

これはどういうことかというと、(5)のような

o.x =1;

というクラス定義に存在しないプロパティにアクセスした場合、Objectが標準で持つハッシュテーブルへのアクセスとして処理されます。つまり、

o["x"] = 1;

と書いたとしても、本質的には変わらないということです(通過するコードに若干違いがあります)。

今回の調査したtamarinソースコード

今回調査したコードは、主にプロパティへの代入に関わる「setproperty」オペレータの実装部分です。

プロパティへの代入が発生すると、コンパイラ

p.x =1;

というコード(pがローカル変数として存在していることを仮定)であれば、

getlocal1          
pushbyte           1
setproperty        :x

というバイトコードが出力されます。:xはプロパティの名前のようなもの(正確にはname indexといいます)です。

また、添字でアクセスした場合は、

 p["x"] = 1;
getlocal1          
pushstring         "x"
pushbyte           1
setproperty        private,,,Main,Main,flash.display:Sprite,flash.display:DisplayObjectContainer
,flash.display:InteractiveObject,flash.display:DisplayObject,flash.events:EventDispatcher,Object,flash.display:IBitmapDrawabl
e,flash.events:IEventDispatcher:null

というように値がプッシュされる前に添字がプッシュされています。setpropertyの引数がエラいことになってますが、これの詳細は不明です。
コンパイル時にプロパティ名が確定していない場合に現われるようです。

tamarinの実際のコード(core/Interpreter.cpp、907行目付近)※説明のために注釈が入っています

                        case OP_setproperty: 
                        {
                                Multiname multiname; 
                                pool->parseMultiname(multiname, readU30(pc)); //(a)
                                Atom value = *(sp--); //(b)
                                if (!multiname.isRuntime()) //(c)
                                {
                                        Atom obj = *(sp--);
                                        toplevel->setproperty(obj, &multiname, value, toplevel->toVTable(obj)); //(d)
                                }
                                else
                                {
                                        if(multiname.isRtns() || !core->isDictionaryLookup(*sp, *(sp-1))) { //(e)
                                                sp = initMultiname(env, multiname, sp);
                                                Atom obj = *(sp--);
                                                toplevel->setproperty(obj, &multiname, value, toplevel->toVTable(obj));
                                        } else {
                                                Atom key = *(sp--);
                                                Atom obj = *(sp--);
                                                AvmCore::atomToScriptObject(obj)->setAtomProperty(key, value); //(f)
                                        }
                                }
                continue;
                        }

(a) setpropertyに渡されたname indexに対応するMultinameオブジェクトを生成しています
(b) スタックからプロパティに代入するvalueを取得しています
(c) isRuntime()が偽になるとき、コンパイル時に定義されているプロパティへのアクセスとみなされます
(d) Toplevelのsetproperty()メソッドをコールします。
(e) 添字によるアクセスの場合、isDictionaryLookup()が真になり(f)が実行されます。
(f) Objectが標準でもつハッシュテーブにアクセスします

core/Toplevel.cpp、1059行目付近

    void Toplevel::setproperty(Atom obj, Multiname* multiname, Atom value, VTable* vtable) const
    {
                Binding b = getBinding(vtable->traits, multiname); //(g)
                setproperty_b(obj,multiname,value,vtable,b);
        }

        void Toplevel::setproperty_b(Atom obj, Multiname* multiname, Atom value, VTable* vtable, Binding b) const
        {
        switch (b&7)
        {
		(途中省略)
                case BIND_VAR: //(h)
                {
                        #ifdef DEBUG_EARLY_BINDING
                        core()->console << "setproperty slot " << vtable->traits << " " << multiname->getName() << "\n";
                        #endif
                        int slot = AvmCore::bindingToSlotId(b);
                        AvmCore::atomToScriptObject(obj)->setSlotAtom(slot, 
                                coerce(value, vtable->traits->getSlotTraits(slot)));
            return;
                }
		(途中省略)
                case BIND_NONE: //(i)
                {
                        #ifdef DEBUG_EARLY_BINDING
                        core()->console << "setproperty dynamic " << vtable->traits << " " << multiname->getName() << "\n";
                        #endif
                        if (AvmCore::isObject(obj))
                        {
                                AvmCore::atomToScriptObject(obj)->setMultinameProperty(multiname, value);
                        }
                        else
                        {
                                // obj represents a primitive Number, Boolean, int, or String, and primitives
                                // are sealed and final.  Cannot add dynamic vars to them.

                                // property could not be found and created.
                                throwReferenceError(kWriteSealedError, multiname, vtable->traits);
                        }
                        return;
                }

(g) Multinameオブジェクトとvtableからbindingを取得します。bindingがプロパティの種類を決定しています。
(h) bindingが変数の場合、slotを通してvalueがセットされています。
(i) bindingが無い場合、辞書へのアクセスとして処理されます。(e)で辞書以外と判定された場合はここを通過しているようです。

tamarinのコード内で出てくるpropertyは、AS3のプロパティとは異なります。tamarin内では、メンバ変数、メソッドなどをひっくるめてプロパティと呼んでいるみたいです(詳細不明)。

まとめ

id:nishiohirokazuさんの推測はずばり当たっていました。

ActionScriptの配列はC的な配列じゃないからかも。

添字を使ったアクセスはすべてディクショナリとして扱われています。

逆にクラスのメンバ変数はすでに配列として確保されていそう。xとかも整数にマップされてそう。

メンバ変数はインスタンス生成時にその領域を確保されており、slotという機構でアクセスが高速化されています(アルゴリズム的にはO(1))。

ソースコード

ここからダウンロードできます。

参照

  1. avm2overview.pdf
  2. http://wiki.libspark.org/wiki/AVM2/Overview - Sparkプロジェクトのここの日本語訳が結構参考になりました。2章のあたりを一部翻訳したので今度アップしようと思います。

[flash][scheme][SICP] Flashで図形言語の描画ツールを作りました(SICP2章の話)

このGW中、ずっと計算機プログラムの構造と解釈(通称SICP)を読みふけっておりました。

この本はいわゆる計算機科学の入門書で、本の中に出てくるプログラムはLISPの方言であるschemeで書かれており、
慣れないと少し取っ付きづらい本です。

この本の2章に「図形言語」という節では、schemeを使って図形描画を行ってみるという部分があります。

私は普段Macbookgaucheというschemeの処理系をインストールしてプログラムを書いています。
この「図形言語」のプログラムをgaucheで書くにはOpen GL拡張であるgl-gaucheをインストールする必要があります。
id:higeponさんのこのエントリーを参考にしながらやってみたところ、
インストールはうまく行きました。しかし、起動するとX関係のエラーが出て動きませんでした。

問題を解くためだけに環境をあれこれいじるのも不毛だと感じ、プログラムを動かすのをあきらめて、とりあえず本だけ読みすすめていくことにしました。
図形言語の中盤あたりまで読んでみると、どうやら直線を引くぐらいができれば良いということが分かり、
それならFlashで速攻できるのでは?と思い、勢いあまってFlashで描画部分を作ってみました。

下の絵は、今回作成した描画ツールを使って書きました。(実装の本体はschemeで、Flashでは線引いてるだけですがw)


使い方

p.80あたりまで読むと手続きdraw-lineの実装が何処にも書いてなくて萎えますが、以下のdraw-lineを自分のプログラムに
組み込んで下さい。

(define (draw-line v1 v2)
    (display (xcor-vect v1))
    (display ",")
    (display (ycor-vect v1))
    (display ",")
    (display (xcor-vect v2))
    (display ",")
    (display (ycor-vect v2))
    (newline))

draw-lineは2つのベクターv1,v2を引数にとり、そのx座標とy座標をカンマ区切りで出力します。

また、実際にペインターで描画するときに渡すフレームは次の定義を使って下さい。

(define canvas-frame (make-frame (make-vect 0 0) (make-vect 400 0) (make-vect 0 400)))

以上で、描画の準備は整いました。
あとは、

(wave canvas-frame)

という風に、paintercanvas-frameを適用させれば次のようなcsv形式の座標データが得られます。

0.0,332.0,72.0,280.0
72.0,280.0,104.0,300.0
104.0,300.0,160.0,300.0
160.0,300.0,140.0,348.0
140.0,348.0,160.0,400.0
240.0,400.0,260.0,348.0
260.0,348.0,240.0,300.0
240.0,300.0,304.0,300.0
304.0,300.0,400.0,100.0
0.0,296.0,80.0,184.0
80.0,184.0,104.0,260.0
104.0,260.0,152.0,200.0
152.0,200.0,112.00000000000001,0.0
160.0,0.0,200.0,80.0
200.0,80.0,240.0,0.0
300.0,0.0,260.0,200.0
260.0,200.0,400.0,40.0
168.0,320.0,200.0,308.0
200.0,308.0,231.99999999999997,320.0

これを描画Flashの右側のテキストエリアにコピペして「描画」ボタンを押すと、

こんな画像が表示されます。

追記(5/8)

座標形とデータのフォーマットについての説明が不足していたので追記しておきます。

このツールの座標形は、MITのschemeにあわせて左下(0,0)で右上(400,400)になっています。なので、SICPの練習問題はそのままで動きます。

また、数値の並びは、

x1,y1,x2,y2

となっており、1行が線分(x1,y1)-(x2,y2)を表しています。

まとめ

とりあえず、環境構築しなくてもコピペの手間を惜しまなければ図形言語の問題を解ける環境ができました。

ここで取り上げられている内容はかなり興味深いので面倒だからパスしてしまった人もこれを機に再挑戦してもらえればありがたいです。

計算機プログラムの構造と解釈

計算機プログラムの構造と解釈

BitmapDataを使ったParticle Systemの実装

前からずっと気になっていたParticle Systemの実装方式について調べました。

調査を進めてみると、SpriteをわずBitmapDataを使って描画をするという選択肢もあるということが分かり新鮮でした。
ということで、今回はBitmapDataを使ったParticleSystemの実装を解説します。

そもそもParticle Systemは、粒子(particle)の動きをシミュレーションし粒子の状態に基づいて画面描画するという動作を繰り返します。

この「動きのシミュレーション」と「画面描画」を分けるのが非常に重要で、調査前の私の理解ではこの境界が曖昧でした。

Spriteを使って描画するParticle Systemの実装は、たまに見かけるのであえて私はあえてBitmapDataに直接書き込む方式で実装してみました。
実装するにあたって、aM laboratoryのparticlesを参考にしました。というか、ほぼそのままです。

描画方法:setPixel() + BlurFilter

BitmapData上にあるParticleの位置にsetPixel()で点を描画し、BlurFilterを適用する方法です。
この方法はとても単純だけど、色々と使い勝手がありそうです。

コードは、以下のようになります。

var buffer:BitmapData = new BitmapData(WIDTH, HEIGHT, false, 0);
var blur:BlurFilter = new BulrFilter(2,2,1);
(中略)
buffer.setPixcel(particle.x, particle.y, particle.color);
buffer.applyFilter(buffer, buffer.rect, ORIGIN, blur);

この方法を使ったサンプルは、次の通りです。

画面中央下からParticleが沸き出してきます。沸き出した際に初速度をランダムで与え、以降はy軸方向を等速、x軸方向をバネの動きにしています。
以上の設定だと画面中央付近のParticleの密度が高くなり炎っぽく見えます。

描画方法:setPixel()のみ

次は単純にsetPixel()のみで描画したサンプルです。

Particleが動き始めた後に外力を加えてないのでParticleの生存時間を記録していれば、逆再生でもとの状態に戻すことができます。
このサンプルでは、逆再生する際に2ステップずつ戻すことで倍速を実現しています。

for ( i = 0; i < particles.length; i++ ) {
   p = particles[i];
   if ( p.lifetime > 0 ) {
       if ( p.lifetime % 2 != 0 ) {
           p.x -= p.vx;
           p.y -= p.vy;
           p.lifetime -= 1;
       }
       else {
           p.x -= p.vx * 2;
           p.y -= p.vy * 2;
           p.lifetime -= 2;
       }
   }
   output.setPixel(p.x, p.y, p.color);
}

ソースコード

ここからダウンロードできます。

(おまけ)Particle Systemとは?

そもそもParticle Systemの定義ってなんなのかいまいちピンと来なかったので、Particleの原著(?)に当たってみました。
原著によると、Particleは、

  • 位置
  • 速度
  • サイズ
  • 色(アルファ含む)
  • 生存時間

の属性を持ち、時間とともにその状態を変化させます。

作成するParticleが必ずしもすべての属性を持たなければならないという訳ではないですが、上記に上げた項目はよく使われます。

ベジェ曲線を使ったアニメーション(リプレイ機能付き)

突然ですが3次ベジェ曲線だと制御点が4つもあるので、スクリプトで制御する際にコードから描画イメージが沸きづらくないですか?
グラフィックソフトで普段からベジェ曲線に慣れている人であればそうでも無いのかもしれないですが。。。

というわけで制御に慣れるために、マウスに連動してベジェ曲線を描画するサンプルを作ってみました。

ベジェ曲線の描画は、id:nitoyonさんのところからcurveTo()を使った3次ベジェ曲線の実装を拝借しています。

今回の実装では、4つの制御点のうち始点と終点はマウス位置にしています。そうすることで鱗形の曲線が描かれます。
ほんとは4つ全部をバラバラに動かした方が制御に慣れるんでしょうが、僕のレベルでは4つを奇麗にまとめて動かす方法が思いつきませんでした。

それとベジェ曲線とは全然関係ないんですが、マウスの動きをリプレイするコードを実装してみました。
単純に、マウス位置と時間を記録してTimerを使って再生しているだけですが。

ソース一式はこちらからダウンロードできます。
コンパイルにはTweenerが必要です。

以下、コード(一部)です。

package {
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.TimerEvent;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	import flash.utils.Timer;
	import flash.utils.getTimer;
	import caurina.transitions.*;
	import com.potix2.utils.HSV;

	[SWF(width="800", height="600", backgroundColor="#000000", frameRate="30")]
	public class BezierTest extends Sprite {
		private var NUMBER_OF_CURVES:uint = 30;
		private var myCurves:Array;
		private var currentCursor:uint = 0;
		private var prevX:Number;
		private var prevY:Number;

		private var recCurves:Array;
		private var recCurrentCursor:uint = 0;
		private var recPrevX:Number;
		private var recPrevY:Number;
		private var replayData:Array;
		private var replayIndex:uint;

		private var recWaitTimer:Timer;
		private var replayTimer:Timer;
		private var counter:Number;
		private var divisions:uint;
		private var visibleCurves:uint;
		private var colorVelocity:Number;
		private var replaying:Boolean;
		private var recording:Boolean;
		public function BezierTest() {
			recording = false;
			replaying = false;

			counter = 0;
			currentCursor = 0;
			divisions = 10;
			visibleCurves = 5;
			colorVelocity = 1.2;

			recWaitTimer = new Timer(1000, 1);
			initCurves();
			initListeners();
		}

		private function initCurves():void {
			myCurves = new Array();
			var i:uint;
			var curve:BezierCurve;
			for (i = 0; i < NUMBER_OF_CURVES; i++ ) {
				curve = new BezierCurve(new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0));
				curve.color = 0;
				curve.divisions = this.divisions;
				addChild(curve);
				myCurves.push(curve);
			}

			// for replay
			recCurves = new Array();
			for (i = 0; i < NUMBER_OF_CURVES; i++ ) {
				curve = new BezierCurve(new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0));
				curve.color = 0;
				curve.divisions = this.divisions;
				addChild(curve);
				recCurves.push(curve);
			}
		}

		private function initListeners():void {
			stage.addEventListener(MouseEvent.MOUSE_MOVE, onMouseMove);
			recWaitTimer.addEventListener(TimerEvent.TIMER, function(event:TimerEvent):void { startReplay(); });
		}

		private function drawCurves(curves:Array, x0:Number, y0:Number, x1:Number, y1:Number, startIndex:uint):void {
			var dx:Number = x1 - x0;
			var dy:Number = y1 - y0;

			for ( var i:uint = 0; i < visibleCurves; i++ ) {
				var curve:BezierCurve = BezierCurve(curves[(startIndex + i) % NUMBER_OF_CURVES]);
				curve.p0 = new Point(x1, y1);
				curve.p1 = new Point(x1 - dx * 10, y1 - dy * 10);
				curve.p2 = new Point(Math.random() * stage.stageWidth, stage.stageHeight * Math.random());
				curve.p3 = new Point(x1, y1);
				curve.hsv = new HSV((counter * colorVelocity) % 360, 0.8, 1.0);
				curve.visible = true;
				curve.draw();
				Tweener.addTween(curve, {
					time: 0.2,
					transition: "easeInSine",
					v: 0,
					onUpdate: function():void {
						this.draw();
					},
					onComplete: function():void {
						this.visible = false;
					}
				});
			}
		}

		private function onMouseMove(event:MouseEvent):void {
			recWaitTimer.reset();
			if ( !replaying && !recording ) {
				recording = true;
				replayIndex = 0;
				replayData = new Array();
			}

			counter++;
			drawCurves(this.myCurves, prevX, prevY, mouseX, mouseY, currentCursor);
			prevX = mouseX;
			prevY = mouseY;
			currentCursor = (currentCursor + visibleCurves) % NUMBER_OF_CURVES;

			if ( recording ) {
				replayData.push({
					x: mouseX,
					y: mouseY,
					time: getTimer()
				});
				recWaitTimer.start();
			}
		}

		private function startReplay():void {
			replayIndex = 0;
			recording = false;
			replaying = true;

			if ( replayTimer != null ) {
				replayTimer.removeEventListener(TimerEvent.TIMER, replayDraw);
			}
			replayTimer = new Timer(10, 1);
			replayTimer.addEventListener(TimerEvent.TIMER, replayDraw);
			replayTimer.start();
		}

		private function replayDraw(event:TimerEvent):void {
			counter++;
			var cd:Object = replayData[replayIndex];
			drawCurves(this.recCurves, recPrevX, recPrevY, cd.x, cd.y, recCurrentCursor);
			recPrevX = cd.x;
			recPrevY = cd.y;
			recCurrentCursor = (recCurrentCursor + visibleCurves) % NUMBER_OF_CURVES;
			replayIndex++;
			if ( replayIndex == replayData.length ) {
				// end
				replaying = false;
			}
			else {
				if ( replayTimer != null ) {
					replayTimer.removeEventListener(TimerEvent.TIMER, replayDraw);
				}
				replayTimer = new Timer(replayData[replayIndex].time - replayData[replayIndex - 1].time, 1);
				replayTimer.addEventListener(TimerEvent.TIMER, replayDraw);
				replayTimer.start();
			}
		}
	}

}

GCWatcher:GCの起動とオブジェクトの回収状況を監視するための方法


GCを強制的に起動する方法について追記しました(4/8)


fladdictさんAS3でガベージコレクションを見張る画期的方法メモにて、


弱参照のDictionaryのオブジェクトキーを利用して、対象のオブジェクトがガベッジコレクションされたかどうかを見張ることができるんじゃね??

という面白そうなお題があがっていたので実装、検証を行ってみました。

検証のために実装したクラスGCWatcherには、次の二つの機能があります。

  1. GCが起動した場合にGARBAGE_COLLECTイベントを発行する
  2. 監視したオブジェクトを監視リストに追加し、GCに回収された場合にCHANGEイベントを発行する

このクラスを使うと、fladdictさんがエントリー中で書いていた、


onGarbageCollection イベントを発行できるし、DictionaryのキーをダンプすればGCされそこねたオブジェクトの一覧が取得できるんじゃないかと。

ということは実現できました。

しかし、オブジェクト一覧を取得できてもDisplayObject系のオブジェクトは強制的にメモリを解放する方法がないのでFlash PlayerのGCにいらついているFlasherの救済にはならないようです。

BitmapDataであれば、強制的に解放できるので利用価値があるかもしれないです。

実装方法

GARBAGE_COLLECTイベントの発行方法は、fladdictさんがエントリー中に書いていた方法の通りで、Dictionaryを弱参照で作成しTimerでオブジェクトがなくなっていないかを定期的にチェックしています。

また、この方法はid:nitoyonさんのブクマコメントにあったこの記事でも実装されています。

監視リストの作成も同様に弱参照のDictionaryを使って作成しています。

GCWatcherの使い方とサンプルなどソース一式は、このエントリーの最後に有ります。

次にGCWatcherを使ったサンプルを見ながら、GCの挙動を検証してみましょう。

BitmapDataのインスタンスを生成しつづけた場合のGCの挙動

このサンプルでは、BitmapDataを10x10の100個を50msごとに生成し、100msごとにGCの挙動をチェックしています。

画面左下に表示されるLeft/Watchedのうち、Leftが監視リストに残っているオブジェクトの個数です。

Watchedは前回のチェックから今回のチェックまでの間に監視していたオブジェクトの数です。(前回チェック時のLeft + 新規に追加されたオブジェクトの個数)

また、GCの起動を検出すると「GC!」と表示されます。監視リストが更新された場合は、「Changed!」と表示されます。

サンプルを眺めていると、GCがかなり高頻度で起動されているのがわかります。

また、毎回BitmapDataが回収されているのも確認できます。ただ、理論上Leftは100でなければならないのになぜか200で安定します。

これはGCWatcherの実装が問題なのか、それともGCが回収するタイミングの問題なのか詳細は不明です。

removeChild()をされたオブジェクトがGCに回収されるまでの挙動

次にDisplayObject系のオブジェクトがGCに回収される様子を観察するためのサンプルを紹介します。

このサンプルでは100個のSpriteを作成し画面にランダムな色と大きさの円を描いています。

すべてのSpriteは、GCWatcherの監視リストに追加されています。

また、表示された円をクリックするとイベントハンドラを残したままremoveChild()が呼び出されます。

このサンプルを動かして、円をクリックすると表示リストから外れるので表示が消えます。

しかし、左下の情報をみるとGCによってSpriteが回収されていないことが確認できます。

removeEventListener()を読んでないせいかと一瞬思うのですが、このままずっとサンプルを表示し続けると10分後くらいにGCによってSpriteが回収され左下の表示が変わります。

GCがどのタイミングでSpriteを回収するのかは、正直よくわからないのですが他のFlashを表示すると比較的すぐに回収されていくように感じます。

GCは相変わらず高頻度で起動されているので、この差は何なのか謎です。DisplayObjectあたりの実装に何か関係があるのかもしれないです。

GCWatcherの使い方

GCWatcherは、コンストラクタに更新確認する間隔をミリ秒で指定します。

watcher = new GCWatcher(100);
watcher.addEventListener(GCWatcher.GARBAGE_COLLECT, onGarbageCollect);
watcher.addEventListener(Event.CHANGE, onChange);			

また、監視リストに追加する場合はwatch()メソッドに渡します。

watcher.watch(bmpData);

今回のサンプルも含めてソース一式はここからダウンロードできます。

まとめ

fladdictさんのメモ先人の知恵にあるよう、Dictionaryの弱参照を使うとGCの起動を監視することができました。

ただ、BitmapDataなど一部のオブジェクト以外にはメモリ解放を強制させる方法がないのでFlash Playerのメモリ使用量が多くていらついているFlasherのエクスカリバーとはならないようです。

今後の課題

Tamarinで採用されているMMgcの挙動については、ここに載っているので今回作ったGCWatcherと会わせれば
もう少しGCの挙動を把握できるのではないかと思いました。ただ、アルゴリズム的な話になってくるので理解するのが面倒です。。。

それと、結局DisplayObject系のオブジェクトを監視してもメモリ解放できないので監視出来てもあまり役に立たないのではないかと思ってしまいました。

またBitmapDataを監視した場合は、監視リストから使用中と未使用のオブジェクトを判別するための効率的な方法を考える必要が有るかと思います。

オブジェクトの数がすくなければ線形探索でよいですが、まともにやるとO(n^2)なので微妙です。

追記(4/8):GCを強制的に起動する方法について

昔のブクマを見直してたらgskinner.com:gBlog: AS3: Resource Management pt 3という記事にGCを強制起動する方法が載っていました。この方法はオフィシャルで、サポートされていない方法なのであくまで開発時に使うようにして下さい。

この方法を追加した、2個目のサンプルを参考までにここにアップしておきます。

FlickrViewer:as3flickrlibを使ってFlickrから画像を読み込む方法

Flickrを検索して表示するViewerを作ってみました。

画像の取得には、Adobe Labsで作成されたas3flickrlibというライブラリを使っています。

このライブラリは、サンプルコードも付属しておらずあまり解説している記事もみかけないので試しに使ってみました。

また、Flickrへのアクセスはクロスサイトのデータアクセスとなるためcrossdomain.xmlを読み込みが必要となります。その方法についても説明します。

as3flickrlibとは

as3flickrlibは、Adobe Labsで作成されたas3用のFlickr APIラッパーライブラリです。

このライブラリを使うとURLLoaderなどを使って直接APIを呼び出した場合と比べて、データの受け渡しが楽になるなどのメリットがあります。

今回サンプルとして取り上げるのは検索用APIのみです。アップロードAPIに関しては、hatayanさんのこのエントリーが参考になります。

as3flickrlibを使うための準備

as3flickrlibはas3corelibに依存しているため下記URLから先にダウンロードして下さい。

http://code.google.com/p/as3corelib/

次に、as3flickrlibを下記URLからダウンロードして下さい。

http://code.google.com/p/as3flickrlib/

ダウンロードしたZIPファイルを展開し、src/以下のファイルをクラスパスが通っているディレクトリにコピーして下さい。

私が使用したas3flickrlibのバージョンは、0.87なのですが、このバージョンには画像のURL生成に必要なfarm idを取得できないという欠陥があったため下記の修正を行いました。パッチファイルもこちらに用意しましました。

as3flickrlibがfarmを無視している問題を回避するための修正点

src/com/adobe/webapis/flickr/Photo.asに追加

private var _farm:String;
public function get farm():String {
	return _farm;
}
		
public function set farm( value:String ):void {
	_farm = value;
}

farm idを格納するためのメンバ変数とプロパティを追加しました。

src/com/adobe/webapis/flickr/methodgroups/MethodGroupHelper.asの370行目に追加

photo.farm = p.@farm.toString();

検索結果をPhotoオブジェクトに追加している箇所でfarm idも読み込むように修正しました。

as3flickrlibのページをみると既にバグ報告としてあがっているのでそのうち修正されるのではないかと思います。

as3flickrlibを使ってみる

実際にas3flickrlibを使って、キーワードに一致した写真の一覧を取得し画像のURLを生成するところまで紹介します。また、crossdomain.xmlの読み込み方についても説明します。

はじめに、com.adobe.webapis.flickr.FlickrServiceのインスタンスを作成します。このとき引数に渡すAPI KEYはFlickrのアカウントを作成すれば取得することができます。


this.flickr = new FlickrService(FLICKR_APIKEY);

つぎに先ほど作成したインスタンスイベントハンドラを登録します。今回はキーワード検索を実行しその結果を取得したいので、PHOTOS_SEARCHイベントのハンドラを登録します。


this.flickr.addEventListener(FlickrResultEvent.PHOTOS_SEARCH, flickr_onPhotosSearch);

イベントハンドラが受け取るイベントはFlickrResultEvent型で、successプロパティにAPIの呼び出しが成功したかどうかが設定されます。
APIの実行結果は、dataプロパティに設定されます。


private function flickr_onPhotosSearch(event:FlickrResultEvent):void
{
if ( event.success )
{
var photos:Array = PagedPhotoList(event.data['photos']).photos;
for ( var i:int = 0; i < photos.length; i++ )
{
var imageUrl:String = 'http://farm' + photos[i].farm + '.static.flickr.com/' +
photos[i].server + '/' + photos[i].id + '_' + photos[i].secret + '.jpg';
var loader:Loader = new Loader();
loader.contentLoaderInfo.addEventListener(Event.COMPLETE, loader_onComplete);
loader.load(new URLRequest(imageUrl), new LoaderContext(true));
}
}
}
Photos.search()を呼び出した際の実行結果はPagedPhotoListオブジェクトとして返されます。

このオブジェクトはFlickr APIのsearchを呼び出した際の実行結果そのものなのでFlickrAPIを使ったことがある人であればどのような結果が返ってきているかで迷うことはないかと思います。

Flickrのサムネイル画像のURLを生成するには、farm idが必要になります。URLのフォーマットについては寺子屋未満さんこのエントリーを参考にしました。

Flickrではサムネイル画像が保存されているfarmごとにcrossdomain.xmlが配置されています。そのため、Loaderのload()オブジェクトの第2引数にcheckPolicyFileプロパティをtrueに設定したLoaderContext()を渡します。

LoaderはcheckPolicyFileがtrueになっている場合、crossdomain.xmlを探すようになります。サンプルコードでは、LoaderContext()のコンストラクタの第1引数で設定しました。

最後にFlickr APIを呼び出します。呼び出しは、最初に作成したFlickrServiceオブジェクトのプロパティを通して行います。


this.flickr.photos.search("", "", "any", keyword);
この呼び出しもFlickr APIflickr.photos.searchそのものです。ただし、APIKEYはFlickrService生成時に渡したものが使われるため個々のAPI呼び出しでは不要です。

で、使用感はどうだったのか?

API呼び出しの結果を自分で解析しなくて良いことはメリットに感じられました。その点に関しては、写真をアップロードしようと試みた場合に恩恵があるのだろうと思います。

また、Flash で Flickr に写真をアップロードするサンプル(FlashCS3 + Action Script3)によると


※残念ながらwww.flickr.comのサーバーに"crossdomain.xml"がないので(api.flickr.comにはある。)swfをwebにアップした状態からはアップロードの処理がうまく動きませんでした。ローカルで実行してください。

ということなので、このライブラリの利用はAIRアプリにFlickrへのアップロード機能を付けたい場合など使用方法は限定される感じがします。

ソースコードコンパイル方法

ファイル一式はここからダウンロードして下さい。コンパイルには、flex3 sdkPapervision3D 2.0とas3corelibとas3flickrlibが必要です。

Papervision3D 2.0はまだアルファリリースなのでsvnから直接ダウンロードするしか入手方法がありません。svnコマンドが使える環境であれば、以下のコマンドで入手出来ます。

$svn co http://papervision3d.googlecode.com/svn/trunk papervision3d

コマンド実行後、branches/GreatWhite/src以下にクラスパスを通して下さい。

また、ソースファイルにはfickrのAPIKEYが含まれていないので各自取得して自分のAPIKEYを入力して下さい。

最後に

今回は、as3flickrlibの説明をメインに書きましたが、Papervision3D 2.0を使ってみた感想を後日レポートしたいと思います。