実機デバッグしたときにクラッシュログを端末から取得する

この記事は、[and factory.inc Advent Calendar 2021](https://qiita.com/advent-calendar/2021/andfactory)の 10日目 の記事です。
昨日は、@hagmonさんの「[【Flutter】iOS課金処理の、PurchaseStatus.pendingの挙動について](https://qiita.com/hagmon/items/d994830669270646c92b)」でした。

## はじめに
多くのアプリではFirebase Crashlyticsなどを使ってクラッシュログ解析をしていると思います。
ツールに則って運用している間は特に問題ないですが、
実機から直接クラッシュトレースを取得したいなど、
時としてマニュアル操作が必要になる場面が発生します。

きちんとdSYMやクラッシュトレース取得の仕組みがわかっていなかったので、
半端な知識で毎回ググって凌いできましたが、備忘ためにも記事にしました。

## 概要
- クラッシュログの収集に関する基本的な知識について
- 実機デバッグしたときにクラッシュログを端末から取得する方法について
- 過去にハマったこと
    - クラッシュログに紐づく正しいdSYMを使用できていない
    - 以前にクラッシュした日付よりも過去の日付で発生したクラッシュは検出してくれない？

## 想定している読み手
- 端末のクラッシュログからクラッシュトレースの生成方法を調べている方
- クラッシュログに関する知識を知りたい・確認したい方
- クラッシュログ収集のトラブルシューティングをしている方

## クラッシュログの収集に関する基本的な知識

### 言葉の定義

[Appleのドキュメント](https://developer.apple.com/documentation/xcode/building-your-app-to-include-debugging-information)から頻出の言葉を以下のように理解したので、先に定義しておきます

**シンボル**

- クラス名、変数名、関数名などの情報

**シンボル化**

- 秘匿化されたり難読化されているシンボル情報を判別可能な形に変換すること

**Debug symbol**

- アプリをコンパイルする過程で生成される、シンボル（クラス、変数、関数の情報）とその行数が記載されているリスト
    - Xcodeのデバッガーや、クラッシュレポートはこのDebug Symbolによってクラッシュ発生源などを特定できる
    - デバッグビルド時
        - バイナリファイル内にDebug Symbolが配置されている
    - リリースビルド時
        - アプリ容量の削減のためバイナリファイル内には含まれず、別のファイル（**dSYM**）として作成される

**dSYM（Debug Symbol file）**

- リリースビルドのときにバイナリファイルとは別に作られるシンボル情報を持ったファイル
- バイナリファイルとdSYMはUUIDによって紐付けることができる
    - バイナリファイルは、App、Framework、App Extensionなどのターゲットや依存ライブラリごとに作られる
    - 異なるBuildSettingやXcodeバージョンから生成されたUUIDは一致しない
    - つまり一口にdSYMといっても、シンボル化したいすべてのターゲットについてdSYMを用意しなきゃいけないし、
    XcodeのバージョンやBuild Settingが変わるとUUIDが変わってしまうので、
    UUIDが一致したdSYM郡を用意したりする必要がある

### クラッシュログ収集の大まかな流れ

- クラッシュログファイルとdSYMファイルを用意します
- クラッシュログには、シンボルの情報が付随していてdSYMを参照することでどこで何が起きたかの情報がわかります

### dSYMを取得するための方法
dSYMの取得方法を調べると様々なバリエーションがありますが、
主に以下の3つのシチュエーションに応じた方法があると理解してます

1. アーカイブ済みアプリをAppStoreConnetへアップロードした場合
    - アップロード後に生成されるdSYMをAppStoreConnectからダウンロードするなどして取得
    - 本番アプリでは、こちらで取得したものをCrashlyticsにあげる運用が多いかと思います
2. アーカイブしたXcodeから取得する場合
    - Xcodeの`Window>Organizer`から取得
        - 詳しくは把握していませんがfastlaneなどで配布版アプリをアーカイブする際にもオプション等で出力できるのかと推測してます
3. ローカルのXcodeでRunした場合
    - `DerrivedData`配下に生成されるファイルを使用

**今回の記事では3の方法について記載します。**

## 実機デバッグ時にクラッシュログを取得する方法

### 必要なもの

- 端末に記録されているクラッシュログ
    - .ipsファイル or .crashファイル
- .dSYMファイル

### クラッシュログの取得方法

1. 端末の`設定`>`プライバシー`>`解析及び改善`を選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287671/09389bee-a42c-f6d8-e1d1-dc65576d0cf3.jpeg" width=50%>

2. 必要なipsファイルを選択
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287671/c7dbba37-6c65-ea27-e8e3-73e841cd0317.jpeg" width=50%>

3. 選択先の右上のシェアボタンがあるのでAir Dropなどでファイルを取得

### dSYMファイルの取得方法

1. `DEBUG_INFORMATION_FORMAT`を`dwarf-with-dsym`に設定する（Releaseビルドではデフォルトで`dwarf-with-dsym`に設定されているはず）
2. ビルドする
3. dSYMが以下のパスに作成されているのでコピーするなどして取得する

```
/Users/<hoge>/Library/Developer/Xcode/DerivedData/<HogeApp-random-strings>/Build/Products/<hoge>.app.dSYM
```

### 必要なファイルからログを出力する
用意した必要なファイルからログを出力します
ログを出力するためにXcodeに付属している`symbolicatecrash`というツールを使用します

前提として、.zshrcや.bashrcに以下を設定しておきます

```
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
```

その上で以下を実行すると、クラッシュログが生成されます

```
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash -d <path_to_dsym> -o <output file> <path_to_crash_report>
```

## 過去にハマったこと

過去にハマったことを記載してみましたが、後々確かめてみたところ必ずしも再現するわけではないようです。
あくまで、もし同じような事象に遭遇した方の解決の一助になれば程度ですが、記載していおきます🙇‍♂️

### クラッシュログに紐づく正しいdSYMを使用できていない

過去にデバッグビルドで取得したログの場合に、上記のエラーが出てシンボル化ができなかったことがありました。

`Error: Can't find "Binary Images" section in log file at /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash line 573.

当時はリリースビルドで再度ログを発生させてみたら、シンボル化することができるようになったので、
「リリースビルドじゃないとダメなのか〜」と思っていましたが、
使用するdSYMが正しくなかった（UUIDが違った）のかもしれません
エラーに遭遇した方は、クラッシュファイルとdSYMのUUIDが正しいか確認してみてください

### 以前にクラッシュした日付よりも過去の日付で発生したクラッシュログは端末上で検出してくれない?

端末でクラッシュログを取得する際に以下のケースでクラッシュログを取れないことがありました

1. 設定アプリで日付を未来日にして、クラッシュログを生成していた
2. その後、現在時刻に戻して再度クラッシュログを発生させたがログが記録されない
3. 1より未来日に設定してクラッシュを発生させたら、再度記録されるようになった

こちらも再現しないこともあるようでしたが、
クラッシュが記録されていないことがあったらこちらを確認してみるといいかもしれません。

## 参考
- [Apple - Building Your App to Include Debugging Information](https://developer.apple.com/documentation/xcode/building-your-app-to-include-debugging-information)
- [iOSアプリのクラッシュログから解析する](https://qiita.com/edo_m18/items/dbf720b1964f7cd5e9b9)
- [iOSアプリケーションのクラッシュログを見つける方法とiOSのクラッシュログをシンボル化する方法](https://qiita.com/MaShunzhe/items/1c98dcb07fd63e756589)