Oredev ricau cover

Crash Fast: Androidのクラッシュに対するSquareのアプローチ

Square認証機能を有するAndroidアプリでは、現在アプリがクラッシュする状況が確認されています。これを解消するには、防御的なコーディング、情報収集、計測の影響、アーキテクチャーの改善といった手順を踏んだアプローチが必要となってきます。 Pierre-Yves Ricau氏は、主催したイベントØredev Conference 2015で、実際のクラッシュの例をもちいて、クラッシュ率を下げるための具体的な手順を哲学レベルの話からSquareの現場レベルでの実用的なツールの話まで説明しています。


イントロダクション (0:00)

一般的な話として、アプリが定期的にクラッシュしたり、画面のフリーズやエラーが表示されるアプリというものは、ユーザーは削除する傾向にあります(出典)。つまり、そういったアプリには人々はとてもネガティブな印象をもつようです 😤 また、一方で、開発する側はそういったバグの修正より新規開発に注力する傾向があります。 たとえば、Twitter社は、’favorites’機能の置き換えとして’like’“❤”️機能の実装をアナウンスしたとき、世の中の主な反応は「なぜ、そんな機能の開発に注力をしているんだ?まず、クラッシュするバグを修正するのが先決なのでは?」というものでした。 つまり、エンジニアとして求められている責任は、バグ修正を通じてユーザー体験を損なわせないということにあるのです。

クラッシュとは? (3:26)

どのAndroidアプリもLinuxシステムのプロセスで実行されています。クラッシュとはプロセスの強制終了を意味しており、最後にはリスタートされます。そして、このクラッシュには、Java上でのクラッシュとネイティブクラッシュの2パターンが存在します。

Java (3:58)

Javaでは、Threadと呼ばれるクラスがあります。何かの処理の実行中に例外が投げられた場合、例外がキャッチされる場合とそうでない場合があります。例外がキャッチされない場合、どんどんメソッドの呼び出し元を遡っていき最終的にはThreadrun()メソッドに到達します。もしそこでもキャッチされない場合には、UncaughtExceptionHandlerスレッドに委譲されます。すべてのスレッドは1つだけUncaughtExceptionHandlerを持つことができます。これは開発者が実装するInterfaceで、これを実装したオブジェクトをスレッドにセットしておくことで”スレッド上で例外をキャッチした。このスレッドは今死んでいる“ことを把握できるようになります。もしこのUncaughtExceptionHandlerがセットされていなければ、そしてほとんどの場合をセットされていませんが、その場合、ThreadクラスのstaticフィールドであるdefaultUncaughtExceptionHandlerに処理が委譲されます。この点についての例はビデオで確認してください。

記事の更新情報を受け取る

Androidの実装には、RuntimeInitと呼ばれるクラスが存在します。AndroidはJavaでプログラミングされているので、プログラム実行の起点となるpublic static void mainを持っています。このメソッドでは、最初にデフォルトの例外ハンドラーを設定します。この例外ハンドラーは、例外を受け取るとまずlogCrashToLogcat();を実行します。次に、おなじみの「Unfortunately XX has crashed」displayErrorDialog() でエラーダイアログを表示します。 このダイアログでOKボタンをクリックするとプロセスをkillします。さらに、このエラーダイアログが表示されたとき、一部のユーザーはレポートボタンをクリックします。他の選択肢としては開発者自身がUncaughtExceptionHandlerを実装するということが挙げられます。たとえば受け取った例外をシリアライズした上でクラウドに送り、最後に処理をデフォルトのUncaughtExceptionHandlerに委譲するというような実装が考えられます。もし、これらを実装したくないというのであれば、以下のようなライブラリを使うことで同様の処理を実現することができます。

  • CrashlyticsはTwitter社に買収された会社です。Crashlyticsはもっとも簡単に実装できるツールです。始めるにあたってはとても簡単なんですが、クラッシュを報告するためのAPIが必要になったときや、特定の方法(クラッシュ前にデバイスの状態をサーバーに送ったりなどの前処理を入れたりすること)でクラッシュを消費したいときでは使い勝手がよいとまでは言い切れません。
  • ACRC(クライアントライブラリ)これは、自らのバックエンドを実装する必要があります。
  • Bugsnag: UIが良いオープンソースのクライアントAPI

ネイティブクラッシュ (9:07)

時々ですが、ネイティブクラッシュが発生する場合があります。これは対応の余地がありません。

ネイティブクラッシュが発生したとき、それをシグナルハンドラ(C言語に組み込まれている関数)が捕捉します。Google Breakpadと呼ばれるシグナルハンドラでは、minidumpを取得してくれます。これを利用することで、文字列データを構築したうえでダミーの例外をつくりることができます。また、独自のスタックトレースをその例外にしておくこともできます。この例外をBugsnagやCrashlytics、さらにはACRAを利用してアップロードするというような使い方をすることができます。取得したminidumpを解析してして何が起こったのかを把握するのに役立ちます。

