iOS 開発者が知るべきデバッグ手法
最近 iOS の開発はほとんど行なっていないのですが、以前動画で観た WWDC 2018 のセッション Advanced Debugging with Xcode and LLDB の内容をまとめました。
このセッションでは Xcode と LLDB を使用して開発時に役出つデバッグ手法が紹介されています。
オブジェクトの情報を出力する
po <expression>
- 出力はカスタマイズ可能 (後述)
p <expression>
- LLDB の組み込みフォーマッタを使用してオブジェクトを表す
frame variable <name>
- 上の2つとは異なり式の評価をせず、変数の値をメモリから直接読み取る
- LLDB の組み込みフォーマッタを使用してオブジェクトを表す
デバッグ中に変数の値を変更する
例えば flag
という Bool 型の変数を false
に設定したいとします。
LLDB の expression
コマンドを使用すると可能で、 expression flag = false
と実行します。
Breakpoint を使用することで、以下のように Breakpoint を設定した箇所のコードが実行されるたびに変数を変更する設定もできます。
- Breakpoint を設定する
- 右クリックの Edit Breakpoint から Add Action で
expression flag = false
と入力する Automatically continue after evaluating actions
にチェックを入れておくと、変数を書き換えて自動的に再開される
任意のクラスのメソッドが実行されたタイミングを検知する
例えば UILabel の setText が実行されたタイミングを知りたい場合、以下の手順で可能です。
- Xcode の Navigator で Breakpoint navigator を開く
- 左下にある
+
ボタンをタップしてSymbolic Breakpoint
を選択する Symbol
にメソッドのシンボルを入力する(今回の場合は-[UILabel setText:]
)- UIKit の場合は Objective-C で記述する必要があるので、今回の場合は
-[UILabel setText:]
- Swift のコードを呼び出す場合は単にメソッド名だけでよい
- UIKit の場合は Objective-C で記述する必要があるので、今回の場合は
この手順を実行して Breakpoint を止めた場合、アセンブリコードとなりますが、LLDB の po コマンドを使用して、以下の情報を取得することが可能です。
po $arg1
: レシーバーオブジェクトの情報が出力される (この場合UILabel
のインスタンスの情報)po (SEL)$arg2
でメソッド名が出力される (この場合setText:
)po $arg3
で引数の情報が出力される (この場合setText
の引数の情報)
ただし、この方法を実行した場合、アプリ内の全ての UILabel の setText が実行されたタイミングで Breakpoint が止まってしまうので、あまり実現的ではありません。
指定したタイミング以降で一回だけ Breakpoint が止まるようにすることができます。
- 任意の場所に Breakpoint を設定する
- Edit Breakpoint から Add Action で
breakpoint set --one-shot true --name "-[UILabel setText:]"
と入力する Automatically continue after evaluating actions
にチェックを入れる
特定の行のコードを実行せずに飛ばす
Breakpoint で止まった際に表示される Instruction Pointer を動かすことによって可能です。 ただし、注意点としてメモリ管理上の問題やオブジェクトが初期化されていないなどの問題が発生する可能性がありえます。
LLDB のコマンドでも可能で、thread jump --by 1
で1行処理を飛ばすことができます。
※ 注意点としてこれらを実際に試してみたのですが、セッションで紹介されているような期待通りの動作はしませんでした...
おまけに記載している thread return
は正常に動作することを確認いたしました。
変数の値が変更された時を検知したい
Breakpoint が止まった際にオブジェクトのヒエラルキーから特定の変数を選択して右クリックで Watch 変数名
で Watchpoint が追加されます。
この Watchpoint によって変数の値が変更された際に止まるようになります。
po コマンドで出力される内容を変更する
- デバッグ情報を出力させたいクラスや構造体に
CustomDebugStringConvertible
を継承させる debugDescription
メソッドをオーバーライドする
View のヒエラルキー情報を出力する
expression -l objc -O -- [`self.view` recursiveDescription]
を実行する(self.view をバッククォートで囲む必要がある)
メモリアドレスから View の情報を出力する
expression -l objc -O -- 0x1234567890ab
を実行する (メモリアドレスには確認したい View のものを指定する)- エイリアスを設定することによって、楽に記述することができるようになる
command alias poc expression -l objc -O --
で Objective-C で実行するためのエイリアスを作成できる- 上記のエしようして使用して
poc 0x1234567890ab
で View の情報を出力できる
Swift の unsafeBitCast を使用しても出力することが可能です。
po unsafeBitCast(0x1234567890ab, to: UILabel.self)
unsafeBitCast の場合、指定された型のインスタンスを返すので、その型に応じた処理を行うことも可能です。
po unsafeBitCast(0x1234567890ab, to: UILabel.self).text = "foo"
設定した情報を画面に反映させるには、以下のコマンドを実行させる必要があります。
expression CATransaction.flush()
Python で書かれたスクリプトを使用する
Python を使えば LLDB API にフルアクセスできるようです。
Python で書いたスクリプトを LLDB のコマンドとして実行する手順は以下になります。
- ホームディレクトリに
~/.lldbinit
ファイルを作成する command script import ~/nugde.py
のように行を追加する- nudge.py は 動画のページ からダウンロード可能
- 以下のようにエイリアスを追加することも可能
command alias poc expression -l objc -O -- command alias 🚽 expression -l objc -- (void)[CATransaction flush]
おまけ
今回観たセッション動画以外にもいくつか便利な機能があるみたいなので追記しておきます。
任意の箇所以降のコードを実行しない
LLDB コマンドの thread return <RETURN EXPRESSION>
でこれ以降のコード実行がスキップされます。
facebook/chisel
- facebook/chisel は iOS 開発に役立つ LLDB コマンドを Python スクリプトで提供
感想
恥ずかしながらこれを観るまでは po
コマンドくらいしか知りませんでした...
色々と便利な機能があるので知っていればかなり開発に役立つのではないかと思います。