watabee's blog

プログラミング関連のブログです

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 を設定した箇所のコードが実行されるたびに変数を変更する設定もできます。

  1. Breakpoint を設定する
  2. 右クリックの Edit Breakpoint から Add Action で expression flag = false と入力する
  3. Automatically continue after evaluating actions にチェックを入れておくと、変数を書き換えて自動的に再開される

f:id:watabee_dev:20181123192501g:plain

任意のクラスのメソッドが実行されたタイミングを検知する

例えば UILabel の setText が実行されたタイミングを知りたい場合、以下の手順で可能です。

  1. Xcode の Navigator で Breakpoint navigator を開く
  2. 左下にある + ボタンをタップして Symbolic Breakpoint を選択する
  3. Symbol にメソッドのシンボルを入力する(今回の場合は -[UILabel setText:]
    • UIKit の場合は Objective-C で記述する必要があるので、今回の場合は -[UILabel setText:]
    • Swift のコードを呼び出す場合は単にメソッド名だけでよい

f:id:watabee_dev:20181123192634g:plain

この手順を実行して Breakpoint を止めた場合、アセンブリコードとなりますが、LLDB の po コマンドを使用して、以下の情報を取得することが可能です。

  • po $arg1 : レシーバーオブジェクトの情報が出力される (この場合 UILabelインスタンスの情報)
  • po (SEL)$arg2 でメソッド名が出力される (この場合 setText:)
  • po $arg3 で引数の情報が出力される (この場合 setText の引数の情報)

ただし、この方法を実行した場合、アプリ内の全ての UILabel の setText が実行されたタイミングで Breakpoint が止まってしまうので、あまり実現的ではありません。

指定したタイミング以降で一回だけ Breakpoint が止まるようにすることができます。

  1. 任意の場所に Breakpoint を設定する
  2. Edit Breakpoint から Add Action で breakpoint set --one-shot true --name "-[UILabel setText:]" と入力する
  3. Automatically continue after evaluating actions にチェックを入れる

f:id:watabee_dev:20181123192754g:plain

特定の行のコードを実行せずに飛ばす

Breakpoint で止まった際に表示される Instruction Pointer を動かすことによって可能です。 ただし、注意点としてメモリ管理上の問題やオブジェクトが初期化されていないなどの問題が発生する可能性がありえます。

f:id:watabee_dev:20181123192910g:plain

LLDB のコマンドでも可能で、thread jump --by 1 で1行処理を飛ばすことができます。

※ 注意点としてこれらを実際に試してみたのですが、セッションで紹介されているような期待通りの動作はしませんでした...

おまけに記載している thread return は正常に動作することを確認いたしました。

変数の値が変更された時を検知したい

Breakpoint が止まった際にオブジェクトのヒエラルキーから特定の変数を選択して右クリックで Watch 変数名 で Watchpoint が追加されます。

この Watchpoint によって変数の値が変更された際に止まるようになります。

f:id:watabee_dev:20181123193005g:plain

po コマンドで出力される内容を変更する

  1. デバッグ情報を出力させたいクラスや構造体に CustomDebugStringConvertible を継承させる
  2. 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 のコマンドとして実行する手順は以下になります。

  1. ホームディレクトリに ~/.lldbinit ファイルを作成する
  2. command script import ~/nugde.py のように行を追加する
  3. 以下のようにエイリアスを追加することも可能
command alias poc expression -l objc -O --
command alias 🚽 expression -l objc -- (void)[CATransaction flush]

おまけ

今回観たセッション動画以外にもいくつか便利な機能があるみたいなので追記しておきます。

任意の箇所以降のコードを実行しない

LLDB コマンドの thread return <RETURN EXPRESSION> でこれ以降のコード実行がスキップされます。

facebook/chisel

感想

恥ずかしながらこれを観るまでは po コマンドくらいしか知りませんでした...

色々と便利な機能があるので知っていればかなり開発に役立つのではないかと思います。

参考サイト

Retrofit2.5.0 でサポートされた Invocation クラスが便利そう

Retrofit2.5.0 で Invocation クラスというものが追加されたようです。

https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-250-2018-11-18

New: Invocation class provides a reference to the invoked method and argument list as a tag on the underlying OkHttp Call.
This can be accessed from an OkHttp interceptor for things like logging, analytics, or metrics aggregation.

業務の Android アプリの開発では OkHttp + Retrofit を使用しており、以前特定の API のみにヘッダを設定したいなぁ、と思ったことがありました。

今回追加された機能で簡単に実現できそうな予感がしたので試してみました。
サンプルプロジェクトはこちらです。

実装内容

GithubAPI ではアクセストークンを必要とするものがあるので、必要とする API だけヘッダにアクセストークンを設定します。

具体的に以下のように @AccessToken アノテーションを定義し、 アクセストークンが必要となる API のメソッドに付与します。

interface GithubService {

    @GET("/users/{username}/repos")
    fun getRepositories(@Path("username") username: String): Single<List<Repository>>

    @AccessToken(githubAccessToken)
    @GET("/user/repos")
    fun getAuthenticatedRepositories(): Single<List<Repository>>
}

@MustBeDocumented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class AccessToken(val token: String)

OkHttp の Interceptor を使用して、Invocation クラスのインスタンスを取得します。
この Invocation インスタンスを使用することによりメソッドの情報が取得できるので、上記で定義した AccessToken アノテーションを参照することができます。

object AuthInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val invocation = request.tag(Invocation::class.java)
        val accessToken = invocation?.method()?.getAnnotation(AccessToken::class.java) ?: return chain.proceed(request)

        return chain.proceed(
            request.newBuilder()
                .addHeader("Authorization", "Bearer ${accessToken.token}")
                .build()
        )
    }
}

