ドリコム Advent Calendar 2017

はじめに

これは ドリコム Advent Calendar 2017 の6日目です。
5日目は taji さんによる 模写で学べる事 です。

自己紹介

  • HN : ogwmtnr
  • ドリコム歴 32ヶ月目
    • 2015年4月入社
  • DRIP 所属
  • 古参音ゲーマー
    • 好きなうさぎは Macho Gang のうさぎ
  • 豚バラ肉および豚バラ肉を用いた料理が好きです

この記事について

ある日、Xcode 9 環境で GoogleAnalytics を導入した iOS プロジェクトを扱うことがあり、その際に問題が発生した。
レガシー1なライブラリの情報ゆえかググっても明確な解決方法が出てこなかったので、今回は上記問題に対する調査の流れや解決策について、報告させていただきます。

環境

  • Mac OS 10.12.6
  • Xcode 9.1
    • iOS SDK 11.1
    • Swift 4.0
  • Ruby 2.2.2
    • gem 2.4.5
    • cocoapods 1.3.1

経緯

はじめ

GoogleAnalytics は CocoaPods を使って導入します。
以下のような Podfile を持つ HogeApp という iOS プロジェクトがあったとします。

Podfile
# Uncomment the next line to define a global platform for your project
platform :ios, '10.0'
swift_version = '4.0'

target 'HogeApp' do
  use_frameworks!

  pod 'Fabric'
  pod 'Crashlytics'
  pod 'Google/Analytics'

  target 'HogeAppTests' do
    inherit! :search_paths
    pod 'Fabric'
    pod 'Crashlytics'
    pod 'Google/Analytics'
  end

end

クラッシュ発生

実機を Mac に繋ぎ、対象プロジェクトを開いて Cmd+U を叩き Test を走らせます。
すると、クラッシュ。

Thread 9: -[UIApplication delegate] must be used from main thread only

Thread 9 Queue : FIRAnalyticsQueue (serial)
#0  0x0000000101509964 in __main_thread_checker_on_report ()
#1  0x0000000101509cd8 in __ASSERT_API_MUST_BE_CALLED_FROM_MAIN_THREAD_FAILED__ ()
#2  0x0000000101509ef4 in checker_c ()
#3  0x00000001015098e8 in trampoline_c ()
#4  0x00000001014c96b4 in handler_start ()
#5  0x0000000101215524 in __38+[FIRAnalytics createAppDelegateProxy]_block_invoke ()
#6  0x0000000186739048 in _dispatch_client_callout ()
#7  0x000000018673c710 in dispatch_once_f$VARIANT$mp ()
#8  0x0000000101213bbc in __47+[FIRAnalytics startWithConfiguration:options:]_block_invoke_2 ()
#9  0x0000000186739088 in _dispatch_call_block_and_release ()
#10 0x0000000186739048 in _dispatch_client_callout ()
#11 0x0000000186742e48 in _dispatch_queue_serial_drain$VARIANT$mp ()
#12 0x00000001867437d8 in _dispatch_queue_invoke$VARIANT$mp ()
#13 0x0000000186744200 in _dispatch_root_queue_drain_deferred_wlh$VARIANT$mp ()
#14 0x000000018674c4a0 in _dispatch_workloop_worker_thread$VARIANT$mp ()
#15 0x00000001869defd0 in _pthread_wqthread ()
#16 0x00000001869dec20 in start_wqthread ()

FIRAnalytics が悪さしているもよう。そのまま FIRAnalytics でググってみます。
どうやら FIRAnalytics は FirebaseAnalytics で使っているクラスとのこと。

調査

Debug、Run ではどうか?

クラッシュはしない。動いては、いる。
GoogleAnalytics ダッシュボードにも値が来ているのを確認。
ただ、出力にチラリと光る怪しいモノ。

