Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

【iOS】Unity Framework とクラッシュ解析の取り組み

こんにちは、Mirrativ iOS エンジニアのちぎらです。クラッシュが発生して、その原因が分からないととてもかなしい気持ちになります。このブログでも以前から触れているように、Mirrativ のクライアントアプリではエモモなどの表示に Unity を使用しています。今回は、Unity の Framework とその内部で発生したクラッシュ解析の取り組みについて紹介をしたいと思います。

隠されたクラッシュ情報

Mirrativ iOS アプリではクラッシュ情報の解析と集計に Firebase Crashlytics を使用しています。Bitcode を有効にしている場合、App Store Connect からダウンロードした dSYM ファイルを Firebase Crashlytics にアップロードすることによってクラッシュ情報の詳細が見えるようになります。しかし、dSYM ファイルをアップロードした後も特定のライブラリに関するクラッシュ情報の詳細が _hidden となって隠されてしまう場合があります1。Mirrativ の iOS アプリの場合、UnityFramework がそれに該当していました。

f:id:naru-jpn:20210618151845p:plain:w500
詳細が _hidden となっているクラッシュ情報

このクラッシュの原因がフレームワーク側の実装にありそうか、そもそも解消が難しい問題なのか、詳細が分からないままでは議論の余地もありません。 _hidden となってしまう現象は Bitcode を無効にすれば解消されるようですが、これを理由にして Bitcode を無効にしたくもありません。

以下では、 _hidden 解消までの道のりを順を追ってみていきます。

_hidden はどこからきているのか

Firebase Crashlytics には dSYM ファイルをちゃんとアップロードしているはずですが2、どの時点で _hidden という状態になってしまっているのでしょうか。まずは実際に _hidden という表記が見つかるところまで掘り下げていきます。

Unity Framework に対応する dSYM ファイルを探す

_hidden はどこからきているのかを調べるためには、まず App Store Connect からダウンロードした dSYM ファイルの内どれが UnityFramework に対応したものかを知る必要があります。

f:id:naru-jpn:20210621155252p:plain:w500
App Store Connect からダウンロードしてきた dSYM ファイル達

***.dSYM ファイルは実はディレクトリになっていて、ディレクトリの奥地には dSYM ファイルの実体が、それぞれ対応するモジュールの名前で配置されています。

# ***.dSYM ファイルは実はディレクトリ
$ file ./21bd27d3-4dd8-3d3f-b877-c83696a83557.dSYM
./21bd27d3-4dd8-3d3f-b877-c83696a83557.dSYM: directory

# 各 ***.dSYM ファイルの奥地には、対応するモジュール名で名付けられたファイルが配置されている
$ ls ./21bd27d3-4dd8-3d3f-b877-c83696a83557.dSYM/Contents/Resources/DWARF
mirrativ
$ ls ./2ba4bd27-261b-373d-91a1-d7c6b64d9c14.dSYM/Contents/Resources/DWARF
UnityFramework

# 奥地にあるファイルが dSYM ファイルの実体
$ cd ./21bd27d3-4dd8-3d3f-b877-c83696a83557.dSYM/Contents/Resources/DWARF/
$ file ./mirrativ 
mirrativ: Mach-O 64-bit dSYM companion file arm64

つまり、どの ***.dSYM ファイルの奥地に UnityFramework という名前のファイルが格納されているのか?というのを調べればよいのですが、人間には非常にやりづらい作業です。以下のようなスクリプト find_dsym.sh を準備し、各 ***.dSYM ファイルと奥地に配置されているファイルの名前の対応を分かりやすく表示できるようにしました。

files=`ls "$1"`

for uuid in ${files[@]}; do
  module=`ls "$1/${uuid}/Contents/Resources/DWARF/"`
  echo "${uuid:0:8}: ${module}"
done

このスクリプトを実行すると、以下のような結果が得られます。これでどの dSYM ファイルが UnityFramework に対応したものなのかが一目でわかるようになりました。

$ ./find_dsym.sh {path_to_appDsyms_directory}
01526605: Shared
21bd27d3: mirrativ
2ba4bd27: UnityFramework
2d4dd1c9: widget
2ebf8484: ...

dSYM ファイルの中身が _hidden になっていることを確認する

dSYM ファイルは dwarfdump というコマンドで中身を確認することができます。上で見つけた UnityFramework に対応する dSYM ファイルの中身を覗いてみましょう。

$ dwarfdump ./2ba4bd27-261b-373d-91a1-d7c6b64d9c14.dSYM | less

dwarfdump は大量のテキストを出力してターミナルを文字で埋め尽くしてしまうので、 結果を less に流して確認しています。上のコマンドを実行すると、以下のような出力が確認できます。

2ba4bd27-261b-373d-91a1-d7c6b64d9c14.dSYM/Contents/Resources/DWARF/UnityFramework:       file format Mach-O arm64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000322, format = DWARF32, version = 0x0004, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x00000326)

0x0000000b: DW_TAG_compile_unit
              DW_AT_producer    ("__hidden#178_")
              DW_AT_language    (DW_LANG_ObjC_plus_plus)
              DW_AT_name        ("__hidden#179_")
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("__hidden#180_")
              DW_AT_APPLE_optimized     (true)
              DW_AT_APPLE_major_runtime_vers    (0x02)
              DW_AT_low_pc      (0x0000000000005970)
              DW_AT_high_pc     (0x00000000000074bc)

