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などという風に指定するほうがよいかと思います。

2010年2月7日日曜日

Mercurial の、 hg revert / hg rollback / hg backout の使い分け

以前からgitを使っていたのですが、最近は職場のバージョン管理システムがMercurial hg になっているので、もっぱらhgばかり使っています。ということで、いくつか覚えたhgネタ。

Mercurialやgitに限らず、いかなるバージョン管理システムを使用していても、人間が使う以上運用中にミスが発生することは避けられません。今回はMercurial使用中に間違ったコミットやプッシュを行ってしまった際の対処法を調べてみました。

参考文献はこちら。
4798021741入門Mercurial Linux/Windows対応
秀和システム 2009-01

by G-Tools


間違いを修正するためのコマンドは、大きく分けて以下の3つがあります。
  • hg revert
  • hg rollback
  • hg backout
またコマンドを用いて修正する意外にも、ローカル作業する際に作業用リポジトリを別に作って、問題に気づいたらリポジトリごと削除するという運用もありますが、今回は説明を省きます。


■hg revert
適用可能なのは「ローカルのリポジトリ上で、コミットをする前に問題に気づいたとき」。
作業中にちょっと間違った際など、ファイル単位でコミット前の状態(parentの状態)に戻すことができ大変便利です。コミットしてしまった場合は、次のhg rollbackを使用します。


■hg rollback
適用可能なのは「ローカルのリポジトリ上で、コミットをした直後に問題に気づいたとき」。
直前の1回分だけ、commitを取り消すことができます。マージ作業だろうがなんだろうが跡形もなく消してくれるため、後から見ても痕跡がなく綺麗です。操作も簡単で安全なため、これが使用可能なときはこちらを使用すると便利です。

一応、直前の1回だけならリモートの共有リポジトリへのpushも取り消すことができるのですが、これは後述の理由からお勧めしません。一度リモートにpushしてしまったら、次のhg backoutを使って取り消すことになります。


■hg backout
適用可能なのは「ローカルのリポジトリ上で、複数回コミットした後にに問題に気づいたとき」か、または「リモートの共有リポジトリにプッシュしてしまった後に問題に気づいたとき」。
どうしようもなくなってしまったときの最後の手段がこれ。指定したリビジョンのcommitの内容を完全に打ち消す新しいリビジョンを作ってくれます。あとは、手動でこの自動で作られたリビジョンを元のリビジョンにmergeしてcommitすることで、いかなる問題であろうとも無理矢理打ち消す事が可能になります。

もっぱら間違ってプッシュしてしまった際に使用します。
一応hg rollbackでも直前のpushを取り消すことが可能ですが、一度pushされてしまった内容はどこかの誰かのリポジトリにpullされてしまっている可能性があり、そうなると共有リポジトリだけをrollbackしても効果がありません。(このように、間違いも分散して広まってしまうのが分散バージョン管理の恐ろしいところ)

このようなときは、hg backoutで修正リビジョンを作りマージしたあと、再度共有リポジトリにpushすれば、他の誰かのリポジトリに問題が取り込まれてしまっていても次のpullで無事修正されます。