2012年9月26日水曜日

Unity 3.5.5 以下で Game Center / iAd / UIImagePicker などを使用したアプリが iOS 6 でクラッシュする問題

現在 Unity 3.5.5 以下でビルドしたiOSアプリが、以下の条件をすべて満たしているとクラッシュしてしまう問題が発生しているようです。
  • UnityのiOSアプリビルド設定で、画面方向を横向き(Landscape Left/Landscape Right)のいずれかのみに設定している。
  • Pluginなどを経由してGame Centerを使用している。またはiAdやUIImagePickerなどのiOSが提供する特定のView Controllerを使用している。
  • iOS 6を搭載したiPhone/iPod Touch上で動作させる(iPadは大丈夫)。
詳細はこちら。
http://developer.coronalabs.com/forum/2012/09/17/ios-6-orientation-crash





すでにUnity側で問題は把握しているようで、現在修正版の3.5.6を用意してくださっているようなので、続報を待ちましょう。・・・といっても、すでにUnity 3.5.5以下でビルドされたiOSアプリをリリースしていて、しかもAsset Bundleを使用していたりすると、Asset Bundle間の後方互換性問題のためかなり厄介なことになると思われます。最悪過去のバージョンのサポートをすべて切る必要が出てくるかもしれません。


さて、ここからは技術話の余談です。

このクラッシュ問題なのですが、原因はiOS 6で変更になった画面回転(Auto Rotation)APIにあると思われます。iOS 6からはなんとこれまで画面回転に使用されていた
UIViewController shouldAutorotateToInterfaceOrientation:
が完全に廃止になっており、基本的には全く呼び出されないようになってしまっています。その代わりにより体型だった画面回転の仕組みが導入されています。iOS 6からの画面回転は、「アプリが対応する画面方向」と「各View Controllerが対応する画面方向」の2つによって画面の回転する方向が決定されるという仕組みになっています。

ここで、アプリが対応する画面方向は以下のように決定されます。
以下優先順位順に、
1. UIApplicationDelegate application:supportedInterfaceOrientationsForWindow: が返す向き。iOS 6以降のみ。
2. UIApplication supportedInterfaceOrientationsForWindow: が返す向き。iOS 6以降のみ。
3. Info.plist で指定されている UIInterfaceOrientation の向き。
各View Controllerが対応する画面方向は以下のように決定されます。
各ViewControllerが supportedInterfaceOrientations を実装しているなら、それが返す向き。
していないならば、
iPhoneだと UIInterfaceOrientationMaskAllButUpsideDown
iPadだと   UIInterfaceOrientationMaskAll
この2つの積集合をとって、両方が一致した向きに画面回転が行われる仕組みになっています。

では、ここで両者が全く一致しない場合はどうなるでしょう?答えは簡単でクラッシュします。ワオ。素敵。ふざけんなバカApple爆発しろ。

それを踏まえると、今回の問題でクラッシュしてしまう仕組みはこう考えられます。
  1. Unity 3.5.5以下が書き出すiOSアプリは当然ながらiOS 6に完全対応していない。
  2. Game Centerなど、Appleが提供しているUIコンポーネントは全てiOS 6の画面回転に対応しているが、それらのうち幾つかのものはiPhoneだと縦向き画面にしか対応していないものがある(iPadは基本縦横どちらでも表示できるように対応する必要があるとされているため、無事のようです)。
  3. Unityが書きだしたアプリは横向き画面にしか対応していないのに、上記のView Controllerは縦向きにしか対応していないと言い出すので、クラッシュする。
やれやれですね><
ちなみに対応策としては、アプリ自体は縦方向にも横方向にも対応しているというふうにapplication:supportedInterfaceOrientationsForWindow:メソッドを使って値を返すようにした上で、コンテンツを表示するUIViewをUIViewControllerに管理させるようにして、UIViewControllerのsupportedInterfaceOrientationsで横向き画面の値だけを返すようにするといいかんじになると思います。しかしながらiOS 5/6両方で上手く回る画面は結構大変そうです。

