2010年2月15日月曜日

Flash の SWC は使ってはいけない

タイトルからしてなかなかひどいですが、調査結果もなかなかひどいです。

■結論:fl.controls(最初からFlashに付属しているコンポーネント)を含むSWCを作ると正しく読み込まれない
まずはこちらに検証用のプロジェクトを用意しましたので、ご覧になってみてください。
package {
import flash.display.MovieClip;

public dynamic class Abesi extends MovieClip
{
public function Abesi() {
trace("abesi");
trace(this.button, this.textArea);
}
}
}
package {
import flash.display.MovieClip;

public dynamic class Hidebu extends MovieClip
{
public function Hidebu() {
trace("hidebu");
trace(this.button, this.textArea);
}
}
}
とまぁ、なんの変哲もないタダのFlashコードをswcとして出力し、TestMainの中でnewして出力しているだけなのですが、なんとswcに書き出すタイミングに応じて確実にエラーになって落ちるというひどい問題があるようなのです。


■検証結果
以下のスプレッドシートにまとめてみました。
http://spreadsheets.google.com/ccc?key=0AoXhhCSOuqOtdE5rNy1wc2N2Z2JuV1NPUFBjdlRNeHc



たぶん私の書き出し方に何か問題があるのだろうとは思っているのですが、何処を見てもそれに関する情報が見あたらなかったので解決できず、結局swcの使用はあきらめることにしました><

2010年2月8日月曜日

ActionScript 3 の flash.net.Loader が読み込んだ MovieClip の挙動がローカルファイルとリモートファイルで変わるみたいです

flash.net.Loaderを用いて、リモートサーバーからswfファイルをロードし、画面に表示するようなアプリを書いていたのですが、そのときふとしたことから以下のような問題に気づきました。


