2012年12月1日土曜日

iOS開発のデバッグ作業(1)

日本語ドキュメント - Apple Developer のInstrumentsガイド
iOS開発ガイド (版が古いけど、最新のものがあるのか不明…)

まずこの辺を読もう。

----

前提知識

ビルドの種類

ビルドには「デバッグビルド」と「リリースビルド」がある。リリースビルドでは最適化が施されるため、両者で生成されるバイナリは微妙に異なります。ごく稀ですが、最適化の結果バグが発生するケースがありえるので、必ずリリースビルドでもデバッグを行いましょう。

ビルドを切り替えるには、[Edit Scheme...]から、[Build Configration]をReleaseにする。デフォルトでは、通常のRunはデバッグビルドで、Instrumentsを常駐させるProfileではリリースビルドで実行するので、ちゃんとこの両者でテストしていれば問題ないです。

なお、NSLogはデバッグビルドでもリリースビルドでも表示されますので、不要なデバッグログ出力は、リリース版を作成する際には出力しないようにすること。専用のログマクロを用意するのが常套手段。

デバッグ用設定を整える

iOSでよくあるバグは、解放済オブジェクトへアクセスして、EXC_BAD_ACCESSが発生すること。XCodeの初期設定ではこのバグの発生箇所をmain()関数としか示さない。

[Edit Scheme...]から、[Diagnostics]を選択し、[Enable Zombie Objects]のチェックを入れると、ゾンビオブジェクト(解放済みオブジェクトへのポインタ)を監視する機能が働いて、こうしたミスの発生箇所を正しく示してくれるようになる。

ただし、KERN_INVALID_ADDRESSを検知できなくなる(本来クラッシュする場所で、クラッシュしなくなる)副作用がありそうなんだけど、この辺どうしてそうなるのか分からないので、明確なことは言えない。念のため、チェックを外した状態でもテストをするのが良さそう。

[Diagnostics]には他にもmallocのログ出力など、いくつかの機能がある。自分は使いこなせてないけど。

例外にブレークポイントを設定する

ナビゲータエリアのBreakPointsペインの左下の+から、[Add Exception Breakpoint...]を選択すると、例外が発生した際に自動的にそこで停止する。

[Exceptions]の対象をAllにすると、Cocoa内部のCやC++で書かれている場所で発生している例外まで全部拾って、いちいち止まって邪魔なときがある(AVAudioPlayerでこの現象を確認)。

[Exceptions]の対象を[Objective-C]にすることで、Objective-C内の例外に限定することができるものの、Cocoaフレームワークの実装はCで書かれていたりするので、例外を見逃すこともありそう。(この辺どーなんだろ)

お薦めなのは(受け売りだけど)、[Actions]に[Log Message]と[Sound]を設定して(Messageは例文通りで問題ないと思う)、[Options]の[Automatically continue after evaluating]にチェックを入れる。すると例外が発生すると指定したメッセージとサウンドを鳴らして例外の発生を通知してくれるが、クラッシュする例外でなければそのまま停止せず次の命令が実行される。

デバッグログを出力する

まず本当にNSLogデバッグが必要なのかどうか考える。NSLogを卒業してデバッガを使いましょう。

どうしても必要ならば、マクロを活用しましょう。C言語のマクロとして、__func__(関数名を表示)、__FILE__(実行中のコードのファイル名を表示)、__LINE__(実行中のコードの行数を表示)が定義されています。これらを使うと、現在どのファイルのどの関数の何行目を実行しているのか表示できます。

自動的にこれらを出力する専用のデバッグログマクロを用意すると捗ります。

が、使いすぎると大量にログが流れてうざったいだけ(特に他人が作ったコンポーネント内でいつ何が呼び出されてるとかこっちは興味ない、どうしても知りたければコードを読む)なので、自分が必要な最小限を記述して不要になったら消す、他人に開示するメリットがあると考えたときのみ残す、とかそういうルールでやるといいと思います。

Objective-Cメソッドの暗黙の変数を利用することもできます。例えばselfに対してNSStringFromClass()でメソッドを呼び出しているオブジェクトのクラス名を取得できます。_cmdに対するNSStringFromSelector()は__func__の下位互換なのでメリットはないです。

あと、[NSThread callStackSymbols]を出力すると現在のメソッドの呼び出し階層を見ることができます。想定されていない場所から呼び出されていないか、などを確認するのに使えます。

ビルド警告や静的アナライザを使用する

ビルド時にエラーほどではない問題は警告として表示されたり、[Analize]で静的アナライザを実行すると、コンパイラがメモリ解放忘れやロジックエラーなどを検出して知らせてくれます。

