この記事は Andrew Flynn & Jon Boekenoogen による Android Developers Blog の記事 " Play Time with Jetpack Compose " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
2020 年、Google Play ストアのエンジニアリング チームのリーダー陣は、ストアのショーウィンドウにあたる部分全体の技術スタックを再構築するという大きな決断を下しました。既存のコードは 10 年以上前のもので、無数の Android プラットフォーム リリースや機能アップデートを経て、大きな技術的負債を抱えていました。デベロッパーの生産性やストア自体のユーザー エクスペリエンスとパフォーマンスに悪影響を与えることなく、数百名のエンジニアが開発できるようにスケールアップできる新しいフレームワークが必要でした。
ネットワーク レイヤからピクセルのレンダリングに至るまで、ストアのあらゆるものを更新するため、複数年にわたるロードマップを作成しました。その一環として、インタラクティブ性とユーザーの快適さを目標とした最新の宣言型 UI フレームワークも採用したいと考えました。さまざまな選択肢を分析した結果、まだアルファ版にもなっていなかった Jetpack Compose を使うという、(当時としては)大胆な決断をすることになりました。
それ以来、Google の Google Play ストアチームと Jetpack Compose チームは、ストアの具体的なニーズを満たせるバージョンの Jetpack Compose を実現するため、非常に密接な連携のもと、リリースや改善を重ねてきました。この記事では、移行のアプローチやその過程で明らかになった課題や利点について説明し、多くのエンジニアが関わるアプリで Compose を採用するとはどういうことかについて共有したいと思います。
新しい UI レンダリング レイヤとして Jetpack Compose を検討するときの最優先事項は次の 2 つでした。
Google Play ストアはすでに 1 年以上 Jetpack Compose で UI のコードを記述しており、Jetpack Compose によって UI 開発がこれまで以上にシンプルになっていることを実感しています。
特にうれしいのは、UI の記述に必要なコードがかなり減ったことで、場合によってはコードが最大 50% 少なくなったことです。これが実現できたのは、Compose が宣言型 UI フレームワークであることに加え、簡潔な Kotlin を活用できるからです。カスタムの描画やレイアウトを作成する際も、ビューをサブクラス化してたくさんのメソッドをオーバーライドする必要はなく、シンプルな関数呼び出しで実現できます。
評価テーブルを例に説明しましょう。
ビューを使う場合、このテーブルは以下の要素で構成されます。
Compose を使う場合、このテーブルは以下の要素で構成されます。
@Composable
「そうは言っても、ライブラリの依存関係でビューが提供される場合どうすればいいのか」と疑問に思う方もいらっしゃるかもしれません。たしかに、すべてのライブラリ所有者が Compose ベースの API を実装しているとは限りません。私たちが最初に移行をしたときは特にそうでした。しかし、Compose では ComposeView と AndroidView API によって、ビューを簡単に利用できる相互運用性が実現されています。ExoPlayer (英語) や YouTube の Player などの人気ライブラリは、この方法によって問題なく統合できました。
Google Play ストアチームと Jetpack Compose チームは、Compose がビュー フレームワークと同じくらい速くジャンクなしで動作できるようにするため、密接に連携しました。Compose は Android フレームワークの一部というよりはアプリにバンドルされるものなので、これは難しい要件でした。画面上の個々の UI コンポーネントのレンダリングは高速でしたが、アプリが Compose フレームワーク全体をメモリに読み込むために必要な時間をすべて合わせれば、かなりの時間になりました。
Google Play ストアで Compose を採用するうえで、特に大きなパフォーマンス改善に貢献したのはベースライン プロファイルの開発でした。以前から利用できたクラウド プロファイルもアプリの起動時間の短縮に役立ちますが、これが利用できるのは API 28 以降に限られ、頻繁な周期で(毎週)リリースされるアプリにとっては効果的ではありません。この問題に対応するため、Google Play ストアチームと Android チームが連携して、ベースライン プロファイルの開発にあたりました。ベースライン プロファイルは、デベロッパーが定義し、アプリの所有者が指定してバンドルできるプロファイルです。アプリに同梱され、クラウド プロファイルとは完全な互換性があり、アプリレベルに限定して定義することも、ライブラリレベルで定義することもできます(Compose を採用すると、このプロファイルもついてきます!)。ベースライン プロファイルをロールアウトすることで、Google Play ストアの検索結果ページの最初のレンダリング時間は 40% 短縮されました。これは大きな成果です。
Compose は、特にスクロールの際に効率的なレンダリングをします。その中核をなす仕組みとなっているのが、UI コンポーネントの再利用です。Compose は、スキップできることがわかっている Composable の再コンポーズをできる限りスキップしようとします(不変である場合など)。しかし、すべてのパラメータが @Stable (英語) アノテーション要件を満たしている場合は、デベロッパーが強制的に Composable をスキップ可能にすることもできます。Compose のコンパイラでも、特定の関数をスキップできない理由を説明した便利なガイドが提供されています。Google Play ストアでは、スクロールが発生する状況で頻繁に再利用される UI コンポーネントを作りましたが、不要な再コンポーズが積み重なってフレーム時間が足りなくなり、ジャンクにつながるという状況が発生しました。そこで、デバッグ設定でもそのような再コンポーズを簡単に見つけることができるように、Modifier を作成しました。この手法を UI コンポーネントに適用することで、ジャンクを 10-15% 減らすことができました。
@Stable
Modifier
Modifier による再コンポーズの視覚化の例。青(再コンポーズなし)、緑(1 回の再コンポーズ)
Google Play ストア アプリの Compose を最適化するうえで、もう 1 つの重要な要素となったのが、アプリ全体の一連の移行戦略を詳細に作成したことです。最初に組み込みの実験をしたとき、「二重スタック問題」に直面しました。これは、1 つのユーザー セッション内で Compose とビューの両方のレンダリングを実行すると、特にローエンドのデバイスにおいて、メモリに大きな負荷がかかるという問題です。これはコードを同じページに展開した場合だけでなく、異なるスタックのそれぞれに 2 つのページ(Google Play ストアのホームページと検索結果ページなど)が存在する場合にも発生しました。これに起因する起動の遅さを解消するには、ページを Compose に移行する順番やスケジュールについて、具体的な計画を立てることが重要でした。さらに、アプリが完全に移行されるまでの穴埋めとして、よく使うクラスの短期的なプリウォーミングを追加することも有用であることがわかりました。
Compose は Android フレームワークにバンドルされていないので、Google Play ストアチームが Jetpack Compose に直接的に関与する手間も少なくすみました。その結果、短い時間でデベロッパーに役立つ改善をすることができました。Jetpack Compose チームとの共同作業で、LazyList のアイテムタイプのキャッシュのような機能追加をしたり、無駄なオブジェクトの割り当てなどに関する簡単な修正をすばやくしたりすることもできました。
Google Play ストアで Compose を採用したことで、チームのデベロッパー満足度は大幅に上昇し、コードの品質と健全性も大きく向上しました。Google Play ストアの新機能は、すべてこのフレームワーク上に構築されています。Compose はアプリの速度向上や利便性に貢献しています。Compose への移行戦略の性質上、APK サイズの変化やビルド速度は細かく測定できませんでしたが、可視化できたものについてはすべて順調に進んでいます。
Compose は Android UI 開発の未来です。Google Play ストアの事例から言うと、これ以上すばらしいものはありません!
この記事は Android Team による Android Developers Blog の記事 " Chrome’s multitasking usage increases 18x on large screens " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
Google Chrome は、世界で最も広く使われているブラウザです。そして Chrome チームは、そのユーザーがあらゆるデバイスで確実にすばらしい体験ができるようにしたいと願っています。多くの Chrome ユーザーは、モバイル、タブレット、折りたたみ式デバイスにも生産性機能が追加され、PC 版 Chrome の機能に匹敵するようになることを望んでいます。こういったニーズに応えるため、マルチタスク機能を推進できる仕組みを構築することにしました。この機能の開発対象にはスマートフォンも含まれていますが、最もよく使われると思われるタブレットや折りたたみ式などの大画面デバイスを特に主眼において実装することにしました。
まずは、複数の Chrome ウィンドウ(インスタンス)を並べて開く方法の開発に注力することにしました。それに当たり、タスクバーなどの 12L の機能や、Samsung のエッジパネルを活用しました。
横に並べる機能は、singleInstancePerTask (英語) 起動モードを使って構築しました。必要だったのは、ユーザーが同時にたくさんのウィンドウを使えるようにすることと、機能のユーザビリティを両立することです。そこで、ユーザビリティのベスト プラクティスを調査し、大画面デバイスでの他のマルチウィンドウ体験を観察し、デバイスで最適なメモリ使用量を保証するための制限について検討しました。そして、大画面デバイスでは最大 5 つのウィンドウを並べても快適に利用できると判断し、この機能をサポートするようにアプリを更新しました。
また、ユーザーにとって使いやすい機能にするため、メニューに新規ウィンドウのショートカットを追加しました。このショートカットは、新しいインテント フラグの組み合わせ LAUNCH_ADJACENT | NEW_TASK (英語) を使って作成しました。プロダクト内でこの機能が目立つように表示されたことで、使用頻度が大幅に向上し、 マルチウィンドウの利用が 18 倍になりました。
これは新機能ですが、Chrome アプリのマルチインスタンスの利用は、この機能をサポートしているスマートフォンと比較して、タブレットと折りたたみ式では 42% 多いことがすでにわかっています。利用状況から、この機能は大画面デバイスの Chrome ユーザーに好意的に受け止められていることが示されています。また、このような機能を構築して、大画面の Chrome ユーザーのエクスペリエンスを拡張することは、価値がある作業であることもわかります。
さらに、アプリのレビューという形でも、以下のように大画面ユーザーからたいへん好意的なフィードバックが寄せられています。「このアプリはすばらしい 👌!画面を分割したり、タブを変更したりできます。その中でたくさんのゲームをプレイすることもできます。私はこのアプリに 5 つ星をつけます」
今後も、大画面での Chrome エクスペリエンスとユーザーの生産性をさらに向上させたいと考えています。
大画面向けにアプリの最適化を始める方法は、こちらをご覧ください。
この記事は Kateryna Semenova, Rahul Ravikumar, Chris Craik による Android Developers Blog の記事 " Improving App Performance with Baseline Profiles " を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。
多くのアプリにおいて、アプリのパフォーマンスとユーザー エンゲージメントとの間に相関性があることがわかっています。ユーザーが期待するのは、応答性が高く、読み込みが速いアプリです。起動時間は、アプリのパフォーマンスと品質の重要な指標の 1 つになっています。
私たちのパートナーのいくつかは、アプリの起動を最適化するために、すでに多くの時間とリソースを費やしています。その一例として、Facebook のストーリーをご確認ください。
この記事では、ベースライン プロファイルと、それがどのようにアプリやライブラリのパフォーマンスを改善するかを紹介します。その結果、起動時間が最大 40% 短縮されることもあります。ここでは起動に注目しますが、ベースライン プロファイルはジャンクにも大変有効です。
Android 9 (API レベル 28) では、アプリの起動時間短縮を目的として、Play Cloud に ART 最適化プロファイル (英語) が導入されました。クラウド プロファイルが利用できる場合、アプリのコールド スタートは、さまざまなデバイスで少なくとも平均 15% 高速になることがわかっています。
アプリがインストール後やアップデート後に初めて起動する場合、JIT コンパイルが行われるまでの間、コードはインタープリタ モードで動作します。APK では、Java と Kotlin のコードは dex バイトコードにコンパイルされますが、完全にコンパイルしたアプリの保存や読み込みにかかるコストの関係で、完全にマシンコードにコンパイルされることはありません (Android 6 以降)。アプリのクラスやメソッドのうち、頻繁に使われるものやアプリの起動に使われるものは、プロファイルのファイルに記録されます。そしてデバイスがアイドルモードになると、ART がそのプロファイルに基づいてアプリをコンパイルします。これにより、その後のアプリ起動が高速になります。
Android 9 (API レベル 28) より、Google Play もクラウド プロファイルを提供するようになっています。デバイスでアプリを実行すると、ART が生成したプロファイルが Play Store アプリにアップロードされ、クラウドに集約されます。アプリに対して十分な数のプロファイルがアップロードされると、Play アプリは集約したプロファイルを利用して、その後のインストールを行います。
クラウド プロファイルが利用できる場合、それが大きな効果を発揮してくれますが、アプリをインストールするときに常に利用できるとは限りません。通常、プロファイルの収集と集約には数日かかるため、多くのアプリで毎週アップデートが行われると、この点が問題になります。クラウド プロファイルが利用できるようになる前に、多くのユーザーがアップデートをインストールすることになるからです。そこで、Google の Android チームは、プロファイルが用意できるまでの時間を短縮する別の方法を探し始めました。
ベースライン プロファイルは、プロファイルを提供する新たなメカニズムであり、Android 7 (API レベル 24) 以降で利用できます。ベースライン プロファイルは、Android Gradle プラグインが生成する ART プロファイルです。人間が読むことができるプロファイル形式になっており、アプリやライブラリによって提供されます。たとえば、次のようなものです。
HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)VHSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)IHLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()VPLandroidx/compose/runtime/CompositionImpl;->applyChanges()VHLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
Compose ライブラリの例
バイナリのプロファイルは、APK のアセット ディレクトリの特定の場所 (assets/dexopt/baseline.prof )に格納されます。
ベースライン プロファイルは、ビルド時に作成され、APK の一部として Play に提供されます。そして、ユーザーがアプリをダウンロードするときに、Play からユーザーに送信されます。クラウド プロファイルがまだ利用できない場合、ベースライン プロファイルが ART クラウド プロファイル パイプラインの足りない部分を補います。クラウド プロファイルが利用できる場合、ベースライン プロファイルは自動的にクラウド プロファイルと結合されます。
ベースライン プロファイルの作成からエンドユーザーへの配信までのワークフローを示した図
ベースライン プロファイルの特に大きなメリットは、ローカルで開発や評価が行える点です。そのため、デベロッパーは実際にエンドユーザーが体験するパフォーマンスの向上を確認できます。また、クラウド プロファイルは Android 9 以降でしか利用できませんが、ベースライン プロファイルはそれよりも古いバージョンの Android (7 以降) でもサポートされています。
2021 年の前半に、Google マップのリリース サイクルが 2 週間から 1 週間に変更されました。アップデートの頻度が高くなるということは、ローカルの作成済みプロファイルがより頻繁に破棄され、Play クラウド プロファイルが存在しないために起動が遅くなるユーザーが増えるということでもあります。しかし、ベースライン プロファイルを使うことで、Google マップの起動時間は平均 30% 短縮され、それに伴って検索が 2.4% 増加しました。このように既に定着したアプリにとっても、非常に大きな成果です。
ライブラリに含まれるコードも、アプリのコードと同じです。デフォルトでは完全にコンパイルされないので、起動時のクリティカル パスで大量の処理を行う場合、問題につながる可能性があります。
Jetpack Compose は、Android システム イメージには含まれない UI ライブラリです。そのため、多くの Android View ツールキットのコードとは異なり、インストール時に完全にコンパイルされることはありません。そのため、パフォーマンスの問題が発生することがありました。特にそれが顕著だったのが、最初の数回のコールド スタート時です。
この問題を解決するため、Compose はプロファイル インストーラを利用します。これがベースライン プロファイルのルールを提供してくれるので、Compose アプリの起動時間やジャンクが減少します。
Google Play ストアの検索結果ページは、Compose を使って書き直されています。Compose からベースライン プロファイル ルールを取り込んだ後では、イメージを含む最初の検索結果ページを表示するまでの時間が最大 40% 短縮されました。
Android チームも、関連する AndroidX ライブラリにベースライン プロファイルを追加しました。これは、対象のライブラリを使うすべての Android アプリにメリットがあります。Constraint Layout では、プロファイルのルールを提供 (英語) することで、アニメーションのフレーム時間が 1 ミリ秒以上短縮されました。
ベースライン プロファイルを含めると、アプリやライブラリのデベロッパーすべてがメリットを得られます。理想的な方法は、特に重要なユーザー操作について、デベロッパーが複数のプロファイルを作成することです。それにより、クラウド プロファイルが利用できるかどうかによらず、常にその操作のパフォーマンスが高速になります。アプリ デベロッパーとライブラリ デベロッパー向けのベースライン プロファイルの設定方法は、詳細なガイドをご覧ください。
すぐにはアプリのベースライン プロファイルを作成できないという方でも、依存関係を更新することで、ベースライン プロファイルによるメリットを享受できます。Android Gradle Plugin 7.1.0-alpha05 以降でビルドすれば、ライブラリ (Jetpack など) が提供するベースライン プロファイルを APK に含めることができます。Google Play は、インストール時にそのプロファイルを使ってアプリをコンパイルします。プロファイルの追加は、アプリのビルドの一環として行うことができます。
忘れずに改善の測定を行うようにしましょう。生成されたプロファイルを使ってローカルで起動を測定するには、こちらの手順に従います。
ぜひフィードバックを共有し、皆さんの体験をお知らせください! (英語)