2012年8月19日日曜日

Unity のプラグインで iOS の bundle を使えるようにする方法

OS XやiOSには Bundle という仕組みがありまして、 NSBundle というBundleを扱うためのクラスが用意されています。皆様も一度は
[[NSBundle mainBundle] pathForResource:@"MyMusic" forType:@"mp3"];
みたいなコードを書いたことがあると思います。このBundleの仕組みを使うと、
  • 複数の画像や文言、JSONなどのデータファイル、音楽などを一つにまとめて扱うことができる。個別のファイルとしてプロジェクトに加えなくてよいので利便性が良い。
  • Bundleには最初から多言語化リソースを扱うための仕組みが用意されているため、多言語化が非常に簡単にできる。
  • OS Xだけになりますが、Bundleにはコンパイル済みのコードを含められるので、プラグインの仕組みが簡単にできる。
以上のようなメリットがあります。例えば実例を上げると、Facebook SDKなどがBundleの中に画像や文言などのリソースを格納して配布するようになっています。実際には確認できていないのですが、おそらくmobage, GREEのSDKもそのようになっているのではないでしょうか?ということでSDKなどを配布するときには非常に便利です。

で。このBundleによる配布はなかなか便利なので、UnityのiOSプラグインとして配布するときにも是非使いたいのですが、そのまま/Assets/Plugins/iOS以下にBundleを配置しても正しくBundleが認識されませんし、iOSのアプリにインストールされません。

そこでBundleを配布するときは、/Assets/Plugins/iOS/以下ではなくて、/Assets/StreamingAssets/以下にBundleを配置するようにしましょう。実は/Assets/StreamingAssets/以下のパスには隠し要素があって、このパス以下のファイル・ディレクトリは全てアプリケーション自身の/Data/Raw/ディレクトリに配置されるようになっています。
参考: http://sehm.blog48.fc2.com/blog-entry-159.html

あとはプラグインとして用意したiOSコードの中で以下のようにしてBundleを取得すればOKです。
NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"MyBundle" ofType:@"bundle" inDirectory:@"Data/Raw"]];
これでUnityプラグインを多言語化したり画像リソースを使ったりするのが楽になると思います。

将来的にUnityのバージョンが上がって、/Assets/Plugins/iOS/以下においたBundleも扱えるようにしてくれると楽なんですが・・・Unity 3.5の地点ではどうもダメっぽいです。

2012年7月25日水曜日

LLDB を使って CUI で動作させているテストケースをデバッグする

GHUnit のテストケースを Jenkins で自動的に走らせるために CUI のテストを作っているのですが、 GUI を使ったテストでは問題なく通るのに CUI を使ったテストのときだけテストが落ちるという問題が発生してしまいました。 Xcode 経由の実行であれば Instruments だろうが内臓のデバッガだろうが使い放題なのですが、 CUI となるとそうもいきません。そこで LLDB を直接使って CUI で動作させているテストケースを直接テストしてみることにしました。

参考にしたのはこちら。
http://lldb.llvm.org/tutorial.html

さっそくやってみましょう。lldbコマンドで LLDB の対話環境を起動した後、
process attach --name MyApp --waitfor
でMyApp.appという名前のプロセスが立ち上がるのを監視します。



この状態でMyApp.appというアプリを起動すると、上の画像のようにばっちり LLDB がプロセスを検知して捕まえてくれます。今、MyApp.appはポーズ中になっているので、適当な箇所に
breakpoint -f MyTestModel.m -l 123
とかやって適当にブレークポイントを設置した後、
thread continue
で続きを実行開始します。



ご覧のとおりバッチリブレークポイントで捕まえることに成功です。こちらの図はEXC_BAD_ACCESSが発生した時になんか自動的に止めてくれたときのものです。Xcode経由で起動するときに比べれば不便ですが、何もないよりはだいぶはかどりますよ。