コンパイルが通るだけで満足せず、ビルド警告やアナライザの警告は極力潰してください。警告の内容は大抵は適切ですし、警告を放置するとどんどん積み重なって、本当に必要な警告が出力されたときにそれに気付けなくなります。

かといってコンパイラに振り回されて、修正する必要のないコードを書き直すのも本末転倒です。もし本当に自分の書いたコードの方が正しく、警告を無視しても問題がないのだと確信できるときは、コンパイラの警告を抑制します。

Clangで警告を抑制する方法はUsers Manualをどうぞ。以下はちょっと自信がないのであれですが…。

Warningを抑制するには、

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-警告の種類"
//ここに無視しても問題ないコードを書く
#pragma clang diagnostic pop

静的アナライザの警告を一時的に無視するにはには次のように書けばいいと思います。

#ifndef __clang_analyzer__
//ここに無視しても問題ないコードを書く
#endif

クラッシュログを読む

iOS端末にはクラッシュ時のログが記録されています。もしMacに繋いでXCodeでデバッグしていなかったときに発生したクラッシュでも、本体のログを読めば原因を追究できます。

[Organizer]から[Devices]タブを選択し、クラッシュログを読みたい端末の[Device Logs]を選択します。すると問題発生時のログ一覧が表示されます。クラッシュは分類がCrashとなっているので、その中で問題のアプリを探し出します。

このログを読み解くにも知識が必要そうですが、とりあえずException Typeで原因の種類と、Crashed Threadでクラッシュが発生したスレッドを見て、そのスレッドのスタックとレースで発生箇所を特定します。分かりやすいクラッシュならばこれで原因を特定できると思います。

iOS端末がクラッシュ原因のアプリを特定できなかった場合など、CrashではなくUnknownに分類されることがあります。この場合のログには、アプリ毎に確保しているPage数(アプリが確保したメモリの仮想領域、詳細はページング方式を。なお1ページは4096バイト)が羅列しています。自分のアプリ名を探して、問題の原因となっていないか確認します。

なお、同画面で[Console]を選択するとその時点でのコンソールログを見ることもできます。デバッグログを出力していれば確認することができるほか、didReceiveMemoryWarningイベントが通知されていないかなども確認できます。

クラッシュログを受け取る

他の端末からクラッシュログを受け取るのは少し面倒です。

「~したらクラッシュした」など漠然とした情報をメールでやりとりしても、それがデバッグに貢献することはほとんどないので、端末に記録されたクラッシュログを利用すべきです。

クラッシュログは端末をiTunesで同期した際に自動的にPCにコピーされます。保存される場所はOSによって異なります(場所は上のiOS開発ガイドを参照)。送る側は保存された.crashファイルを送るだけでOKです。

しかし.crashファイル内の生のスタックトレースは、呼び出したメソッド名ではなくそのアドレスが記されているだけで、そのまま見てもほとんど役に立ちません。なので受け取る側は、dSYMファイルを使ってシンボルを解決する必要があります。

詳細はiOS開発ガイドか、iOSデバイスのクラッシュログを読むには - Awaresoftをどうぞ。

同じアプリケーションでも、ビルド毎に生成されるdSYMファイルの内容は変わりますし、ログとdSYMとでビルドが一致していなければ正しくシンボル解決できません。

---

ここまでのまとめ

NSLogデバッグを卒業しよう

効果的に使えばNSLogデバッグも有効です。デバッガを使えば数秒で済む変数値の確認のために、わざわざログ出力コード書いてビルドしてそれを消して…といった作業を行うのは単に非効率的なだけです。

個人開発でないならば、NSLogデバッグを書いた後は、それが他人にとっても必要なのか検討し、必要なければ削除した方が良いでしょう。不要なデバッグログ出力が溜まると、最終的に膨大な量となり、本当に必要なメッセージを見逃すことになりかねません。

コンパイラの警告を活用しよう

コンパイル時に出力される警告や、静的アナライザの解析結果を無視しないで下さい。

可能であれば修正し、もし本当に警告が無視できると確信できるならば、警告を抑制してください。無視ブロックの範囲は極力狭くしておきます。また、無視ブロック内のコードを修正するときには一時的に警告抑制を解除して、新たな警告が出ていないか確認するのを忘れないようにしましょう。

警告を無視し続けるとこれも蓄積して膨大な量となり、本当に必要な警告が見逃されます。

端末のクラッシュログを利用しよう

「~したら落ちた」といったような曖昧な口頭の伝達は、問題解決にほとんど寄与しませんので、クラッシュログを活用しましょう。

XCodeからデバッグ実行したとき以外でも、端末に残されたログから有用な情報が得られます。

0 件のコメント:

コメントを投稿