0x0000002b:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000005970)
                DW_AT_high_pc   (0x0000000000005a00)
                DW_AT_call_all_calls    (true)
                DW_AT_name      ("__hidden#0_")

0x0000003c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000005a00)
                ...

_hidden という文字列が確認できました。つまり、App Store Connect からダウンロードした dSYM ファイルの内容が既に _hidden となっていて、従って Firebase Crashlytics 上の表示も _hidden となっていた訳です。

クラッシュ情報を復元する

Firebase Crashlytics 上の _hidden がどこからくるのかが分かりました。今度はクラッシュ情報を復元します。

bcsymbolmap ファイルを使って dSYM ファイルの内容を復元する

UnityFramework を生成する際、副産物として bcsymbolmap というファイルが生成されます。bcsymbolmap ファイルにもユニークな uuid が名前として付けられており、例えば上の UnityFramework には E003569B-D03E-3032-9B07-F39A6DE94BAC.bcsymbolmap というファイルが付随して生成されています。この bcsymbolmap ファイルは UnityFramework のビルドと同時に生成されるので、生成された Framework と紐付けて保存しておく必要があります。

bcsymbolmap ファイルはテキスト形式で中身を見れるようになっています。中身を覗いてみましょう。

$ less ./BCSymbolMaps/E003569B-D03E-3032-9B07-F39A6DE94BAC.bcsymbolmap

BCSymbolMap Version: 2.0
+[UnityURLRequest storeRequest:taskID:]
+[UnityURLRequest requestForTask:]
+[UnityURLRequest removeRequest:]
...

メソッド名らしきものが並んでいます。dsymutil というコマンドに _hidden となっている dSYM ファイルとそれに対応する bcsymbolmap ファイルの情報を渡すことによって、dSYM ファイルの内容を復元することができます。3

# --symbol-map オプションには bcsymbolmap ファイルが含まれるディレクトリを指定する
$ dsymutil --symbol-map ./BCSymbolMaps ./2ba4bd27-261b-373d-91a1-d7c6b64d9c14.dSYM

上記を実行した後、もう一度 dwarfdump で dSYM ファイルの中身を確認してみます。( Framework ビルド時のファイルの絶対パスなどが含まれているので一部伏字 *** にしています )

$ dwarfdump ./2ba4bd27-261b-373d-91a1-d7c6b64d9c14.dSYM | less

2ba4bd27-261b-373d-91a1-d7c6b64d9c14.dSYM/Contents/Resources/DWARF/UnityFramework:      file format Mach-O arm64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000322, format = DWARF32, version = 0x0004, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x00000326)

0x0000000b: DW_TAG_compile_unit
              DW_AT_producer    ("Apple clang version 11.0.3 (clang-1103.0.32.62)")
              DW_AT_language    (DW_LANG_ObjC_plus_plus)
              DW_AT_name        ("/***/Classes/Unity/UnityWebRequest.mm")
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("***")
              DW_AT_APPLE_optimized     (true)
              DW_AT_APPLE_major_runtime_vers    (0x02)
              DW_AT_low_pc      (0x0000000000005970)
              DW_AT_high_pc     (0x00000000000074bc)

0x0000002b:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000005970)
                DW_AT_high_pc   (0x0000000000005a00)
                DW_AT_call_all_calls    (true)
                DW_AT_name      ("+[UnityURLRequest storeRequest:taskID:]")

0x0000003c:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000005a00)
                DW_AT_high_pc   (0x0000000000005b54)
                DW_AT_call_all_calls    (true)
                DW_AT_name      ("+[UnityURLRequest requestForTask:]")

0x0000004d:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000005b54)
...

無事、dSYM ファイルに含まれていた _hidden が人間に読める文字列になりました。

Firebase Crashlytics に復元した dSYM ファイルをアップロードする

あとは情報を復元した dSYM ファイルを Firebase Crashlytics にアップロードするだけです。4 すでに Firebase Crashlytics 上で集計されたクラッシュはそのままですが、新たに発生したクラッシュには復元されたクラッシュ情報が付与されます。

f:id:naru-jpn:20210622103406p:plain:w500
復元されたクラッシュ情報

これで無事クラッシュ情報が Firebase Crashlytics 上で確認できるようになり、問題の把握に一歩だけ近づきました!

クラッシュ情報の中に _hidden という文字列を見かけた際には、この記事を思い出していただけると幸いです🙂

We are hiring!

Mirrativ では一緒にアプリを作ってくれる iOS エンジニアを募集中です!気軽にご連絡ください!

www.mirrativ.co.jp

speakerdeck.com


  1. 公式では obfuscated symbolshidden symbols などと呼ばれています。 Building Your App to Include Debugging Information

  2. Firebase Crashlytics のバージョン 8.0.0 から hidden symbols を含む dSYM ファイルをアップロードする際に警告が出るようになりました。Firebase iOS Release Notes

  3. Adding Identifiable Symbol Names to a Crash Report - Restore Hidden Symbols

  4. Firebase に確認したところ、同名で内容の異なる dSYM ファイルをアップロードした場合、古い dSYM ファイルを新しいもので上書きしてくれるようです。つまり、とりあえず全ての dSYM ファイルをアップロードしておいて、後で必要に応じて情報を復元した dSYM ファイルだけアップロードする、という運用でいけます。