ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
クラッシュとクラッシュログについて理解する
Appの突然なクラッシュは、ネガティブなユーザー体験やApp Reviewでの却下の原因となります。クラッシュログを分析する方法、クラッシュログに含まれている情報、クラッシュの原因を診断する方法を学んでいきましょう。再現しにくいメモリ破損やマルチスレッドに関する問題などについても説明します。
リソース
- iOS Debugging Magic
- Mac OS X Debugging Magic
- Thread Sanitizer
- Understanding and Analyzing Application Crash Reports
- プレゼンテーションスライド(PDF)
関連ビデオ
WWDC21
WWDC20
WWDC18
-
ダウンロード
(音楽)
どうも おはようございます ようこそ (拍手) 私の後に 優秀な仲間たちも登場します 面白い内容を用意しています
まず言わせてください クラッシュするコードを 書かない人には― このセッションは不要です これからする話は ミスをする人たち向けです
今日はユーザに影響する クラッシュを― 解決するためのツールや 技術の話をします まず私から クラッシュの基本的な話を 原因や症状をお話しします それからクラッシュログを 実際に調べるツールを紹介します
その後 グレッグから― クラッシュログの内容の 読み方を詳しく説明します そして厄介なメモリ問題の 読み方を掘り下げていきます その後 クバが スレッド競合の話を これが原因のクラッシュは 再現が困難です
まず定義しましょう クラッシュとは?
許可されていない動作を しようとして― 突然 Appが停止することです 許可されないこととは? 例えば ゼロで割るなど CPUが実行できないコードです あるいはOSのポリシーです
Appの起動が遅すぎたり メモリを使いすぎたりすると Appを停止させます
プログラミング言語が エラーを避けようとして― クラッシュする場合もあります SwiftやNSArrayは配列の範囲から 外れるとプロセスを停止します
エラーを避けようとした デベロッパが原因の場合もあります パラメータが“nil”ではないと アサートするAPIがあるとか それは悪くない
これを見たことは? これはAppに接続した Xcode内のデバッガの画像です Appが停止される直前の 状態です
左のバックトレースを 詳しく見ましょう ここでOSによって Appがスタートされています メイン関数が呼び出され― 関数が他の関数を呼び出します やがてクラッシュしか 選択肢がないところまで進みます 何か問題があったようです デバッガがクラッシュ寸前という 信号を受け― Appが停止されます
デバッガに接続されていない 場合もあるでしょう デバッガがない場合― OSがプレーンテキストで バックトレースをキャプチャします そしてクラッシュログとして ディスクに保存します
Appのリリースビルドが クラッシュした場合― ログはこんなに整ってません バイナリの名前とアドレスが 書き出されます これがシンボル化されてない クラッシュログです
Xcodeがクラッシュログを シンボル化してくれるので― 関数名やファイル名や 行番号が表示されます
クラッシュログに アクセスする方法は複数あります まずはTestFlightの ベータ版テスターや― App Storeの カスタマーを通じてです XcodeのCrashes Organizerを 使ってダウンロードできます こちらです きれいなダークモードですね UIのツアーをしましょう 左側にTestFlightとApp Storeで 配信されているAppが見えます watchOSやApp Extensionなどの プラットフォームをサポートします
右には各クラッシュポイントで 影響を受けるデバイスが示されます
そして似た問題で クラッシュログを分類し― ソースリストで デバイスごとにランク分けします そして各ログのサンプリングが 下で見られます
このボタンを押すと― デバッグナビゲータで クラッシュログが開けます 後でお見せしましょう 詳細ビューでは シンボル化されたバックトレースと クラッシュポイントが示されます では一度 見てみましょう Xcodeを開いています Organizerウインドウを開いて
Crashesタブを選択します 2番目のタブですね クバと作った “ChocolateChip”Appを選択します このビルドを TestFlightに上げました 今 ビルド5を見ています テスターから クラッシュの報告があり― いくつか対処しました 未対応のものを解決しましょう
242のデバイスに影響しています クラッシュ時のバックトレースと クラッシュポイントが見られます まだ状況は分かりません でもクラッシュログを開けば 何が起きたか分かるでしょう “Open in Project...”ボタンを クリック Appのビルド5に適合する プロジェクトを選択 これはクラッシュログを― デバッグナビゲータで 再現のように開いたものです このエラーで停止しています 妥当なエラーかを考えます 不要なクラッシュは避けたい これはIntにおける― enumのイニシャライザです enumが“0”か“1”以外なら― フェイタルエラーになります 妥当でしょう プログラマの誤用でしか クラッシュしません イニシャライザの呼び出し元を コールスタックから見てみると Table Viewの デリゲートメソッドです 指定のセクション番号での ヘッダのタイトルを求めてます セクション番号が “0”か“1”ではないのでしょう かなり状況が分かりました Appで再現して さらに詳しく見てみましょう “再生”を押します
ChocolateChipはレシピのAppです ホイップクリームのレシピで テストすると― 問題ありません “材料”と“手順”が 示されています ここがレシピのセクションです セクションは材料が“0”で ステップが“1”です 他のレシピを選ぶと― クラッシュしました 同じフェイタルエラーで止まり バックトレースも 先ほどのログとよく似ています 同じ問題だと思われます クラッシュログを削除し― デバッグセッションを見ましょう このフェイタルエラーの セクション番号は“8”とあります “0”か“1”以外だから クラッシュした どうやら私が悪かったようです numberOfSectionsという― デリゲートメソッドを クラスに実装していました それがヘッダの数を求めており 私は“材料”の数を返しています その数が“8”なのです いい解決法があります “レシピ”セクションenumの ケースの数を返せばいい Swift 4.2では― オープンソースコミュニティから 新しい機能が追加されました CaseIterableというプロトコルです “レシピ”セクションを CaseIterableに準拠させると “レシピ”セクション enumにある― ケースの数を返すことができます そうすれば― 正確なセクションの数を返せます これでうまくいきます
ではレシピを確認してみましょう クラッシュせず 材料と手順が示されます よかった これで私も満足です Organizerに戻って この問題を解決済みとし― クッキーを焼く作業に戻れます
(拍手) Crashes Organizerを使って TestFlightからログをダウンロード ソースコードを開き 解決する方法でした
次に進めるには何が必要か? とてもシンプルです カスタマーがサードパーティとの シェアにオプトインすれば ログが自動的に アップロードされます
Apple IDで Xcodeに サインインすればいいのです
アップロードでは― ログがシンボル化されるように シンボルも含むようにしましょう
あとはOrganizerで Crashesタブを開き対処します
以上がOrganizerで クラッシュを見る方法です TestFlightや App Storeで配信しない場合 他の選択肢があります Devicesウインドウです デバイスをつないでいれば “View Device Logs”ボタンから デバイスのログが見られます Macの情報を使って これらのログがシンボル化されます
XcodeやXcode Serverや ビルドでのテストでは テスト中に書き出されたAppの クラッシュログの全結果が出ます これらのログも シンボル化されています
コンソールAppを使えば Macやシミュレータから― クラッシュログを見られます デバイスでは“設定”から― “プライバシー”“解析” “解析データ”と行けば― すべてのログを見られます ユーザもログを共有できます
シンボル化を 確実に機能させるための― ベストプラクティスが3つあります 1つ目 Crashes Organizerを 使う場合― Appでシンボルをアップロードする これがデフォルトです サーバ側で シンボル化を保証するためです
2つ目 Appのアーカイブを保存する アーカイブにデバッグシンボルの コピーが含まれます dSYMです XcodeはSpotlightで自動的に dSYMを見つけシンボル化します
BitcodeのあるAppを アップロードするなら― Archives Organizerの― Download Debug Symbolsボタンを 使いましょう ストア側の dSYMをダウンロードできます
以上― 実際にクラッシュログを 調べるツールを紹介しました 続いて より深く クラッシュログを読むガイドです グレッグ・パーカーを 拍手でお迎えください (拍手) ありがとう クリス ここまでは Xcodeでクラッシュを見つけて デバッガで調べる方法を 紹介しました でもクラッシュログには― スタックトレース以外に 多くの情報が含まれています 問題を解決する上で その情報が役立つことも多いのです ではログの全文をどう得るか? これはXcode Organizerです コンテキストメニューに “Show in Finder”があります テキストファイルが出るので― コンソールAppか 好みのテキストエディタで開けます こんな感じです では中身を見てみましょう トップに概要があります Appの名前 バージョン 使われていたOSバージョン クラッシュの日時などです その下に クラッシュの理由があります OSがプロセスを停止するため 送った具体的なシグナルです
それからログ情報もあります App固有の情報セクションです ここにはコンソールログや― 未処理の例外の バックトレースが含まれます 必ずあるものではなく― iOSデバイスではプライバシーの 都合上 隠されることが多い でもシミュレータでは ここに役立つ情報も含まれます
その下はスレッドスタックです クラッシュ時に動いていた スレッドのバックトレースです 1つがクラッシュスレッドで― その他に動いていたスレッドも 表示されます
その下は低レベルの情報です クラッシュしたスレッドの レジスタ状態と― ロードされた バイナリイメージがあります これは実行可能なAppと 他のライブラリです Xcodeは これをシンボル化に使います シンボルやファイルや 行番号の情報を得るためです
これがクラッシュログ ファイルの中身です では どうデバッグするか? 読み方は? まずはクラッシュの理由から 例外タイプです この場合は EXC BAD INSTRUCTIONですね SIGILLは不正命令のシグナルです つまりCPUが 存在しないか 無効な命令を実行しようとして プロセスが止まったわけです
クラッシュしたスレッドも 見られます クラッシュ時のコードは何か? Swiftランタイムに fatalErrorMessage関数があります fatalErrorMessage関数が 何をするかは分かりません この場合 エラーメッセージは App固有情報に含まれています プロセスが終了する際に 何が表示されたかが分かります
スタックトレースを 詳しく見てみましょう fatalErrorMessage関数は― コード内の関数に呼び出されました “レシピ”というクラスの image関数が呼び出されます その関数のエラーでフェイタル エラーメッセージが呼び出された
これはシンボル化されて 全デバッグ情報があるため― コード内にクラッシュの ファイルと行番号があります その行を見てみましょう プロジェクトを開きます これは RecipeImage.swift 26行目がクラッシュ時に マークされた行です Swiftに慣れた方なら― この行が クラッシュの原因だと 見当がつくでしょう 強制アンラップ演算子があります それから関数 UIImageコンストラクタがあります オプション値を返します オプション値が“nil”なら― 強制アンラップ演算子が プロセスを停止し クラッシュログを生成して 終了させます
そしてApp固有の情報には― Swiftランタイムが表示した エラーメッセージが含まれています オプション値がアンラップ中 “nil”となったとあります いいことです コードと一致しているからです 26行目に強制アンラップ演算子 クラッシュログに― オプション値をアンラップしていた というエラーメッセージがある クラッシュの原因として つじつまが合っています
強制アンラップのエラーは コード内の前提条件 または― アサーションの例です 前提条件やアサーションは― エラーが起きた際 プロセスを 止めるエラーチェックです
今見た オプション値の 強制アンラップがその例です Swiftランタイムが オプションは “nil”ではないとアサートし― “nil”ならクラッシュします Swift.Arrayの範囲外アクセスも 例の1つです 配列にアクセスして インデックスが範囲を超えると Swiftランタイムは 前提条件を満たせず停止します Swiftの算術オーバーフローも アサーションを含みます 2つの数を足す時 結果が 整数型変数として大きすぎると 前提条件からプロセスを停止します
捕捉されない例外も 前提条件により よく発生します 多くのエラーチェックで 満たされない前提条件が例外を投げ それが捕捉されない場合 クラッシュログを発生させます 自分のコードに 前提条件やアサーションを書けば エラーが起きた際 クラッシュログを生成させられます
また別の例として― OSが外から プロセスを停止する場合があります
タイムアウトなどの 監視イベントがその例です Appが何かをするのに時間が かかりすぎると OSが検知し プロセスを停止し クラッシュログを生成します
環境条件も OSがプロセスを 停止する原因になります デバイスが過熱状態になると CPUを 使いすぎるプロセスを停止します メモリが不足してくると メモリを 多く使うプロセスを停止します 無効なコード署名も一例です OSはコードに署名を求めます 署名が無効であるか存在しない場合 プロセスを停止して クラッシュログを生成します
OSによる停止は Xcodeの Devicesウインドウで確認できます macOSのコンソールにも出ます Xcode Organizerには 必ずしも表示されないので ご注意ください
Appleの デベロッパドキュメントでは― ログの様々なシグネチャや構造が 技術注記に書かれています これらの例のように その見た目や見分け方など より詳しい情報があります
例を1つ見ましょう クラッシュログのファイルです 読み解くには クラッシュの理由から見ていきます この場合は 例外タイプが “EXC CRASH (SIGKILL)”です SIGKILLシグナルが よく使われるのは― OSがプロセスを止める時です OSがSIGKILLシグナルを送り― そのシグナルが プロセスによって捕捉されないため プロセスが停止するわけです
OSがシグナルを送った理由も ログで見られます “8badf00d”のコードと 停止の理由が書かれています 先ほど言った技術注記に― “8badf00d”の意味があります そしてテキストで― 19.95秒の猶予時間を 使い切ったと書かれています この情報と 技術注記を合わせて見れば 起動に時間が かかりすぎたと分かります 20秒の間に起動できず― OSがプロセスを停止した
下に停止時の クラッシュログが出ています これらのコードが 長くかかった原因かもしれない 無限ループになったか― それとも ネットワークのI/Oが長かったか あるいは このコードは無実で― もっと前のプロセスが遅いために 停止した可能性もあります
起動のタイムアウトを どう防ぐか? ぜひ防いでください AppleのApp Reviewで よく見られる却下の理由です
どう防ぐか? Appをテストします ただ問題があります タイムアウトの監視は― シミュレータやデバッガでは 動作しません つまりシミュレータやデバッガで テストすればタイムアウトはない だからデバッガなしで Appをテストしてください macOSのAppなら Finderで起動してください iOSなら TestFlightで実行するか― iOS Appランチャーで 起動してください デバッガ外で起動され タイムアウトが有効になります
テストは 本物のデバイスで行いましょう より古いハードで試しましょう サポートしたい 最も古いハードでテストします 新しいハードなら十分早く 起動できても 古いものは― 時間がかかるかもしれません
別のエラークラスの話をしましょう クラッシュログに メモリエラーがどう現れるのか? メモリエラーとは例えば 二重解放されたオブジェクトの 参照カウントをした場合や 解放後のオブジェクトを使った場合 バッファのオーバーフローとか バイト配列や― C配列に対して 範囲外の アクセスをした場合などです
クラッシュログを見ましょう これはメモリエラーのものです また例外タイプから始めます “EXC BAD ACCESS (SIGSEGV)”です メモリエラーによくある形です EXC BAD ACCESSの意味は2つ 読み取り専用のメモリに 書き込んだか 存在しないメモリを 読み込んだかです この2つの場合 EXC BAD ACCESSで停止します これはクラッシュ時に アクセスしていたアドレスです
スタックトレースも見ましょう この関数が アクセスエラーを起こしています これはobjc release関数で― Objective-CとSwiftオブジェクトの 参照カウントの実行の一部です これもやはり メモリエラーを匂わせます
では objc releaseは何が原因か? スタックトレースを見ましょう オブジェクトの破棄関数があります Objective-Cランタイムの関数で オブジェクトを解放するものです 破棄関数が LoginViewControllerクラスで ivar destroyerという関数を 呼び出しました ivar destroyer関数は Swiftコードの一部です オブジェクトのプロパティや ivarストレージを― 解放時に処分します ここから クラッシュの 原因の一部が見えてきます LoginViewControllerクラスの オブジェクトを解放していました そのdeinitコードのクラスが プロパティとivarを処分しようとし いずれかが解放される間に クラッシュしています
問題の詳細が少し見えました クラッシュログに もっと状況が分かる情報はないか?
無効なアドレスを見ましょう 問題のあるアドレス値が 役に立つ情報を含む場合があります この問題のあるアドレスは 解放後の使用に見えます 主に経験から そうと分かります クラッシュログを読んでると 少しずつパターンが分かってきます このアドレス値は― mallocメモリアロケータの アドレス範囲とよく似ています クラッシュログにもありました これがそのアドレス範囲です 無効なアドレスは mallocの範囲内のようですが 4ビット移っています 4ビット回転しています 有効なmallocアドレスが 回転したと思われます
以上が メモリアロケータからの ヒントです 理由を見せましょう こちらは 有効だったオブジェクトです isaフィールドから始まり isaがクラスを指しています Obcjective-Cや Swiftのオブジェクトも同じです objc release関数は何をするか? isaフィールドを読み― isaフィールドを間接参照し クラスオブジェクトを獲得します 通常なら これでうまくいきます でもオブジェクトが解放済みなら?
free関数が 削除したオブジェクトは―
他の削除済みオブジェクトの リストに入り― 次のオブジェクトに向け free listポインタを書き込みます isaフィールドがあった場所にです ただし 通常のポインタではなく― 回転したポインタを書き込みます 書かれた値を 無効な メモリアドレスにするためです 誤用するとクラッシュするように
objc releaseが isaフィールドを読むと 回転したfree listポインタを獲得 それを間接参照すると クラッシュします メモリアロケータが ポインタを回転することで 使えなくしてくれたわけです
それがクラッシュログで 見えるシグネチャです 無効なアドレスは mallocのポインタに似てますが free listポインタと 同じように回転しています つまり ここで起きたのは 解放しようとしたオブジェクトは すでに解放されていたという― メモリエラーでした
以上が詳しく見た内容です オブジェクトが解放され ivarを処分しようとしたら― ivarの1つが解放済みでした そしてクラッシュ さらに何か? 解放しようとしていた オブジェクトは分かるのか? 通常ならobjc releaseを 呼び出した関数がヒントになります ただivar destroyer関数は― コンパイラが生成した関数です 我々が書いたものではない つまりクラッシュ関連の ファイル名や行番号がなく― 当時 どのプロパティが 解放されてたか分かりません これはクラスです プロパティが3つあります ユーザ名と データベースとビューの配列です 現時点では どれが 解放されていたか分かりません どれもあり得ます まだ何か? クラッシュログの情報から― 解放されていた オブジェクトは分かるのか? デバッガで再現できないなら ログだけが頼りです この場合 まだ分かります ファイル名と行番号の箇所に “+ 42”と書かれています この“+ 42”が手がかりです “+ 42”はその関数の アセンブリコードのオフセットです ivar destroyer関数を 逆アセンブルし コードを見れば オフセット“42”でアクセスされた プロパティが分かります どうやるか? デバッガコンソールを使います ターミナルでLLDBを実行します Xcodeのデバッグターミナルで LLDBを実行します
デバッガにはクラッシュログを インポートするコマンドがあります デバッガの中で クラッシュしたように見せます まずこのコマンドでクラッシュログ 翻訳コマンドをロードし 別のコマンドで クラッシュをインポートします 必要なものは3つです まずクラッシュログのコピー それからAppのコピー それとdSYMファイルのコピーです クラッシュログと 同じバージョンのものが必要です だからAppのアーカイブを 保管しておいてほしいんです これらのファイルが Macに用意できたら実行します LLDBは Spotlightを使って― 実行ファイルやシンボルを見つけ ロードします クラッシュスレッドの スタックトレースや― ファイルや行番号の情報があります これで準備できました ivar destroyer関数のアドレスを 見つけ 逆アセンブルしましょう これは関数のアセンブリコードです
アセンブリコードの読み方ですが 幸い― アセンブリコードを完璧に 読めなくても問題ありません ざっと読んで おおまかな 流れをつかめれば十分です クラッシュログの作業では 全部 理解する必要はありません
この関数を見ると 呼び出し命令と ジャンプ命令は分かりますね 関数を呼び出すものです このコードは 3ブロックに分けられます 一番上のセクションは― 参照カウント解放関数を 関数に呼び出させます これはユーザ名の プロパティを解放しています
次の領域は データベースのプロパティを解放 次は ビューのプロパティを 解放しています 各命令の意味は分かりませんが 各領域の おおまかな働きは分かります コードに関連する行番号があるのと 少し似ていますね
クラッシュログの情報に戻ります ivar destroyer関数 + 42が objc releaseを呼び出している
だから“+ 42”に命令があります ただ注意点がもう1つ スタックトレースの中では― 大半のスタックフレームの オフセットがリターンアドレスです 関数呼び出し後の命令です 呼び出されたobjc releaseの命令は 1つ前の命令 この命令です 読むと objc releaseなので 問題ありません スタックトレースで見たものと つじつまが合う このオフセットでのobjc releaseの 呼び出しでした そして この解放関数は データベースの― プロパティを解放しています クラッシュの詳細が見えてきました ユーザ名のプロパティの解放は 成功しました まだビューのプロパティには 行ってません 無効か有効かは分かりません 分かったのは データベースの プロパティを解放しようとしたが そのオブジェクトが解放済みの オブジェクトだったことです これでかなり分かってきました LoginViewController オブジェクトを解放していたら データベースのプロパティが 無効でした
まだバグは見つかっていません コードは正しく ivar destroyer関数も 間違っていません 何か他のものがおかしい でもクラッシュログから 絞り込めました 何をテストし― どこでバグを再現すべきか? このクラスを見るべきです データベースフィールドを見て データベースオブジェクトを 使うコードからバグを見つけます
ここまで何をしたか? クラッシュログを読みました まずクラッシュの理由です 例外タイプを読み その意味を理解しました 次にクラッシュしたスレッドの スタックトレースを調べ 何をしていて 何のエラーで 停止したかを理解しました 他の手がかりも探しました 今回はメモリエラーの 問題のあるアドレスです そしてクラッシュした関数の 逆アセンブルをしました
メモリエラーには 様々なクラッシュがあります メモリエラーが原因となる ログのシグネチャは多様です いくつか例があります objc msgSend関数のクラッシュ SwiftやObjective-Cの― 参照カウント装置や 解放装置のクラッシュ これらの多くは メモリエラーが原因となります
もう1つ よくあるエラー症状は 認識されないセレクタ例外です 状況としては 何かのオブジェクトがあり コードがそれを使います 解放された後 また使用されます しかし malloc空きページリスト シグネチャを得ずに― 新しいオブジェクトが 古いものと 同じアドレスに割り当てられました コードが前のオブジェクトを 使おうとしても― 同じアドレスに別のタイプの オブジェクトがあります そのため関数が認識できず 認識されないセレクタ例外に
もう1つ よくあるエラー症状は― メモリアロケータ内での停止です malloc/free関数内です これも前提条件の例の1つで― メモリアロケータ内の前提条件です 代表的なケースとしては mallocメモリ自体の ヒープデータ構造が― メモリエラーで壊れ プロセスや反応が停止する場合です あるいはmalloc APIの 間違った使い方を探知した場合 例えば オブジェクトを 2回連続で解放すると― アロケータが二重解放として認識し 停止する場合があります
最後にクラッシュログや メモリエラーの分析に関して― いくつかコツをお教えします
ここまでの話では― クラッシュした具体的なコードや スレッドに注目してきました でもクラッシュに関係している― 別のコードを見るのも大事です 例えば このクラッシュでは― ivar destroyer関数は悪くない バグの場所ではありません バグは別にあり どこか他のコードが間違っています
クラッシュしたスレッド以外の スタックトレースも見るべきです クラッシュログには 全スタックトレースが含まれ― その中には役立つ情報や 手がかりがあるかもしれません 他のスレッドにはAppのどこで 実行されていたかの詳細があるかも ネットワークコードを実行していて 別のスレッドで分かるかもしれない あるいは マルチスレッドエラーがあり 何のスレッド競合だったか 他の スレッドから分かるかもしれません
それから1つの原因につき ログは複数 見るべきです Xcode Organizerは― クラッシュの箇所により グループ分けしてくれます 同じ箇所で複数のクラッシュが 起きることもありますが ログにより情報量も変わります 先ほどのmalloc空きページリスト シグネチャは― ログによって 見られないものもあります だから1つのクラッシュでも 複数のログを見て― 役立つ情報を探すといいでしょう Organizerは 異なる原因のクラッシュでも― 場合によっては 同じグループに分けたりします クラッシュしたスレッドや バックトレースを見れば― 原因の異なるクラッシュが 複数あると分かることもあります 同じグループにあってもです ログを1つ見ただけだと 2つ目のクラッシュを見逃し― そのまま気づかずに 出荷してしまうかもしれません
クラッシュを分析し― 起きた場所や使ったオブジェクトを ある程度 絞り込むことができたら Address SanitizerやZombiesを 使ってクラッシュを再現してもいい クラッシュログから何が起きたか 絞り込むことができたとしても エラーメッセージを伝えてくれる デバッガやテスト内でのほうが デバッグするのは はるかに楽です
マルチスレッドエラーの 診断には― 複数のスタックトレースや スレッドを見るよう言いました それについて詳しい話を クバにしてもらいましょう (拍手) ありがとう
どうも グレッグが言ったように― メモリ破損はマルチスレッドから 生じる場合もあります マルチスレッドのバグは 診断と再現が非常に難しいのです たまにしか起きないからです コードが99%は うまくいっていたりするので 長く気づかれないこともあります
マルチスレッドバグは メモリ破損を招きがちで クラッシュログも メモリ破損のように見えます 先ほど 例を紹介しました mallocやfree リテインカウント内のクラッシュは メモリ破損の典型的症状です マルチスレッドバグの 特徴的な症状もあります よくクラッシュログに― 関連するコードを実行する 複数のスレッドが含まれます 特定のクラスやメソッドが 複数のスレッドのログにあれば― マルチスレッドバグの可能性がある マルチスレッドバグが原因の メモリ破損の多くはランダムです だから よく似たコードや アドレスでクラッシュが見られます そして同じバグでも Xcodeではクラッシュポイントが 別だと判断されることもあります クラッシュしたスレッドが 原因ではない場合もあります だからログで 他のスレッドも見ることが重要です ではバグの例を見てみましょう Xcodeに含まれるツール Thread Sanitizerで 診断する方法を紹介します
クッキーレシピのAppで― ユーザから入手した クラッシュログを見てみましょう この2つ目のクラッシュログに 注目しましょう
これを見ると LazyImageView クラスに何か問題があるようです 私が書いたクラスです この後 お見せします その前にログを読みましょう
スレッドのスタック全体を見ます このボタンを押すと― 他のスレッドが表示されます 上のほうを見ると― free関数が停止を 呼び出していることが分かります ヒープの破損だという示唆です
他のスレッドも見ましょう スレッド5を見ると LazyImageView内で やはりコードを実行しています
別のクラッシュも見ましょう
どのログでも共通しています あるスレッドが free関数の停止でヒープ破損し 別のスレッドが コードの似た箇所で処理しています やはりLazyImageView内です 偶然ではないでしょう マルチスレッドバグだと思われます
LazyImageViewクラスを見ましょう ボタンを押してコードに飛び プロジェクトで開きます LazyImageViewのソースが出ます UIImageViewのサブクラスです イメージを遅延させ 非同期的にロードする機能がある イニシャライザに ロジックがあります バックグラウンドのキューに ジョブを割り当てて― バックグラウンドスレッドに 画像を作成します 終わればメインキューに戻し スクリーンに画像を表示します
クラッシュログは このコードを示しています 画像キャッシュに アクセスしている箇所です 不必要に同じ画像を何度も 作成しないようにするためです ここにバグがあるのかもしれません 確認しましょう シミュレータでAppを実行し クラッシュを再現しましょう クラッシュログを閉じます
これがクッキーレシピのAppです 新しいレシピを加えるため このプラスボタンを押すと― 新しいレシピの画像を選ぶよう 言われます 今 スクリーン上にある コントローラは― LazyImageViewで 画像を表示しています こうしてスクロールすることで― コードは実行されているはずです でもクラッシュは起きない 残念ながら マルチスレッドバグは― 再現が難しいんです 何度もバグのあるコードを テストしても― クラッシュしないことも では何度かコントローラを 閉じたり開いたりしましょう そのうち運よく クラッシュが起きるかもしれません
起きましたね クラッシュしたので終了しました でもデバッガで再現できても あまり意味がありません EXC BAD ACCESSだと 分かるだけです 一体 原因は何でしょう なぜ起きたか教えてくれません でもXcodeに うってつけのツールがあります Thread Sanitizerです これを使います スキームエディタを開きましょう ここで このAppを選び “Edit Scheme...”をクリック Diagnosticsタブに切り替えると いくつか診断ツールがあります Address Sanitizerはバッファ オーバーフローの発見に便利です Thread Sanitizerを選択し “Pause on issues”も選択します バグを検出すると デバッガが止まるということです
Thread Sanitizerを有効にして― もう一度 Appを操作しましょう
プラスボタンを押すと… Appがすぐに止まりました バグ発見です しかも1回で済みました Thread Sanitizerは 高確率で再現してくれます バグの詳細を見ましょう Swift Access Raceだと分かります 左のデバッグナビゲータを見ると さらに詳細が分かります 2つのスレッドが 2つのアクセスをしています スレッド2と4が― 同じメモリに同時に アクセスしようとしていますが 許可されていません
競合している この2つのコードを見ると― 画像キャッシュに アクセスしています このデータは複数のスレッドで 共有されたデータ構造なので― スレッドデータ構造で なくてはなりません どう実行されるか見てましょう ストレージにジャンプして スレッドセーフか確認します 画像キャッシュのソースが ファイルの一番上にあります すぐ問題が分かりますね これは単なる Swiftの辞書なのでダメです Swiftの辞書は スレッドセーフじゃありません 複数のスレッドで Swiftの辞書を共有するには― 同期により守る必要があります 複数のスレッドが同時に アクセスしないようにします ではクラスを スレッドセーフにしましょう ステップは2つ まずコードを少し リファクタリングし― ストレージを制御します 次にディスパッチキューで クラスをスレッドセーフにします
まず 問題はストレージが パブリック変数であること どのコードも アクセス可能だということです コードを完璧にするのは かなり大変なので― プライベートに変更します 画像キャッシュへのアクセスも 変えましょう それにはsubscriptを使います ブラケットを使い キャッシュ からのデータをロードできます subscriptにはゲッタが必要です セッタも必要です とりあえず 下層ストレージに 直接アクセスしましょう
残りのファイルを作るには ユーザをアップデートします ストレージプロパティに アクセスせず― ブラケットやインデックスを 画像キャッシュに直接 使います ビルドを押せば コードは正常にコンパイルします まだバグは直してませんが 進展はありました ストレージにアクセスするコードに 制限を設けられました ゲッタかセッタのどちらかです 他のコードはアクセスできない これでSwift Access Raceの修正に 近づきました ディスパッチキューで直します “queue”という プライベート変数を作り ディスパッチキューを割り当てます ディスパッチキューはシリアルです これも同様 つまり このqueueの中で一度に 1つのコードしか実行できません これが必要なことです ディスパッチキュー内で コードをどう実行するか? queue.syncを使います queue.syncに移動したコードは―
1つずつqueue内で実行されます ゲッタから何か返す必要があるので 値を返します セッタでも同様です
コードをqueue.syncに移すと ディスパッチキューの一部として 実行されます これでスレッドセーフです ストレージに アクセスするコードは常に― シリアルディスパッチキュー内で 実行されます 一度に1つずつ実行されるので スレッドセーフです セッタのみに 同期を 使いたくなるかもしれません ストレージを修正し― こうしてゲッタでは避けます でもダメです これでもメモリ破損は起きます このバージョンを シミュレータで試してみましょう Sanitizerは バグを発見できるでしょうか
やはりできますね ゲッタとセッタの両方を 同期で守らなくてはなりません 最後にもう一度 Appを実行します レシピを追加してみましょう コントローラは正常にロード クラスはスレッドセーフなので 警告も出ません Organizerウインドウに戻って 解決したとマークしましょう このバグを発見し修正できました
(拍手)
今 お見せしたように マルチスレッドバグの症状が クラッシュログに見られました そしてThread Sanitizerで バグを発見し修正しました Thread Sanitizerは― マルチスレッドバグを 高確率で再現できます 何度も操作を繰り返す必要は ありませんでした macOSとシミュレータで動作します ただ他の診断ツール同様― コードを発動させないと バグを見つけられません 覚えておいてください テストでは Thread Sanitizerを使いましょう スレッドやGCDを使うコードは 特にです さらなる情報は― WWDC 2016での 私のセッションの動画をどうぞ “Thread Sanitizer and Static Analysis”です このツールを紹介し その効果を説明しました 復習すると スキームエディタのツールです “Product”から“Scheme”を選び “Edit Scheme...”で 立ち上がります Diagnosticsタブを開くと 診断ツールの中に Thread Sanitizerが並んでいます
もう1つデバッグのコツを マルチスレッドバグに有効です ディスパッチキューを作成する際 イニシャライザでラベルを使えます オペレーションキューに カスタム名をアサインでき スレッドにも カスタム名を使うことができます 名前とラベルはデバッガに また一部のログにも表示されます マルチスレッドバグの原因を 絞り込むのに役立ちます
クラッシュ対策で 覚えておくべきコツを3つだけ 1つ目 App Storeにアップロードする前に 実際のデバイスでテストすること App Reviewで 却下されにくくなります 2つ目 ユーザから報告されたクラッシュは 再現すること Appのどの部分を発動させれば そのクラッシュが再現できるか クラッシュログを見て 考えるのです 最後に― 再現が難しいクラッシュには ツールを使う Address Sanitizerや Thread Sanitizerなどです メモリ破損や マルチスレッドバグに役立ちます
今日のおさらいをしましょう クリスからは Xcodeの Organizerウインドウを使って クラッシュログを 見る方法を学びました グレッグからは ログの読み方と分析を学びました 多くの場合 再現が可能です Appの起動タイムアウトなど
それからメモリ破損など 再現が難しいクラッシュと― そのログの症状について話しました 最後に Sanitizerなどを活用した― メモリ破損やスレッドのバグの 再現法を紹介しました ぜひ使うことをお勧めします さらに詳しい情報は セッションのページへ 技術注記へのリンクや― クラッシュの際 デバッグに 役立つドキュメントもあります この後 12時から Technology Lab 8で― ラボがあります 質問がある方は ぜひどうぞ 残りのWWDCをお楽しみください どうも (拍手)
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。