中級者向け iOS デバッグ Tips
導入
iOS 開発者のみなさん、こんにちは。 このブログでは主にチュートリアルだったりフレームワークの紹介みたいなことを書いてきました。 そこで、たまには中級者向けのエントリを書いて「Xcode バリバリ使って、ビシバシ Objective-C 書いてますよ」アピールします。
iOS 開発をはじめて一通り Framework は理解したけど Xcode 使いこなせてる感が足りない方、夢にまで EXC_BAD_ACCESS が出てくる方に参考になる記事となればと思います。
といっても Xcode はマッシブな IDE なので、4つのデバッグツールに分けて「あれ、それ知らなかった!便利じゃん!」な方法を紹介します。
ブレークポイント
デバッグには切っても切れない関係ですね。アプリを実行中に指定した行で処理を中断し、そこからステップ実行で細かいデバッグを可能にしてくれます。
- ショートカットキー
僕は今までブレークポイントを打つ時は必ず、該当する行番号をクリックして打ってきました。しかしカーソルが該当行にある状態で以下のコマンドを実行することで同様のことができます。
⌘ + \
- return 値の閲覧
最近からかもしれませんが、return 文のところでステップを合わせデバッグエリア内の Step Out ボタン(↑のボタン)を押した後に Variables View をみると Return Value が表示されています。
- 条件判定
ブレークポイントがブレークする条件を設定することができます。 設置したブレークポイントを右クリックして「Edit Breakpoint…」を選択します。すると Condition というテキストエリアがあるので、条件式を入力します。ただし少々厄介なので注意が必要です。
他にもブレークポイントには色々編集できるところがあり、たとえばブレーク後のアクションなども設定できるので
po self.view
と設定すると、ブレークしたタイミングでデバッガコンソールにビューをダンプしてくれます。他にも Shell Script や AppleScript も設定できるので、シミュレータのスクリーンショットの撮影などカスタマイズは色々出来ます。
- 例外ブレークポイント
iOS 開発者なら幾度となくみるであろう、上記画面。そしてデバッガコンソールに吐き出される、「どこかで配列の NSRangeException が起きてますよ」の案内…。そんな時に例外ブレークポイントを設定しておくと、例外を投げた時点でブレークするの発生箇所を特定できてとても便利です。
- シンボリックブレークポイント
あるモジュール内の関数にブレークポイントを打ちたい時にはこれが便利です。
- ブレークポイントの共有
設定しているブレークポイントを右クリックし、「Share Breakpoint…」をクリックすることでそのブレークポイントはコミットに含まれ、他のマシンで開発を行っているユーザのプロジェクトにも同様のブレークポイントが登場します。 ブレークポイントの共有先を User にすれば、そのコミットは自分のアカウントでのみ共有されます。どこにも共有しない場合(デフォルト状態)には共有先をプロジェクトファイルにします。変更は右クリックメニューの「Move Breakpoint…」から。
コンソール
- コンソールから手動出力
デバッグウィンドウの左側には Variables View として変数の値が表示されるものがありますが、若干バギーな時もあります。そのような時はブレークポイントでブレークしている時に、右側のデバッガコンソールで"po" と打つと引数に与えたオブジェクトを返してくれます。
po self.view
そして以下のように入力すると、ビューの階層を全て表示してくれます。
po [[self view] recursiveDescription];
(出力)
(lldb) po self.view; (UIView *) $3 = 0x07448200 <UIView: 0x7448200; frame = (0 0; 320 499); autoresize = W+H; layer = <CALayer: 0x74482b0>> (lldb) po [self.view recursiveDescription]; (id) $4 = 0x0767a5d0 <UIView: 0x7448200; frame = (0 0; 320 499); autoresize = W+H; layer = <CALayer: 0x74482b0>> | <UILabel: 0x7448370; frame = (20 123; 280 43); text = 'Second View'; clipsToBounds = YES; opaque = NO; autoresize = W+BM; userInteractionEnabled = NO; layer = <CALayer: 0x7448430>> | <UITextView: 0x90ac400; frame = (20 206; 280 88); text = 'Loaded by the second view...'; clipsToBounds = YES; opaque = NO; autoresize = W+BM; userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x744b010>; layer = <CALayer: 0x744a530>; contentOffset: {0, 0}> | | <UITextSelectionView: 0x744a860; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <CALayer: 0x744a8e0>> | | <UIImageView: 0x744b7a0; frame = (273 0; 7 88); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x744b8d0>> | | <UIWebDocumentView: 0x90b2a00; frame = (0 0; 280 88); text = 'Loaded by the second view...'; opaque = NO; userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x74515c0>; layer = <UIWebLayer: 0x7450020>> | | | <TileHostLayer: 0x7450690> (layer) | | <UITextSelectionView: 0x745bfc0; frame = (0 0; 0 0); userInteractionEnabled = NO; layer = <CALayer: 0x745c040>> (lldb)
ロギング
- フォーマット指定
意外と知らない人も多い(ほんとか?)みたいなんですが、以下のように NSLog 内で [NSString stringWithFormat:] 形式もサポートしています。
NSLog(@"%d view is %@", view.tag, view);
- ログはスマートに
NSLog で出力されているものはリリースバージョンでも Organizer 経由でコンソールをつなげると見えてしまうので出さないようにするのがスマートです。 コード内で以下のような感じで DEBUG ビルド時のみ出すようにしましょう。
#ifdef DEBUG #define JRLog(...) NSLog(@"%s %@", __PRETTY_FUNCTION__, [NSString stringWithFormat:__VA_ARGS__]) #else #define JRLog(...) do { } while (0)
- オブジェクトライフサイクルのロギング
ARC の時代になったとはいえ、オブジェクトのライフサイクルの把握は重要です。一見面倒ですが必ず役に立つので、スニペット化しましょう。
- (id)init { self = [super init]; if (self) { NSLog(@"%@: %@", NSStringFromSelector(_cmd), self); } return self; } - (void)dealloc { NSLog(@"%@: %@", NSStringFromSelector(_cmd), self); }
- NSLog の Next Step を目指す
NSLog は素晴らしいけれどイケてない所もいくつかあります。 ローカルにしかログを出力できなかったり、ログレベルを指定できなかったり、そもそも遅かったり。 そこで人々は sqlite3 を用いて非同期でサーバに送信するロガーを内製しだすのですが、オープンソースで有名なものを見当するのも一つの手です。 CocoaLumberjack SNLog
静的アナライザ・インスペクタ
コードをブラッシュアップし、これから起こりうるバグやエラーを少なくするツールが Xcode には二つもあります。それが静的アナライザとインスペクタです。 静的アナライザは未使用オブジェクトやリリース過多の可能性があるオブジェクトなどを指摘してくれます。実行方法はビルドメニューの Analize を実行するだけ! インスペクタは Xcode とは他のアプリである Instruments を用いて、メモリ使用量、ファイルシステム、UIインタラクションなどを計測してくれます。別アプリといってもビルドメニューから Profile を実行するだけ! Instruments のテンプレートは一つずつ説明すると切りがない位たくさんあるので、一度みてみてください。
ビルド、設定
- ワンランク上の Clean ビルド
iOS 開発をするうえで「Clean ビルドを癖にする」のは上達への一歩と言っても過言ではありません。しかし通常の Clean ビルドは現在選択しているターゲットのビルドフォルダを削除しているにすぎません。Option キーを押しながらビルドメニューをみると Clean が Clean Build Folder… となります! これを実行することで、全てのターゲットのビルドフォルダが削除されます。
- ゾンビと仲良く
上記でも説明しましたが、EXC_BAD_ACCESS はクセ者ですね。 以下の方法を利用すると、リリースされたオブジェクトをゾンビのようにメモリ上に残したままアプリは実行されます。そうすることで EXC_BAD_ACCESS を起こさなくなり、デバッグが用意になります。 Edit Scheme メニューから Diagnostics タブを開き、「Enable Zombie Objects」にチェックをするだけです。 説明は省きますが、Instruments の Zombie テンプレートでも同様のことが容易に出来ます。
- アプリケーションデータの抜き出し、クラッシュ再現
これが最後になります。なので個人的に一番感動したものを持ってきましたw
デバイス又はシミュレータであれこれデバッグ中にクラッシュしたとします。しかし別のデバイス又はシミュレータだと全く再現しない、どうやら何かしらの不整合データが入り込んだことが原因のよう、までの見当は付いたとします。 しかし、クラッシュしたデバイス又はシミュレータのデータ環境を再現することが非常に難しい…。
それ、以下の方法で完全再現できるんです!!!
- まずクラッシュしたアプリが入っているデバイス又はシミュレータを Mac に接続します。
- Xcode の Organizer を使って applicationData を Download します。
- そのファイルをプロジェクトファイルにD&Dします、プロジェクトには関係なく後で消すのでターゲットには追加しないでください。
- Edit Scheme の Options タブを開き、 Application Data で3で追加したアプリケーションデータを選択します。
- ビルドすると完全に再現されたアプリが起動します!
終わりに
最後まで読んで頂き、ありがとうございました。 知ってることだらけだったらすいません。 もし他にも iOS 玄人しか知らないデバッグ方法などあったらコメントなどで教えて下さい!
関連リンク
[WWDC 2012] Working Efficiently with Xcode
Conditional Breakpoint (条件付きブレークポイント)
http://mobile.tutsplus.com/tutorials/iphone/debugging-in-ios-essential-tips/
http://blog.manbolo.com/2012/01/23/xcode-tips-1-break-on-exceptions