こんにちは。去年の10月からロコガイドでAndroidアプリの開発をしている石川です。
入社以降は、トクバイAndroidアプリの開発をやらせてもらっていて、デザインシステムの導入、AndroidViewからComposeへの移行、パフォーマンス改善といったことに取り組んできました。
この記事では、その中からパフォーマンス改善の取り組みについて紹介します。 トクバイのパフォーマンス改善ではある程度の結果が出てきているので、ボトルネックの調査方法や実際に行った施策等、これからAndroidアプリのパフォーマンス改善に取り組みたいという方の参考になる内容になっていると思います。
なぜパフォーマンス改善をするのか
具体的に取り組んだ内容を紹介する前に、そもそもなぜパフォーマンス改善に取り組んだのかを話したいと思います。 パフォーマンス改善を行うメリットとしてざっくりと以下の二つあるかと思います。
- ユーザー体験の向上によるアプリ継続使用率UP
- AndroidVitalsの数値向上によるストア掲載順位UP
どちらも事業に対して大きくプラスの効果を持つもので、アプリの継続率やストア掲載順位を上げられるなら十分に取り組む価値があるものだと思います。 ということで、トクバイAndroidアプリでもパフォーマンス改善に取り組んでいます。
トクバイアプリパフォーマンス改善の現在地(12月中旬)
では、ここからトクバイアプリでの事例を紹介していきます。
具体的な方法論を紹介する前に、ここまでのパフォーマンス改善の結果を紹介します。
主に、「アプリの起動時間短縮」と「主要画面のスクロールパフォーマンス改善」という2つの側面からパフォーマンス改善に取り組んでいて、開始から1ヶ月ほどの現時点でそれぞれに一定の結果が出てきています。
アプリの起動時間短縮
アプリの起動時間(青い線)はパフォーマンス改善開始前と比較して 15% 速くなっています。
また、v6.19.2(紫の線)とv6.20.1(緑色の線)の起動時間の推移を比較してみると、線の始点であるアプリリリース直後の起動時間がv6.19.1では10秒以上だったのが、v6.20.1では5秒以上短くなって4秒強のところから始まっていることがわかります。これは後述する、Baseline Profilesというものを導入した効果だと思われます。(15%だとそこまで改善できていないように思えますが、言い訳をさせてもらうと、トクバイの場合SDKが2秒ほど起動時間を使っているので、パーセンテージは低めにでます。)
主要画面のスクロールパフォーマンス
スクロールパフォーマンスには一般的に使われている指標だと、2つあって「遅いレンダリングフレームの発生率」と「フリーズフレームの発生率」が広く使われているかと思います。 トクバイでもこの2つの発生率を類似アプリ以下の水準に持っていくということを1つの基準にしています。元々、遅いレンダリングの方は類似アプリよりも良い数値なので、今回のパフォーマンス改善ではフリーズフレームの値をよくすることに先に取り組んでいます。
取り組みの結果、フリーズフレームの値は31.35%から15.41%になっていて、50% 以上数値を改善できています!! 地道なボトルネック調査と調査に基づいた施策を実施した結果が綺麗に出ていて非常に嬉しい結果です。 更に言うと、元々圧倒的に負けていた類似アプリの水準に、勝ったり負けたりと言う水準まで持ってこれているので、あとちょっとで完全に勝つところまで持っていけそうです。
調査方法
結果を紹介できたので、具体的な取り組み内容に入っていきます。
パフォーマンス改善の取り組みは、以下のような流れで実施しました。
- 現状把握
- ボトルネック調査と改善策の立案
- 改善策の実施
- リリース & 結果の確認
現状把握でどのパフォーマンス数値が悪いのか、どの動作をしている時にパフォーマンスが悪いのかといった大雑把なあたりをつけて、あたりをつけたところを重点的にボトルネック調査をするといった形で、パフォーマンスを下げている原因を特定していきました。
現状把握に使用したツール
現状把握では以下のようなツールを使用しました
Android Vitals
AndroidVitalsでは起動時間、レンダリングパフォーマンス、バックグランドでの通信状況などを確認できます。AndroidVitalsの数値改善自体がパフォーマンス改善の目的になるので、必ず確認しましょう!
類似アプリとの比較から、どの数値に積極的に取り組むべきか見えてきます。
Firebase Performance
FirebaseパフォーマンスはAndroidVitalsよりもより細かいパフォーマンス指標を見ることができて、画面単位のレンダリングパフォーマンスやアプリバージョンごとの数値を確認できます。
トクバイアプリでは、このFirebasePerformanceの指標から以下のようなことをつかむことができました。
- トップ画面のフリーズフレームの値が圧倒的に悪い
- 起動時間があるバージョンから2秒も伸びてしまっている
- アプリをアップデートした直後は起動時間が爆伸びする
Firebase Performanceだけでも、どの画面のスクロールパフォーマンスがAndroid Vitalsの数値を下げているのか、いつ行った変更が起動時間を下げているのかというのがわかったりするので、ボトルネック調査をする対象を絞り込むのに便利です。
LayoutInspector
Android Studioのレイアウトインスペクタを使うとどれだけのViewが描画されているかがわかるのと、Composeの場合は無駄にリコンポーズされている箇所をチェックすることができます。
トクバイアプリでは、次に紹介するGPUレンダリングプロファイルを使用してホーム画面ではGPUの処理待ちによってフレームレートが低くなっていることがわかったのですが、その理由はホームでViewを大量に持っているからだというのがレイアウトインスペクタから予測することができます。
GPUレンダリングプロファイル
GPUレンダリングプロファイルは開発者オプションの1つで、フレーム時間を棒グラフでリアルタイムに表示してくれます。棒の色でどういった種類の処理に時間を取られているかがわかります。(developerドキュメント)
リアルタイムでフレーム時間を表示してくれているので、Firebase Performanceでパフォーマンスが悪い画面に行って、スクロールしたりすると、どのあたりの描画でフリーズフレームが発生しているのかわかります。また、フレーム時間の内訳もわかるので、そこからある程度何をしたらパフォーマンス改善できるかが見えてきます。
例えば、トクバイアプリではトップを半分くらいスクロールしたタイミングでフリーズフレームが発生していて、その内訳として「その他の時間とVsync遅延(developerの定義 )」がほとんどの時間を占めているので、Compose関数の中で何かしら重い処理をしていて、それがフリーズフレームを発生させているというのが予想できました。
ボトルネック調査
上記の4つのツールを使って、どのあたりの処理を修正すべきか見えてきたらボトルネック調査をしていきます。トクバイでは、トップ画面の特に下半分にフリーズフレームを発生させている原因があるというのと、v6.9からv6.10への変更で起動時間が2秒伸びたこと、アップデートするたびに起動時間が爆伸びする現象があるということがここまででわかっていたので、トップ画面下半分のコンポーザブル関数、v6.9からv6.10の変更内容(特に起動時に関連する箇所)、アップデートのたびに起動時間を伸ばす要因は何か?といったことを重点的に調査していきました。
その時にはAndroid StudioのSystemTraceを使用してボトルネック調査を実施しました。
System Trace
Android StudioのSystem Traceでは、どの処理がどこで実行されているかタイムラインで見ることができて、関数ごとの累計処理時間も見ることができます。
遅いフレームがどのタイミングで発生しているかもタイムラインで確認できるので、JankFrame発生時の長い処理を特定する形でスクロール中のフリーズフレームの発生源となっている処理もこれで特定することができました。
また、Android端末側でトレースファイルを作成することで、起動中の重い処理が何なのかを特定するのにも使用できます。トレースコードを仕込むことで、時間を計測する場所を指定することもできるのでボトルネック調査には非常に便利。
CanaryビルドのAndroid Studio Fragmingoとライブラリを使用すると、コンポーザブル関数の中まで追うことができるようになるので、ボトルネック調査の時にはお勧めです。
トクバイアプリではSystem Traceを使ってExoplayerの初期化をUIスレッドで行っているのがフリーズフレームの主な発生源であることを特定して、フリーズフレームの値を半減することができました。
また、重たい処理を特定して別スレッドにオフロードすることによって、Firebase Performanceで測定しているアプリ起動時間を短縮できています。
取り組み中に直面した課題と解決方法
ここまでが実際にトクバイアプリでパフォーマンス改善に取り組んだ時の、調査方法の紹介です。
調査後の実際の改善の実施については、トクバイアプリ以外では再現性の低いものになるので割愛して、パフォーマンス改善中に直面した課題とその解決方法について紹介したいと思います。
直面した課題
パフォーマンス改善の取り組みの中で直面した課題は、「改善策として実装したコードが本当にパフォーマンスを改善しているのかわからない」ということです。
また、実装者としては意味があるとわかっていても、スレッドがどうこうとか、ライブラリのキャッシュ設定がどうこう、といったアプリ開発者としては普段聞きなれないトピックを扱うので、Pull Requestのレビュワーは本当に意味があるか判断できないという状況になりました。
結果として、改善策を実装したPull Requestを出してみるもののペンディングしてしまうという状況になってしまいました。
ではどうしたのか?
この問題の解決策として、トクバイアプリにMackrobenchmarkライブラリを使用した、起動時間とトップのスクロールパフォーマンスを計測するためのベンチマークテストを導入しました。
下の画像は、Baseline Profilesなしのウォームアップスタート(アプリ起動の一種)の時間を計測したものです。10回アプリを起動して、起動時間の最小値、中央値、最大値を出してくれています。
timeToInitialDisplayMs min 82.2, median 178.5, max 216.3
トレースファイルも作ってくれるので本当に便利です。(今回は、ここのトレースファイルは使いませんでしたが)
ちなみに、フレームレートの計測や自分で計測する部分を設定して、ベンチマークテストを走らせることも可能で、フレームレート計測ではパーセンタイルで結果を出力してくれます。
ベンチマークテストを導入すると、実装した改善策の結果をリリースしなくてもすぐにみることができるので、結果をPull Requestに貼り付けるとその効果をレビュワーに伝えることができます。
また、実施したものの意味がなかった施策はリリースする前に、破棄することができます。
トクバイでは、ベンチマークテストの結果を以下のようにPull Requestに貼ったものもありました。フレームレートが改善していることがわかります。
このような形で、Macrobenchmarkによるベンチマークテスト導入によって、改善策実施時や、Pull Requestレビュー時のモヤモヤを軽減することができました!
Baseline Profiles
ここまででトクバイでのパフォーマンス改善への取り組みのうち、他のプロジェクトでも使える事例はほとんど紹介することができました。
最後に、どのアプリでも導入によってパフォーマンス改善が見込めて、再現性の高い、Baseline Profilesについて紹介したいと思います。
下の画像は、トクバイアプリでBaseline Profilesを導入する前のアプリ起動時間の推移です。
アプリをアップデートしてリリースすると、リリース直後のアプリの起動時間がかなり長く、リリース1日後あたりから急激に起動時間が短くなって落ち着く動きが見られます。これは、クラウドプロファイルという仕組みに影響を受けています。クラウドプロファイルはストアからアプリをダウンロードしたユーザーの動きから事前コンパイルしておくとアプリパフォーマンスが改善する箇所を収集して、事前コンパイルをしてくれるようになります。ただ、アプリの更新後、このクラウドプロファイルが配布されるようになるまで、数日から数週間あるため、アプリ更新後の起動時間が爆伸びするという現象が発生します。
なので、頑張って頻繁にアプリを更新すればするほど、アプリパフォーマンスが悪くなるという悲しい現象が発生してしまいます。
トクバイではアップデート後数日は、リリース範囲をかなり絞っているので、アップデート後の起動時間の伸びの影響を受けているユーザーはそこまで多く無かったのですが、それでも、一定数のユーザーがかなり長い起動時間を体験してしまうのは望ましくないので、Baseline Profilesを導入してみました。
BaselineProfilesを導入するためのコードの変更自体は、Macrobenchmarkのテストコードを呼び出すようにすればすごく簡単で、トクバイでは以下のようなコードを追加するだけでした。(Macrobenchmarkのテストコードを作るのはすごく大変でしたが..)
@OptIn(ExperimentalBaselineProfilesApi::class) @RunWith(AndroidJUnit4::class) class BaselineProfileGenerator { @get:Rule val baselineRule = BaselineProfileRule() @Test fun generateBaselineProfile() = baselineRule.collectBaselineProfile( packageName = "....", ) { pressHome() startActivityAndWait() walkThrough() scrollTop() } }
たったの17行です。
このテストをエミュレータで実行するとbuildパッケージの中に、数MBあるテキストファイルが生成されるので、これをappモジュールの中のmanifestと同じ階層に入れてあげればBaseline Profilesのセットアップ完了です。
では、結果を見てみましょう。
紫色の線がBaseline Profiles導入前バージョンの起動時間の推移を表していて、緑色の線は導入後のバージョンの起動時間の推移を表しています。一番起動時間が長い状態を比較してみると、10.25秒から4.37秒まで落ちています。これで、とてつもなく長い起動時間を経験するユーザーを減らすことができました!!! みなさんもBaseline Profilesを是非導入してみてください。
まとめ
トクバイでのパフォーマンス改善の事例はいかがだったでしょうか?
細かい方法論ではなく、調査ツールや使用したライブラリの紹介といった、他のプロジェクトでも再現性の高い内容を紹介したつもりなので、紹介したものの中から「これを試してみよう!」といったものを見つけていただけていたら幸いです。
トクバイAndroidアプリのパフォーマンスは現在も課題を抱えた状態で、これからも改善に取り組んでいこうと思っています。
また、パフォーマンス改善では新機能追加のタイミングで無駄に重い処理を追加してしまうことを防ぐのも大きなポイントだと思います。そういったことを防ぐためにも、おいおいMacrobenchmmarkのベンチマークテストや仮想マシンを使用したBaseline Profiles生成のCIなんかも設定できたら良いなと考えているところです。