スタックトレース (10:09)

アプリのクラッシュレポートを受け取って時、まず確認すべきことは、クラッシュしたアプリのバージョンとそのソースコードです、クライアント側のクラッシュはコールバックを中心に調査する必要があります。また、サーバー側では、リクエストやスタックトレースのあらゆるスタックが対象となります。Androidではコールバックが主なクラッシュの原因ですが、連鎖的に発生するコールバックの内、直接の原因となった前に発生したコールバックを把握することはとても困難なことです。

バグの再現 (12:17)

クラッシュを再現することはとても重要です。そのためにはユーザーとコンタクトをとることも必要になるかもしれません。ではどのようにすればクラッシュの不満をやわらげ、⭐️ 1つの評価をさけることができるのでしょうか?

対策としては、バグが起きた時にカスタムダイアログを表示し、追加の情報を求めることです(他にはカスタマーサポートへのリンクを表示することも考えられます。詳細はビデオで確認してください)。ActivityLifecycleを用いてactivityの有無を確認します。アプリケーションがアクティブでない場合は、単にプロセスをkillします。そうすることでプロセスは自動的にリスタートします。たとえばプロセスがクラッシュした後、ユーザーがRecentApps画面経由で他のアプリから戻ってきた場合、プロセスは自動的にリスタートし、かつ、保存されたactivity stateが渡されます。これは画期的で、クラッシュしたことにまったく気づかせなくすることができます。

もしアプリケーションを使用中にクラッシュした場合は、アクティビティを再度起動するためのIntentを作成したうえでフィードバックをうけるダイアログを表示します。注意点としては、現在表示中のアクティビティが終了しているかの確認をするということです。システムに対してアクティビティを起動することを依頼して自らは終了します。こうすることで、システムが現在表示中のアクティビティを再表示させます。このやり方は、JakeのProcess Phoenixの影響を大きく受けた手法で、おそらく、TwitterやFacebookも似たようなやり方をしているはずです。

クラッシュしたときにできる他のこと: 静的情報の取得 (17:39)

あと、たとえば、デバイスのタイプ、OSのバージョンだけでなくSHA(コードリポジトリのコミット時のハッシュ値)といったものも情報の収集に必要となってきます。ここまで集めることで、ソースリポジトリ上で問題となっている箇所を確認できるようになるのです。

表示中の画面 (18:56)

クラッシュ時の画面のスクリーンショットがあればもちろん尚よいことですが、それはビットマップのアップロードを意味します。これは、データとしてとても重い上に、プライベートな情報が含まれてしまう可能性があります。ですので、代わりにviewの階層構造をテキストデータで表したものを取得してみてはどうでしょうか。例として、Squareが提供するレジアプリの署名画面の場合をみていきましょう(ビデオをみてください)。署名のためのビューがあり、そこにはRelativeLayoutが含まれています。見ていただければわかるように、さまざまなビューや属性が表示されていますが、テキストは表示されません。表示されているプライベートな情報を見たいわけではないので、

UIテストフレームワークのEspressoのRootsOracleクラスを参考にしてみましょう。RootsOracleはリフレクションをつかってプロセスのすべてのWindowにアクセス可能にしてくれます。WindowはAndroidのバージョンによって異なった実装になっていますが、それぞれのバージョンでアプリケーションのすべてのルートビューへのアクセスを可能にします。すべてのルートビューにアクセスし、それらを文字列の形式に変換します。現在この機能を提供するライブラリを作成中で、近日中にオープンソース化する予定です。

履歴: ハイレベルのログ (20:56)

