どうも、フリーランスのITエンジニア兼ブロガー兼投資家のKerubitoです。
iOS(iPadOS)アプリを開発し、本格的に運用しはじめたら必ずクラッシュという問題に直面します。
クラッシュしないアプリであるのが望ましいのですが、100%クラッシュしないアプリはAppleでも無理。
そして、複雑なアプリになればなるほど、クラッシュの要因は多岐に渡ります。
重要なのはクラッシュしたら、その原因をつきとめ、対処することですよね。
開発中で簡単に再現すればいいのですが、ローンチ後はそうもいきません。
不特定多数のユーザーがアプリを使用し、いつどこでクラッシュしているかわからないためです。
iTunesConnetにクラッシュレポートを届けるという手段もありますが、自分の経験上、これはあまりあてにならないんですよね。
そこで完全にクラッシュ情報を捕捉する手段について考え、対策をしました。
KSCrashを利用する
クラッシュ情報を捕捉できるサービスとかありそうなんだけどな〜とネットで調べていたらRollbarというサービスを見つけました。
これはめちゃめちゃ簡単でSwiftにも対応しているとのことでしたので、さっそく検証。
・・・しかし、自分でやった限りではきちんと動きませんでした。
偶然、別のエンジニアさんも使ったことがあったようで、聞いたらやっぱりまともに動かないとのこと。
がっかりしたのですが、このRollbarがKSCrashというライブラリを使っているではありませんか。
KSCrashはGitHubに公開されているので、誰でも簡単に利用できます。
さっそく組み込んでみると、クラッシュ情報取れました!
以下のようなクラッシュ時にちゃんとレポートを作成してくれます。
・nilに対する強制アンラップ
・データベースエラー(一意制約違反など)
・storyboardとコードの紐付け忘れなど
・SIGKILL
KSCrashの仕組みですが、クラッシュが発生した次回起動時にレポートが作成されます。
利用側はただそれを取得するだけです。
KSCrashを使ってみる
では実際にKSCrashを使って、クラッシュ情報を取得します。
手元の動作環境は以下です。
・macOS Big Sur 11.2
・Xcode Version 12.4
・CocoaPods 1.10.1
KSCrashを導入するにはライブラリパッケージを使います。
私はCocoaPodsを使っていますが、Swift Package Managerでも構いません。
CocoaPodsについては以下の記事に書いてあります。
SwiftでCocoaPodsを使う手順メモとios-chartsでグラフ表示
Swift でiphoneやiPad向けのアプリ開発をする際に、もはや必須となりつつあるライブラリ。 そのライブラリを便利に使うためのCocoaPodsの導入手順です。 CocoaPodsって何?って ...
続きを見る
KSCrashの利用は非常に簡単。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import Foundation import KSCrash internal class CrashReport { internal static func fetch() { guard let kscrash = KSCrashInstallationConsole.sharedInstance() else { return } kscrash.install() kscrash.sendAllReports { reports, completed, error in guard let reports = reports, reports.count > 0 else { return } if let error = error { print("\(error)") return } let jsons: [Any] = reports.compactMap { report in guard let data = "\(report)".data(using: .utf8), let json = try? JSONSerialization.jsonObject(with: data) else { return nil } return json } guard let filter = KSCrashReportFilterAppleFmt.filter(with: KSAppleReportStyleSymbolicatedSideBySide) else { return } filter.filterReports(jsons, onCompletion: { reports, completed, error in if let error = error { print("\(error)") return } if completed, let reports = reports { reports.forEach { report in print("\(report)") } } }) } } } |
これだけです。
KSCrashInstallationConsole.sharedInstance()でインスタンスを取得し、kscrash.sendAllReportsでクラッシュレポートを取得します。
reportsにクラッシュした情報が入っていますので、ここではそのまま出力しています。
と言いたいところですが、このままだと本番環境ではうまくSymbolicateできません。
ですので、Symbolicateできるようにレポートの形式を変えてやります。
そのためにはKSCrashから受け取ったレポートを一度バイナリにして、さらにjson形式にしてやる必要があります。
さらにjson形式からAppleのクラッシュレポートの形式に変換します。
KSCrashReportFilterAppleFmt.filter(with: KSAppleReportStyleSymbolicatedSideBySide)でフォーマットを指定しています。
もう少し楽な方法があるのかもしれませんが・・・。
それでは試しにViewControllerのviewDidLoadにてCrashReport.fetch()を呼び出し、クラッシュレポートを取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import UIKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. CrashReport.fetch() } @IBAction func crash() { let a: Int? = nil let b: Int = 1 + a! } } |
サンプルアプリでは画面の真ん中にボタンを置いて、タップすると落ちるようにしています。
以下はクラッシュレポートの一部抜粋です。
"crashed": true,
"current_thread": false,
"index": 0,
"notable_addresses": {
"stack@0x16b14d150": {
"address": 4375332422433813336,
"type": "string",
"value": "Unexpectedly found nil while unwrapping an Optional value"
},
原因が特定できます。
{
"cpu_subtype": 0,
"cpu_type": 16347772328,
"crash_info_message": "Fatal error: Unexpectedly found nil while unwrapping an
Optional value: file CrashReport/ViewController.swift, line 20\n",
"image_addr": 68244880056432,
"image_size": 38666898824,
"image_vmaddr": 6507776664064,
"major_version": 1200,
"minor_version": 2,
"name": "/usr/lib/swift/libswiftCore.dylib",
"revision_version": 41,
"uuid": "B2CFD4E7A1-BA9DB-309-9DBA-8CB0367D8C87XF"
}
場所の特定もバッチリです。
KSKrashはこちら。
https://github.com/kstenerud/KSCrash
KSKrashを使ったサンプルはこちら。
https://github.com/Kerubito0/CrashReport
以上、楽しい開発ライフを!