Xcode
iOS
LLDB
Swift

Xcode + LLDBで再ビルドなしにデバッグする

概要

TwitterのタイムラインでLLDBの話を見かけた時にこんなことを書きました。

こちらのセッションになるのですが、特に「話がうまいなー」と感じたのは前半部分です。

ポイントは「Xcode + LLDBはちゃんと使えば再ビルドなしにアプリを書き換えながらデバッグできるよ」っていう所で、具体的なテクニックと共に伝えております。
この記事ではその内容をまとめていきます。

Xcode+LLDBをつかったテクニック

ここで紹介するものは基本的に以下を繰り返しながら、再ビルドなしでデバッグします。

  1. データの流れを理解・制御して原因箇所を突き止める
  2. ロジックを入れ替えて正しい動作を確認する
  3. 実コードへその内容をコピーする

1と2をやるためにはランタイムでの書き換えが必要です。その時に便利なのがBreakpointのEdit Windowで、expressionコマンドとauto-continuingを利用します。Edit WindowはBreakpointをダブルクリックすることで表示させられます。

スクリーンショット 2018-07-29 19.20.30.png

個別のコマンドを覚えるより、このようにデバッグの流れやリズムを理解したほうが身につきやすいでしょう。
それではどのようなケースで活用できるか書いていきます。

変数が特定の値を取る時に発生するバグを再現したい

通常の操作ではめったに取らない値をデバックする方法です。
例えばViewController内でflagという変数が使われており、その値で処理出し分けをしているケースを考えます。

変数値が使われているところでBreakpointを仕掛ける

まずは値が使われているところでBreakpointを仕掛け実行を止めましょう。

スクリーンショット 2018-07-31 22.59.30.png

この状態ではflagの値がfalseになっています。expressionコマンド(もしくはDebug AreaVariables View)で確認してみます。

スクリーンショット 2018-07-29 20.39.49.png

LLDBで値の代入を行う

止まっている状態で変数の値を書き換えます。Debug Areaのコンソールを開き、値の変更を行います。

スクリーンショット 2018-07-29 20.39.56.png

そうすると、値が書き換わってif文のtrueのほうが実行されます。

スクリーンショット 2018-07-31 22.59.50.png

BreakpointのEdit Windowへ代入コマンドを入れる

デバッグしたい処理が走ることを確認したら、動作確認中は常にその状態になるようにしましょう。
最初に仕掛けたBreakpointをダブルクリックしてEdit Windowを開きます。"Action"Debugger Commandにして、Consoleの変更処理をコピペします。
また、Breakpointへ来ても実行が止まらないようにしましょう。そのためには、'Options''Automatically continue after evaluating actions'にチェックを付ければいいです。

スクリーンショット 2018-07-29 20.44.00.png

そうすると、コマンドが実行された上でそのまま後続の処理が走るようになるでしょう。
その結果、変数が特定の値を取る時に発生するバグを常に再現させることができます。

特定の行に処理を挟んでその実行結果を確認したい

ある部分に実装漏れがあって、コードを追加した結果を見てみたい場合の方法です。
例えばViewController内で使っているViewのdelegate設定を忘れていたとします。

実装漏れがあるところでBreakpointを仕掛ける

前と同じく、コードを追加したいポイントでBreakpointを仕掛けます。(ここではviewDidLoad)

スクリーンショット 2018-07-29 20.53.31.png

BreakpointのEdit Windowで追加したいコードを書く

BreakpointのEdit Windowを開いて、Debugger CommandActionに追加したいコードを書きます。'Options''Automatically continue after evaluating actions'にチェックを付けて実際に動かし、正しく動作するまで何度か試します。

スクリーンショット 2018-07-29 20.54.03.png

Breakpointを仕掛けた箇所へ追加したコードをコピーする

うまくいく方法がわかったら、Breakpointを仕掛けた行へ実際にコードを追加します。

スクリーンショット 2018-07-29 21.01.33.png

これで実装漏れによるバグの修正ができました。

問題のある行が見つかったので書き換えた実行結果を確認したい

メソッドへ代入している値が間違っていないか確認する時に使います。例えばUIImageViewをスクロールに合わせて拡大させたいというケースを考えます。スケール値の計算式が間違っていて逆に縮小されてしまうため修正しようと思います。

問題のある行へBreakpointを仕掛けて、その位置を下へずらす

以下の部分でマイナスにするべき計算式をプラスで書いてしまっているのが原因のようにみえます。そこを直して確認してみます。

スクリーンショット 2018-07-29 23.50.58.png

Breakpointは右側の3本線をつまむと上下に動かすことができます。"thread jump"に相当する操作のようです。

スクリーンショット 2018-07-29 23.58.24.png