■前提条件
仮に以下のような条件があるとします。
  • main.swfとresource.swfがあって、main.swfはflash.net.Loaderクラスを用いてresource.swfをロードしてくるものとする。
    var loader:Loader = new Loader();
    loader.load(new URLRequest("http://akisute.com/static/swf/sample.swf"); //リモートサーバーからロード
    loader.load(new URLRequest("../swf/sample.swf"); //ローカルファイルからロード
  • ロードしてきたswfは、以下のコードでloader.loaderInfo.contentから取り出して、MovieClipとして処理する。Loaderは破棄する。
    function onComplete(event:Event):void {
    var loaderInfo:LoaderInfo = event.target as LoaderInfo;
    var content:MovieClip = loaderInfo.content as MovieClip;
    var mc:MovieClip = content.getChildAt(0) as MovieClip;
    }
  • ロードしてきたswf(以下resourceと呼称)には合計200フレームのタイムラインがあり、1から100フレーム目までにleftというラベルがついて左向きのデータが、101から200フレーム目までにrightというラベルがついて右向きのデータが格納されている。1から100フレーム目までに右向きのデータは存在しない。その逆もしかり。


■問題
  • 対象のファイルresource.swfを、ファイルシステムからロードしたときは、なんの問題もなくラベルleftとrightを入れ替えることができる。
  • 対象のファイルresource.swfを、httpでリモートサーバーからロードしたときは、leftとrightを入れ替えた瞬間に、resourceのアニメーションがバグる。stop()やgotoAndPlay()などの命令を無視した動きをする。
  • ローカルファイルとリモートサーバー、どちらのresource.swfも完全に同一のファイル。ファイルサイズもタイムスタンプもsha1ハッシュも完全に一致。
・・・なにこれ。最初に見かけたときはAdobe焼き討ちじゃコラと思ったものです。


■調査:取ってきたswfをdescribeTypeしてみる
愚痴っても仕方がないので調査します。まずは取ってきたswfに対して、flash.util.describeType()を実行し、オブジェクトダンプしたXMLを見てみます。するとローカルファイルから取ってきた場合とリモートサーバーから取ってきた場合で、以下のような違い(diff)がある事が分かりました。
1c1
< ## LOCAL ##
---
> ## REMOTE ##
5,6c5
< <type name="sample_fla::left_33" base="flash.display::MovieClip" isDynamic="true" isFinal=&quot;false" isStatic="false">
< <extendsClass type="flash.display::MovieClip"/>
---
> <type name="flash.display::MovieClip" base="flash.display::Sprite" isDynamic="true" isFinal="false" isStatic="false">
15,18d13
< <variable name="leftArm" type="flash.display::MovieClip"/>
< <variable name="leftLeg" type="flash.display::MovieClip"/>
< <variable name="leftHead" type="flash.display::MovieClip"/>
< <variable name="leftBody" type="flash.display::MovieClip"/>
diffなのでちょっとわかりにくいかもしれませんが、よく見ると以下のような差異があることが分かります。
  • LOCALにはきちんとしたクラス名がついており、MovieClipのサブクラスになっている
  • LOCALにはプロパティ情報がきちんと残されている
  • REMOTEはタダのMovieClipクラスになっており、プロパティも一切無い
つまり、全く同一のファイルをロードしているにもかかわらず、flash.net.Loaderは、ローカルファイルからロードするかリモートサーバーからロードするかによって生成するMovieClipオブジェクトを変化させているということがわかりました。

どうしてこのようなことをになっているのか少し考えてみたのですが、おそらくセキュリティ要件の問題ではないかと言う結論に至りました。というのも見ての通り、ローカルファイルから読み込んだswfをdescribeTypeすると、クラス名としてファイル名とシンボル名がついてしまっていて、これを悪用することができるのではないかと危惧したのではと。

■対応:しかしさらに泥沼
クラス情報が欠落してMovieClipになってしまっているのが問題だとすれば、読み込まれる側のresource.swfのすべてのシンボルを「アクションスクリプトに書き出し」して、きちんとクラスとして定義すれば、問題が解決するのではないかと考えました。

ということで、早速試して見たところ・・・
TypeError: Error #1034: 強制型変換に失敗しました。flash.display::MovieClip@677b34c1
を rightArm に変換できません。

at flash.display::MovieClip/gotoAndPlay()
at com.akisute::Main/onLoadCompleted()
at flash.events::EventDispatcher/dispatchEventFunction()
at flash.events::EventDispatcher/dispatchEvent()
at flash.net::URLLoader/onComplete()
・・・bullshit。ジョブズがどうしてあんなにFlashを毛嫌いするのか分かった気がします(絶対違)。

まぁスタックトレースが出るようになってくれたので、なんとかなりそうです。さっきまではスタックトレースすら出さずに勝手にバグを出してくれやがってたので大変困っていました。まったく、これ読んで出直せ、Adobe。
Errors should never pass silently.


■考察:どうしてこうなるのか
さらに考えてみます。一見さっきのスタックトレースはロードに失敗してバグが発生している用に思われますが、実際にエラーを吐いているのはonLoadCompletedの中のgotoAndPlayです。そう、ロードが完了した瞬間にgotoAndPlay("right")を実行しているのですが、そこでエラーが発生しているのです。

ここでちょっと仮説を立ててみます。
  • ロードが完了された瞬間のresourceのcurrentFrameは1、つまりleftである。このとき、right側のパーツは一切読み込まれていない(nullである)。
  • gotoAndPlay("right")を実行すると、おそらくロードが完了しているMovieClip自身がタイムラインに従って101フレーム目以降の内部構造を補完しようとする。すなわち、right側のパーツ(MovieClipとかShapeとか)をどっからか取ってきてセットし、left側のパーツをnullにセットしようとする。ここまでの動きはデバッガを使って確認できました。
  • このとき、先ほどのflash.net.Loaderは完全な情報を持っていたのできちんとMovieClipを作ることができた(left側のパーツをセットできた)が、MovieClip自身にはその情報がないので、right側のパーツをセットしようとしてタダのMovieClipをrightArmクラスの変数にセットしようとし、キャストに失敗して落ちている。
・・・長くてすみません><
要するに、ローカルファイルからロードし生成されたMovieClipはデータを完全に保有しているのでMovieClip自身でnullフレームを埋めることができるが、リモートサーバーからロードしてきたswfから生成されたMovieClipは生成時にLoaderクラスによってデータを欠落させられているので、自力でnullフレームを埋めることができないのではないかと考えたのです。


■解決策:外部ロードするswfは、絶対に空のフレームを作るな
そこで以下のような対応をしてみました。
  • leftラベルの箇所にもright側のパーツを配置する。ただし見えては困るので、透明にして配置する。
  • 同様に、rightラベルの箇所にもleft側のパーツを配置する。
  • こうすることで、最初からすべてのフレームのデータがLoaderクラスによって読み込まれ、 nullフレームがないのでタイムラインがどのように動いても一度ロードされたデータが欠落することがない。従って問題なく動作する。
はたしてこの方法が功を奏し、見事に動作するようになりました!


■というのを
@moriyoshiさんから教えて貰いました。ありがとうございます!
・・・というか、なんでFlash使いでもないのにこんな詳しいんですか><

Mercurial と git の branch にまつわるちょっとした tips 3選

3選とか言ってますが大した内容ではございません>< すみませんすみません><


■1:gitのbranchは跡形もなく消せる
ほとんど常識ですが、以下のコマンドでgitのbranchは消せます。
git branch -D
このコマンドを実行すれば、たとえHEADに対してマージされていなくてもそのままブランチを消すことができます。ということで、ちょっとしたテストコードなどはブランチを切ってそこで実験し、後からブランチごとたたき落とす運用が可能です。

余談ですが、gitのbranchはSubversionと使い勝手や実装が似ている感じがします。Subversionのbranchもタダのディレクトリコピーなので、好き勝手に作って消してが可能ですから。


■2:Mercurialのbranchは基本消せない、「未使用」か「クローズ」にはできる
問題はここから。Mercurialのbranchは、基本的に完全に跡形もなく削除することができません。また、マージしていないブランチがあるとpushの際に怒られます。そのため、ちょっとしたテストコードをブランチ切ってそこで作成し、いざいらなくなったので削除しようと思うとえらい大変なことになります。

一応、ブランチを「クローズ」するコマンドを使うことで常用範囲内からは消す事が可能ですが、完全に情報が消えたわけではないのでリポジトリのサイズは小さくなりません。ゴミが溜まります。もしMercurialでテストコードを管理しようと思うならば、branchを切るのではなくリポジトリごとcloneしてそちらでテストする運用にすればよいです。


■3:Mercurialのbranchに数字で名前を付けてはならない
gitと違ってMercurialのリビジョンには、簡便のため、リビジョンのハッシュIDの他にリビジョン番号が1番から採番されるようになっています。で、各種操作の際に、このリビジョン番号を代わりに指定して実行することが可能です。

ここまではOK。

問題はbranchに数字の名前が付けられることで、もしbranchに数字の名前を付けてしまった場合、Mercurialはその数字をリビジョン番号として解釈してしまい、ブランチ名指定の処理ができなくなってしまいます。
hg branch 255
hg commit -m "created branch 255 for ticket #255"
hg update 255
# ブランチ255ではなくリビジョン番号255番に対してアップデートしようとする・・・
ただそれだけなのですが、チケットシステムなどと連携しているとよくチケット名でブランチを切る事があると思いますので、そのような際にはticket255などという風に指定するほうがよいかと思います。