使い方によっては、今まで実装が難しかったことも簡単にできる気がします。

参考

https://gist.github.com/swankjesse/16389e5b57b04fd3ca28a798c1e90910

Android と iOS のロジックを Kotlin Native で共通化する

最近 Kotlin Native が気になっていたので、Kotlin Native の勉強がてらにサンプルプロジェクトを作成してみました。

このサンプルプロジェクトでは以下のことを行なっています。

  1. 楽天のランキング API を使用して、ランキングデータを取得
  2. 取得したランキングデータを一覧表示
  3. 一覧をタップすると WebView で商品の詳細情報ページを表示
  4. 詳細情報ページを閲覧した商品をアプリ内のデータベースに最大10件まで保存し表示


Android iOS
f:id:watabee_dev:20181117222611p:plain:w260 f:id:watabee_dev:20181117222632p:plain:w260

AndroidiOS で View 周り以外では共通のロジックを使っています。

HTTP 通信

HTTP クライアントには ktor を使用してます。

ktor の HTTP クライアントに Engine を指定するのですが、Android では OkHttpiOSIos を使用しています。

JSON からデータモデルへの変換

JSON からデータモデルへの変換も ktor の JsonFeature を使用することで可能です。

内部的には kotlinx.serialization が使用されています。

SQLite

SQLiteSQLDelight の fork 版で使用できます。

sq ファイルに SQL を記述して特定の Gradle Task を実行することで、SQL を実行するコードやデータモデルが自動生成されます。

感想

HTTP 通信処理とデータベースなどの永続化処理をプラットフォーム共通で記述できるのであれば、シンプルなプロジェクトであれば割と使えるのではないでしょうか。

現状でも例えば Swagger Codegen で Kotlin のデータモデルのコードを自動生成して、それを AndroidiOS で共通で使用するといったくらいであれば可能かと思います。

iOS のメインスレッド以外での Kotlin Coroutine のサポート本家の SQLDelight でのマルチプラットフォームサポート が充実すれば、よりKotlin Native を使用してのプロジェクト実装が実現的になる気がします。

参考

実装するにあたって、以下のプロジェクトを参考にしました。