はじめに
アプリがリリースされてしばらくするとFirebaseから問題があるよって連絡(メール)が来ました。
クラッシュ率の増加で問題があると判断されるんですね。
今回はその調査で苦戦したのでポイントをまとめておきたいと思います。
どんな情報がある?
上部に現在のクラッシュの統計情報が表示されて下部にクラッシュの一覧が表示されています。
どうやって調べるの?
まずは青くなってるところをみます。
大抵ここを訳すだけでなんとなく原因がわかると思います。
Fatal Exception: NSInternalInconsistencyException
Modifications to the layout engine must not be performed from a background thread after it has been accessed from the main thread.
google先生にお出ましいただいて
致命的な例外:NSInternalInconsistencyException
メインスレッドからレイアウトエンジンにアクセスした後、レイアウトエンジンの変更をバックグラウンドスレッドから実行しないでください。
はい。そうです。ごめんさなさいmm
アプリ開発初心者の時ならみなさん一度は経験あるんじゃないでしょうか?
バックグラウンドスレッドからUIアクセスエラーですね。
でスタックトレースの青いところも同じように(TEXTとして)展開して見るとどこのクラスでのクラッシュか情報がありますので場所もある程度特定できます。
ログにはどんな情報が出てるの?
スタックトレースだけじゃわからないこと多いのでログをみてみます。
当然ですがログを出力(コーディング)してないとでません。
個人的にはオペレーションとin/outのデータは欲しいところです。
画面のままじゃ見辛いのでダウンロードしてフィルタするとかして調査すると良いでしょう。
ログの大事さ
ログの出力が細かく定義されているプロジェクトもあると思いますが、そうじゃない場合でも必要最低限のログは埋め込むべきです。
私がログを出す時に最低限心がけていることは
- 操作手順がわかる情報を出力する
- エラーが発生する可能性があるところは必ずin/out/(exception)を出力する
- 意味不明(無駄)・間違ってるログは出力しない
です。
1. 操作手順がわかる情報を出力する
例えば画面を開いた時、ボタンを押した時、画面を閉じた時などの操作情報をデバッグログとして埋め込みます。
画面を開いた時や閉じた時はログ出しておいて損はないと思います。
MOAspectsとかのライブラリを使ってログイベントを横付けして出力することをお勧めします。そうすることで追加漏れもないでしょうし。
MOAspects.hookInstanceMethod(for: UIViewController.self, selector: #selector(UIViewController.viewWillAppear(_:)), position: .before, range: MOAspectsHookRangeAll) { (object: Any?) in
if let object = object {
let className = String(describing: type(of: object))
// Firebase Crashlytics のログ出力
CLSLogv("CLSLogv: \(className) viewWillAppear", getVaList([]))
// もしくは普通にログ出力
log.debug("\(className) viewWillAppear")
}
}
MOAspects.hookInstanceMethod(for: UIViewController.self, selector: #selector(UIViewController.viewDidDisappear(_:)), position: .before, range: MOAspectsHookRangeAll) { (object: Any?) in
if let object = object {
let className = String(describing: type(of: object)))
// Firebase Crashlytics のログ出力
CLSLogv("CLSLogv: \(className) viewDidDisappear", getVaList([]))
// もしくは普通にログ出力
log.debug("\(className) viewDidDisappear")
}
}
あとはこれは必要であればですがViewModelとかのメソッドで処理開始終了とか。
func fetchProcess(id: String) -> FetchResult {
log.debug("fetchProcess start")
// ・・・略・・・
log.debug("fetchProcess end")
return result
}
2. エラーが発生する可能性があるところは必ずin/out(exception)を出力する
何かの処理でエラーになったけど、どんなパラメータかわからないと原因特定に時間がかかることがあります。なので処理のin/outを出力するようにします。
viewModel.fetchProcess(id: id)
.subscribe(onNext: { [weak self] result in
switch result {
case .success:
// 成功したときの処理
case .failure(let error):
log.debug("viewModel.fetchProcess error")
log.debug("input:id=\(id)")
log.debug("error:\(error)")
// エラーになったときの処理
}
}).disposed(by: disposeBag)
viewModel.fetchProcess(id: id)
.subscribe(onNext: { result in
// 成功したときの処理
}, onError: { error in
log.debug("viewModel.fetchProcess error")
log.debug("input:id=\(id)")
log.debug("error:\(error)")
// エラーになったときの処理
}).disposed(by: disposeBag)
inputの出力はfetchProcessの中でやってもいいですね。
とにかくエラーになった時の情報はあるに越した事ないです。
3. 意味不明(無駄)・間違ってるログは出力しない
意味不明なログは混乱を招くので注意しましょう。
よくあるのがメソッドをコピペで流用作成したのにログの出力が前のメソッドの情報を垂れ流しているとか。
func login(id: String) {
log.debug("login:start>")
// 略
log.debug("login:end>")
}
func logout(id: String) {
log.debug("login:start>") // loginかログアウトかわからない!
// 略
log.debug("login:end>") // loginかログアウトかわからない!
}
どっちやねん?!ってなります!
終わりに
FirebaseのCrashlyticsはクラッシュしたアプリのバージョン・端末・端末のOS・スタックトレース情報・ログなど調査に必要な情報が一通り揃っています。無料の割には高機能ですので積極的に活用していきたいですね。
また、ログの出力を怠っているとトラブルの原因究明に時間がかかってしまいます。
なので横着することなく適切なログを出力を心がけましょう。
初心忘るべからずですね。