Main Thread Checker: UI API called on a background thread: -[UIApplication delegate]
PID: 477, TID: 296284, Thread name: (none), Queue name: FIRAnalyticsQueue, QoS: 9
Backtrace:
4   HogeApp                             0x00000001007a9524 __38+[FIRAnalytics createAppDelegateProxy]_block_invoke + 56
5   libdispatch.dylib                   0x000000010249d45c _dispatch_client_callout + 16
6   libdispatch.dylib                   0x000000010249e17c dispatch_once_f + 120
7   HogeApp                             0x00000001007a7bbc __47+[FIRAnalytics startWithConfiguration:options:]_block_invoke_2 + 104
8   libdispatch.dylib                   0x000000010249d49c _dispatch_call_block_and_release + 24
9   libdispatch.dylib                   0x000000010249d45c _dispatch_client_callout + 16
10  libdispatch.dylib                   0x00000001024ac110 _dispatch_queue_serial_drain + 692
11  libdispatch.dylib                   0x00000001024a09a4 _dispatch_queue_invoke + 332
12  libdispatch.dylib                   0x00000001024ad104 _dispatch_root_queue_drain_deferred_wlh + 424
13  libdispatch.dylib                   0x00000001024b4100 _dispatch_workloop_worker_thread + 652
14  libsystem_pthread.dylib             0x00000001869defd0 _pthread_wqthread + 932
15  libsystem_pthread.dylib             0x00000001869dec20 start_wqthread + 4
2017-11-27 18:47:20.971119+0900 HogeApp[477:296284] [reports] Main Thread Checker: UI API called on a background thread: -[UIApplication delegate]
PID: 477, TID: 296284, Thread name: (none), Queue name: FIRAnalyticsQueue, QoS: 9
Backtrace:
4   HogeApp                             0x00000001007a9524 __38+[FIRAnalytics createAppDelegateProxy]_block_invoke + 56
5   libdispatch.dylib                   0x000000010249d45c _dispatch_client_callout + 16
6   libdispatch.dylib                   0x000000010249e17c dispatch_once_f + 120
7   HogeApp                             0x00000001007a7bbc __47+[FIRAnalytics startWithConfiguration:options:]_block_invoke_2 + 104
8   libdispatch.dylib                   0x000000010249d49c _dispatch_call_block_and_release + 24
9   libdispatch.dylib                   0x000000010249d45c _dispatch_client_callout + 16
10  libdispatch.dylib                   0x00000001024ac110 _dispatch_queue_serial_drain + 692
11  libdispatch.dylib                   0x00000001024a09a4 _dispatch_qu2017-11-27 18:47:21.015465+0900 HogeApp[477:296288] TIC Read Status [2:0x0]: 1:57
2017-11-27 18:47:21.015527+0900 HogeApp[477:296288] TIC Read Status [2:0x0]: 1:57
eue_invoke + 332
12  libdispatch.dylib                   0x00000001024ad104 _dispatch_root_queue_drain_deferred_wlh + 424
13  libdispatch.dylib                   0x00000001024b4100 _dispatch_workloop_worker_thread + 652
14  libsystem_pthread.dylib             0x00000001869defd0 _pthread_wqthread + 932
15  libsystem_pthread.dylib             0x00000001869dec20 start_wqthread + 4
2017-11-27 18:47:21.167076+0900 HogeApp[477:296288] [Firebase/Analytics][I-ACS023007] Firebase Analytics v.3900000 started
2017-11-27 18:47:21.167 HogeApp[477] <Notice> [Firebase/Analytics][I-ACS023007] Firebase Analytics v.3900000 started
2017-11-27 18:47:21.167934+0900 HogeApp[477:296288] [Firebase/Analytics][I-ACS023008] To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled (see http://goo.gl/RfcP7r)
2017-11-27 18:47:21.167 HogeApp[477] <Notice> [Firebase/Analytics][I-ACS023008] To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled (see http://goo.gl/RfcP7r)
2017-11-27 18:47:21.179107+0900 HogeApp[477:296288] [Firebase/Analytics][I-ACS003007] Successfully created Firebase Analytics App Delegate Proxy automatically. To disable the proxy, set the flag FirebaseAppDelegateProxyEnabled to NO in the Info.plist
2017-11-27 18:47:21.186 HogeApp[477] <Notice> [Firebase/Analytics][I-ACS003007] Successfully created Firebase Analytics App Delegate Proxy automatically. To disable the proxy, set the flag FirebaseAppDelegateProxyEnabled to NO in the Info.plist

Main Thread Checker という単語が出てきました。

Main Thread Checker とは?

Xcode 9 から追加された機能で、メインスレッドで呼ぶべきコードが実際にメインスレッドで呼ばれているかどうか検出するものです。
その上で上記エラーを見ると [UIApplication delegate] が異なるスレッドで呼び出されているため、クラッシュしたことがわかります。

Main Thread Checker を無効にすれば良い?

スキーマ選択の箇所をクリックして「Edit Schema」 -> 「Test」 -> 「Diagnostics」へ行き、Main Thread Checker のチェックを外すことで無効化できます。
無効化した状態でテストを走らせてみると、テストが成功しました。

やった!完! ...?

まさに同じ問題を同じやり方でテスト走るようにしてる方を見つけました。
作者曰く「Remember, this isn’t the ideal solution, this is the hack that we shouldn’t be putting in our code.」意訳すると「良い解決方法ではないよー」とのこと。自分も同意です。
なので Main Thread Checker を有効にしたままでの解決方法を調べていきます。

同じ状況になった先人が居ないか調べる

GoogleAnalytics main thread checker でググってみたところ、日本語の記事が上位にあったので見てみます。
しかもタイトルが個人アプリを Xcode9 Swift4 対応した時にやったことなのでピッタリな内容かも。ありがとうございます。本当にありがとうございます。

曰く、

ググったら Firebase が古い時のエラーのようで、 Firebase を新しくすれば消える。

とのこと。
つまり Firebase 4.x 系にすればエラーは解消される。
今の Podfile.lock を確認してみます。

Podfile.lock
PODS:
  - Crashlytics (3.9.3):
    - Fabric (~> 1.7.2)
  - Fabric (1.7.2)
  - FirebaseAnalytics (3.9.0):
    - FirebaseCore (~> 3.6)
    - FirebaseInstanceID (~> 1.0)
    - GoogleToolboxForMac/NSData+zlib (~> 2.1)
  - FirebaseCore (3.6.0):
    - GoogleToolboxForMac/NSData+zlib (~> 2.1)
  - FirebaseInstanceID (1.0.10):
    - FirebaseCore (~> 3.6)
  - Google/Analytics (3.1.0):
    - Google/Core
    - GoogleAnalytics (~> 3.12)
  - Google/Core (3.1.0):
    - FirebaseAnalytics (~> 3.2)
  - GoogleAnalytics (3.17.0)
  - GoogleToolboxForMac/Defines (2.1.3)
  - GoogleToolboxForMac/NSData+zlib (2.1.3):
    - GoogleToolboxForMac/Defines (= 2.1.3)

FirebaseAnalytics (3.9.0): が確認できます。
つまり、今入っているのは Firebase 3.x 系です。
記事の通りであれば、確かにエラーとなりますね。

Firebase 4.x 系を入れよう

Podfile
# Uncomment the next line to define a global platform for your project
platform :ios, '10.0'
swift_version = '4.0'

target 'HogeApp' do
  use_frameworks!

  pod 'Fabric'
  pod 'Crashlytics'
  pod 'Google/Analytics'
  pod 'Firebase', '~> 4.0'

  target 'HogeAppTests' do
    inherit! :search_paths
    pod 'Fabric'
    pod 'Crashlytics'
    pod 'Google/Analytics'
    pod 'Firebase', '~> 4.0'
  end

end

よしよし。

$ pod install
Analyzing dependencies
[!] Unable to satisfy the following requirements:

- `FirebaseAnalytics (= 3.9.0)` required by `Podfile.lock`
- `FirebaseAnalytics (~> 3.2)` required by `Google/Core (3.1.0)`
- `FirebaseAnalytics (= 4.0.0)` required by `Firebase/Core (4.0.0)`

むむむ、エラー!
依存関係を解決できないっぽい?

何故 Firebase 4.x 系をインストールできないのか調べる

先程の Podfile.lock を見てみましょう。

Podfile.lock
PODS:
  - Crashlytics (3.9.3):
    - Fabric (~> 1.7.2)
  - Fabric (1.7.2)
  - FirebaseAnalytics (3.9.0):
    - FirebaseCore (~> 3.6)
    - FirebaseInstanceID (~> 1.0)
    - GoogleToolboxForMac/NSData+zlib (~> 2.1)
  - FirebaseCore (3.6.0):
    - GoogleToolboxForMac/NSData+zlib (~> 2.1)
  - FirebaseInstanceID (1.0.10):
    - FirebaseCore (~> 3.6)
  - Google/Analytics (3.1.0):
    - Google/Core
    - GoogleAnalytics (~> 3.12)
  - Google/Core (3.1.0):
    - FirebaseAnalytics (~> 3.2)
  - GoogleAnalytics (3.17.0)
  - GoogleToolboxForMac/Defines (2.1.3)
  - GoogleToolboxForMac/NSData+zlib (2.1.3):
    - GoogleToolboxForMac/Defines (= 2.1.3)

Google/Core (3.1.0): のところに FirebaseAnalytics (~> 3.2) とあります。
そして Google/Analytics (3.1.0): のところに Google/Core とあるため、GoogleAnalytics が Firebase 4.x 系 のインストールを阻害しているようです。
まじか...

先に Firebase 4.x 系を入れてから GoogleAnalytics を入れたらどうなる?

ここでふと、思いつきます。
「先に Firebase 4.x 系を入れてから GoogleAnalytics を入れたら、依存関係の無い GoogleAnalytics がインストールされるのではないか...?」
と。
やってみましょう。やっていき!
先程の Podfile から pod 'Google/Analytics' 行をコメントアウト。

$ pod install
Analyzing dependencies
Downloading dependencies
Using Crashlytics (3.9.3)
Using Fabric (1.7.2)
Using Firebase (4.6.0)
Using FirebaseAnalytics (4.0.5)
Using FirebaseCore (4.0.11)
Using FirebaseInstanceID (2.0.6)
Installing Google (2.0.4)
Installing GoogleAnalytics (3.17.0)
Installing GoogleInterchangeUtilities (1.2.2)
Installing GoogleNetworkingUtilities (1.2.2)
Installing GoogleSymbolUtilities (1.1.2)
Using GoogleToolboxForMac (2.1.3)
Installing GoogleUtilities (1.3.2)
Using nanopb (0.3.8)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There are 5 dependencies from the Podfile and 14 total pods installed.

成功しました。Firebase (4.6.0) が入っています。
まぁこれは当然ですね。
次に pod 'Google/Analytics' 行を復活させて pod install してみます。

予想

  1. 古い Google/Analytics がインストールされる。
  2. 依存関係が解決できずにインストール失敗する。

さあ、どうなる?

結果

$ pod install
Analyzing dependencies
Downloading dependencies
Using Crashlytics (3.9.3)
Using Fabric (1.7.2)
Using Firebase (4.6.0)
Using FirebaseAnalytics (4.0.5)
Using FirebaseCore (4.0.11)
Using FirebaseInstanceID (2.0.6)
Installing Google (2.0.4)
Installing GoogleAnalytics (3.17.0)
Installing GoogleInterchangeUtilities (1.2.2)
Installing GoogleNetworkingUtilities (1.2.2)
Installing GoogleSymbolUtilities (1.1.2)
Using GoogleToolboxForMac (2.1.3)
Installing GoogleUtilities (1.3.2)
Using nanopb (0.3.8)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There are 5 dependencies from the Podfile and 14 total pods installed.

入った!インストール成功した!
でもなんで?

何故、依存関係が解決したのか調べる

Podfile.lock Google/Core の箇所を見てみます。

Podfile.lock
  - Google/Core (2.0.4):
    - GoogleInterchangeUtilities (~> 1.0)
    - GoogleNetworkingUtilities (~> 1.0)
    - GoogleSymbolUtilities (~> 1.0)
    - GoogleUtilities (~> 1.1)

Google/Core が 3.x 系から 2.x 系に変わり、依存するパッケージが丸ごと切り替わっています。

Podfile.lock
  - Google/Analytics (3.1.0):
    - Google/Core
    - GoogleAnalytics (~> 3.12)

Google/Analytics (3.1.0):Google/Core に依存していますが、バージョンの依存はしていないので、Firebase 3.x 系を使わないバージョンまで Google/Core が若返った、と見て良さそうです。
これでライブラリのインストールはできるようになりました。

各種動作は?

Main Thread Checker 有効を確認して実機での動作を確認したところ、クラッシュしたような出力無く動作しました。

スクリーンビュー
スクリーンビュー.png

イベント
イベント.png
も、ちゃんと飛んでいます。

そして Main Thread Checker 有効にしたままテストを動かしたところ、問題無くテスト成功しました。

解決

今までの調査から、Main Thread Checker を有効にしたまま開発・運用が可能な2通りの解決方法を見出しました。

① FirebaseAnalytics に切り替える

GoogleAnalytics を辞めて Firebase に切り替えれば依存関係が無くなり Firebase 4.x 系が入るようになるはずなので、移行すれば解決しそうです。
今後も最新バージョンの維持を期待できるため、積極的な解決方法と言えます。
コード側は以下のようにすれば切り替え完了。

  • Firebase 初期設定を終わらせる
    • Firebase の管理画面でアプリ登録して GoogleService-Info.plist を作ったりなど、必要な設定をしていきます。
  • Podfile 書き換える
  • AppDelegate での初期化コードを書き換える
  • イベント送るコードを書き換える
    • スクリーンビューという概念は無くなるので、目的の値をイベントだけで取れるように書き換えていく。

ただし、GoogleAnalytics で今まで貯めたログを FirebaseAnalytics に移行するのはできない2ので、実装の変更だけでなく「データ分析業務の移行」にもコストが必要になる可能性が高いです。
日頃どのように分析し、どういったツールを使っているかによってもコストが変わってくるので、担当者と話し合いましょう。

② GoogleAnalytics を使い続ける

上記移行コストがどうしても支払えない、もしくは可能な限り GoogleAnalytics コンソールだけで計測していきたい、という意思がある場合に限り、Google/Core 2.x 系を使うことで GoogleAnalytics でも計測していけそうです。
ただしレガシー1であるため、ある日突然使えなくなるかもしれないリスクがあり、消極的な解決方法だという事は心に刻んでおきましょう。
また、それをエンジニア以外のチームメンバーにも認識してもらいましょう。不満が出たら①を提示すべし。

pod install 一発で依存関係を解決できる Podfile を作る

今のままだと

  1. Podfile から pod 'Google/Analytics' 行をコメントアウトする。
  2. pod install
  3. Podfile の pod 'Google/Analytics' 行を復活させる。
  4. pod install

ということをしなければ依存関係が解決しないので、手間である。
そこで、キーマンならぬキーパッケージの Google/Core (2.0.4) を Podfile に追記します。
全体像はこのようになりました。

Podfile
# Uncomment the next line to define a global platform for your project
platform :ios, '10.0'
swift_version = '4.0'

target 'HogeApp' do
  use_frameworks!

  pod 'Fabric'
  pod 'Crashlytics'
  pod 'Google/Analytics'
  pod 'Google/Core’, '~> 2.0'

  target 'HogeAppTests' do
    inherit! :search_paths
    pod 'Fabric'
    pod 'Crashlytics'
    pod 'Google/Analytics'
    pod 'Google/Core, '~> 2.0'
  end

end

Google/Core 2.x 系が入るようにします。
Podfile.lock および Pods/ 以下を削除して pod install します。

$ pod install
Analyzing dependencies
Downloading dependencies
Installing Crashlytics (3.9.3)
Installing Fabric (1.7.2)
Installing Google (2.0.4)
Installing GoogleAnalytics (3.17.0)
Installing GoogleInterchangeUtilities (1.2.2)
Installing GoogleNetworkingUtilities (1.2.2)
Installing GoogleSymbolUtilities (1.1.2)
Installing GoogleUtilities (1.3.2)
Generating Pods project
Integrating client project
Sending stats
Pod installation complete! There are 5 dependencies from the Podfile and 8 total pods installed.

GoogleAnalytics はバージョン固定しない?

固定する

GoogleAnalytics 4.x 系がリリースされて Google/Core 2.x 系 が逆に依存問題を引き起こす可能性があります。
GoogleAnalytics 3.17.0 を使い続けている(かつ他の Google サービスに関するライブラリを入れない)限りは Google/Core 2.x 系 を使い続けられるはずなので、3.17.0 で固定するのも手です。

Podfile
  pod 'Google/Analytics', '~> 3.17.0'
固定しない

消極的解決とは言え最新バージョンを使えるなら使いたい。
固定しないで定期的に pod update する運用をしていれば、もしかしたら GoogleAnalytics 4.x 系のリリースを検知し、より良い環境にクラスアップできるかもしれません。
上げた結果 Google/Core 2.x 系との依存エラーだったり Main Thread Checker エラーが再発するかもしれません。
解決へコストを使うか、バージョンを下げるかは、その時に判断すれば良いです。
また、バージョン上げても問題無く動作しました!の場合、Google/Core 2.x 系 が不要になる(=ゴミが残る)可能性もあるので、Podfile にコメントを残しておくと良いかもです。

まとめ

SDK や言語のメジャーバージョンアップによって、過去に動いていたものが警告を発したり、また動かなくなったりする事は往々にして発生します。
できれば事前に情報をキャッチして対策を講じておくべきですが、何気なくバージョンアップしてしまうと突然こういった自体に遭遇して

  1. コストを支払って良い環境に移行しリスクを減らす
  2. 今の環境を維持してリスクを抱えつつコストを0にする
  3. あるいは別の手段を模索する

という決断が発生しがちです。
なので、できるだけ正確な判断材料を素早く得る、といったスキルはエンジニアに留まらずあらゆる職種で必要だと僕は考えます。
しかしその情報がネットに無い、先駆者が居ない、なんて場合もあり、それでも調べきってやるんだという「やっていき精神」が重要です。

特に今回の調査で分かれ目になったのは「先に Firebase 4.x 系を入れてから GoogleAnalytics を入れたらどうなる?」のタイミング。
「どうせ依存関係で問題になるしな...」と手を動かさず実態の結果を見なければ、また別の結論が出ていたかもしれません。
実際にやってみて判明する事実はこの世界に多々あり、そこに可能性を見出して実行していく心がけを、今後も維持していきたい。
2年前の自分も良い事言ってました。

という感じで、新しいやり方を思いついたらさっさとコードにして実機で動かす。
失敗してもめげない。
良さげな仮説を思いつく限り、手を動かしていけば、きっと道は開ける。

頑張りましょう。やっていき!

明日は

Takuya Katsurada さんの わがままボディを目指すマスターデータをなんとかしたい です。


  1. Google/CoreThis library is deprecated. すなわち非推奨なため、それに依存している GoogleAnalytics ライブラリも、ここではレガシーと定義しています。GoogleAnalytics サービスそのものはレガシーではないです。 

  2. Firebase で取得したイベントログを GoogleAnalytics 側で表示する事はできます。詳しくはこちら