修正コードをコンソールで実行する

飛ばした2行分の修正コードをコンソールで実行してみます。

スクリーンショット 2018-07-29 23.53.42.png

するとその結果が一度だけ反映されました。

BreakpointのEdit Windowへ位置変更処理と修正コードを入れる

ここではスクロール操作で繰り返し計算が走って欲しいので、Edit Windowに上記コマンドを書きたいと思います。まず、Debugger Commandに実行位置を変更するコマンドを書きます。

thread jump --by 2

そしもう一つDebugger Commandを追加し、そこに修正コードを書きます。

expression let scale = (imageView.bounds.height - scrollView.contentOffset.y) / imageView.bounds.height

スクリーンショット 2018-07-30 0.06.01.png

実際に動作を確認して正しいコードが分かったら、飛ばした場所へコピーします。これで間違ったコードの修正ができました。

スクリーンショット 2018-07-30 0.06.32.png

変数の値がどんな操作をしている時に書き換わっているのか知りたい

値が様々なところで書き換わっていて、操作をしながらどんなタイミングで書き換わるのか知りたいときなどに使えます。最初の3つのステップの内、「1. データの流れを理解・制御して原因箇所を突き止める」で利用しましょう。

まず、変数の初期化(もしくは変更や利用)がされているところでBreakpointを仕掛けます。

スクリーンショット 2018-07-31 21.27.50.png

Debug AreaVariables Viewを開き、対象の変数上でコンテキストメニューを開きましょう。

スクリーンショット 2018-07-31 21.34.56.png

「watch "変数名"」をクリックすると、Watchpointがしかけられます。これで、値が変更されると都度Watchpointがフックされるようになります。Breakpointと同じく、実行を止めたりコマンドを実行したりできます。

スクリーンショット 2018-07-31 21.30.42.png

UIのレイアウトを微調整したい

Viewの位置や色を繰り返し微調整したい時にも同じような流れで変更ができます。

ViewのFrameを変更する

コンソールを開いて変更したいViewのプロパティを変更しましょう。

expression self.hogeView.center = CGPoint(x: 100, y: 100)

privateなプロパティに対して行いたい場合は、アドレスからViewを取得してい呼びましょう。
ここでは飛ばしていますが、objcの[self.view recursiveDescription];からViewのアドレスを取得した場合などこの方法が使えます。

// objcのコード上で実行する場合は下記の画面停止中の場合では明示的にswiftとして実行させる必要がある
expression -l swift -O -- unsafeBitCast(0x7fb3afc3f480, to: HogeView.self).center = CGPoint(x: 100, y: 100) 

UIKitのクラスが見つからないというエラーが出る場合は先にimportしましょう。

expression -l swift -O -- import UIKit

また、画面表示後に停止ボタンを押してそこからUIパーツをピンポイントで呼び出す場合にも、アドレスから参照するのがいいかと思います。

スクリーンショット 2018-08-02 13.22.25のコピー.png

アドレスはView Hierarchyから簡単に調べることができます。

スクリーンショット 2018-08-02 13.22.25.png

オブジェクトを選択して右ペインからアドレスをゲットしましょう。

スクリーンショット 2018-08-02 13.23.58.png

画面をリフレッシュする

そのままでは画面に変化がないので、以下のコマンドをコンソール上で実行してリフレッシュします。

expression CATransaction.flush()

すると実行結果を確認しながら変更ができます。

test.gif

ちょうどいい位置に変更したら、差分を実コードのAutolayoutやframeに反映させましょう。
また、Core Animationなのでテキスト色なども微調整可能です。

この方法に関しては、よりサクサクUIを変更するためのカスタムコマンドがサンプルとして用意されています。
セッションのページからダウンロードできるので利用してみてください。

このスクリプトを.lldbinitに追加するか下記コマンドをコンソール上で実行して読み込むと利用できます。

command script import ~/.nudge.py

最後に

再ビルド・再実行なしでデバッグしていくためのやり方をまとめました。
Swift+iOS SDKでのアプリ開発でのデバッグは何度もビルドしないといけなくて苦手だなぁっていう方は是非LLDBを活用してみてください。

また、元ネタのセッションでは

  • 繰り返し実行されてしまうもので一度だけ実行したい時に使う —one-shot true
  • ソースコードを確認できないUIKitに対してassembly frameから引数を確認する
  • Symbol breakpointでメソッドが呼ばれた場所を確認する

などなど他にも色々紹介されていました。

また、こちらの本がXcode上でのLLDB操作をかなり詳しく解説しています。(上記セッションのようにわかりやすいストーリーになっているわけではないですが…)

他にもQiitaで同じセッションの内容をまとめている記事があったので合わせて載せておきます。

参考資料