またハイレベルのlogの履歴という意味で”flight recorder”が必要となってくる。例えば、HTTP call にかかった時間を知ることはクラッシュ(タイミングの問題)での原因を解きほぐすのに役立ちます。さらには、”Show_Screen”では、クラッシュを再現するステップを示してくれます。私であれば、通常Viewの階層構造を確認し、過去のactivityのlogをチェックし、最新のactivity(ナビゲーションやHTTP call)もチェックするということをしています。これらの作業を簡単にするには、JakeWharton/Timberを使うというのが有益で、ここにlogを以下のように入れ込むという使い方をします。 (Timber.i("Navigating from that %s to %s, outgoingPath, incomingPath)). また、主要なHTTPはインターセプター呼び出しがあるので全てのHTTP call のlogを容易に取得することができます。

LeakCanary (22:37)

主要なクラッシュの原因としてOutOfMemoryErrorがありますが、問題となる箇所を特定しにくいのでスタックトレースはあまり活用されていません。LeakCanaryを使うと、簡単にメモリーリークをしている箇所を発見することができるので、予防的な観点からも有益なツールです。

いかにクラッシュを避けるか (23:06)

ここまで、crashをどのように直していくのかということを話してきました、しかし、本当に解決したいことは、いかにcrashを起こさせないか ということです。ここからは、これについて見ていきましょう。

防御的プログラミング (23:20)

防御的なプログラミングといっても、いろんな意味合いがあります。たとえば、入力フォームでemail欄を空白にするとなぜかクラッシュするというバグがあるとします。そういった場合に、空白を入力させないということでクラッシュさせず問題を無視するというやり方をとることもあります。後になって問題が明らかになることもあるからです。

攻撃的プログラミング: クラッシュファースト (25:10)

問題を無視するのではなく、まずは、クラッシュさせるという攻撃的プログラミング手法もあります。これは事前条件を明示するということです(今回だと、このフィールドは空白にはならないということ)。スタックトレース上で直接的な相関関係は見つけられないかもしれませんが、はやいうちに問題を検出することで、修正するのに必要なより多くの情報を得れるかもしれません。例えば、static methodを使い「ここでは、空白でないことがのぞまれており、空白が入力されると例外を投げクラッシュする」ということを示すことができます。早期にクラッシュさせることで、結果的に全体のクラッシュを軽減させることにつながっていきます。

例外のグルーピング (26:26)

共通のメソッドで事前条件をチェックして例外を投げるようにすると、異なる場所で発生した例外があたかも関連するものとしてまとめて表示されてしまうという副作用が発生します。詳細はビデオで確認していただきたいですが、ここでは、スローする例外をカスタマイズする方法を紹介します。checkNotNull()メソッドではまずNullPointerExceptionを生成し、StackTraceの配列から最後の要素以外をコピーしています。そしてそれを例外に入れなおすという作業をしています。こうすることによって、スタックトレース上で呼び出し元のメソッドが例外を投げているように表示することができます。グルーピングも行われなくなるうえに事前条件チェックの行われている部分がわかりやすくなりとても有益です。

統合テスト (27:48)

事前条件をたくさん使うようになると、それにともないたくさんのクラッシュを検出できるようになります。そのためのテストをEspressoで記述することができます。これを仮想マシン上で走らせます(仮想マシンを使わず、デバイス自体でやると間違ったテスト結果が表示されることもあるので)。意識して欲しいのは、ビルドの実行速度を早くさせるということです。極端なことをいえば、ビルドの実行に時間がかかるということは、テスト結果を得られないということですし、そこまでいかなくとも、1日に1回実行し、たった1度フィードバックを得るという大変効率の悪いことです。

スモークテスト (28:41)

スモークテスト(簡易版のテスト)と手動でのQAは、クラッシュを避ける上で有益な手段です。プロのテスターにお金を払ってアプリをクラッシュさせてもらいましょう。

ドッグフーディング / ベータ (28:37)

開発者は、ユーザとしてアプリを使うこともできます。Squareのレジアプリでは簡単ではないですが、外に出て実際にユーザが利用しているところで作業してみるのも有効です。

段階的なリリース (30:24)

リリースの方法としては、5%のユーザに段階的なリリースをするというやり方もあります。クラッシュが増える想定されるのですが、1〜2日ほどクラッシュを集めて、その数に想定ユーザ数を掛けあわせてクラッシュ率を想定します。注意しないといけないのは、多くの人に影響を与えるバグは何かということです(例えば、フェイスブックはリリースポリシーとしてニュージーランドでのリリースを想定してます。理由としては、ユーザの母集団がアメリカと似ているにも関わらず、メディアのインパクトが低いからです)。

Androidクラッシュ率 (31:45)

我々は、それぞれのリリースごとにクラッシュ率を表示するツールを開発しています。Squareで重点的に見ているのは、支払いとトランザクションのクラッシュ率です。ユーザが支払いなどをしたときに、何がクラッシュを引き起こす可能性があるでしょうか。一定期間ごと、バージョンごとにクラッシュ数をトランザクション数からクラッシュ率を算出します。これは、我々にとってビジネス上必要な重要な要素と評価基準になっています。

まとめ (32:45)

  • 再現
  • 静的情報(ユーザーのフィードバック)
  • Flight recorder
  • ビューの階層構造
  • 例外、そしてうまくいっているかを確認する方法
  • 段階的リリース、マニュアルテスト。スローするすべての例外を利用者に到達するまえに捕まえること

About the content

This talk was delivered live in November 2015 at Øredev. The video was transcribed by Realm and is published here with the permission of the conference organizers.

Pierre-Yves Ricau

Pierre-Yves Ricau is an Android Baker at Square, working on the Square Register. He formerly worked at Siine in Barcelona. He also enjoys good wine and low entropy code.

4 design patterns for a RESTless mobile integration »

close