問題
Android アプリ(Android 版 Chrome も含む)では、コンパイルされた Java コードが
.dex ファイルに格納されます。Android 版 Chrome には
マルチプロセス アーキテクチャが採用されているため、そのユーザー エクスペリエンスが .dex サイズの増加に特に影響されやすくなります。通常、Android の Chrome では、ブラウザ プロセス、GPU プロセス、1 つ以上のレンダラ プロセスという 3 つ以上のプロセスが常に実行されています。Chrome の Java コードの大半はブラウザ プロセスでのみ使われます。しかし、そのコードを読み込むためのパフォーマンスとメモリのコストは、すべてのプロセスが支払うことになります。
バンドルと機能モジュール
プロセスを実行するために必要な最小チャンクの Java を読み込むことができれば理想的です。
Android App Bundle を使ってコードを
機能モジュールに分割することで、それに近づくことができます。機能モジュールを使うと、コードやリソース、アセットを個別の
APK に分割し、オンデマンドでもアプリのインストール時でも、ベース APK とともにインストールできます。
ということは、まさに必要としているものが手に入りそうです。つまり、ブラウザ プロセスのコード用機能モジュールを作り、必要なときにそれを読み込むことができるかもしれません。しかし、Android はそのようにして機能モジュールを読み込むわけではありません。デフォルトで、すべてのインストールされている機能モジュールは起動時に読み込まれます。ベース モジュールと 3 つの機能モジュール "a"、"b"、"c" があるアプリなら、Android の
Context と、次のような
ClassLoader が得られます。
状況によっては、インストールするモジュールを最低限にとどめ、起動時にこれらのモジュールすべてを即座に読み込むという方法が役立つこともあります。たとえば、一部のユーザーしか必要としない大きな機能がある場合、必要のないユーザーはそれをまったくインストールしないようにします。しかし、一般的に使われる機能の場合、実行時に機能をダウンロードしなければならないと、ユーザーは不便を感じる可能性があります。たとえば、動作が遅くなったり、モバイルデータが利用できないときに問題になったりします。理想的な方法は、標準モジュールをすべて事前にインストールしておいて、実際に必要になったときのみ読み込むことです。
解決策は Isolated Splits
数日間 Android ソースコードを探し続けた結果、
android:isolatedSplits という属性が見つかりました。これを "true" に設定すると、インストールされた分割 APK が起動時に読み込まれなくなり、明示的な読み込みが必要になります。これこそ、プロセスのリソース使用量を減らすために必要としていたものです。これにより、先ほどの ClassLoader は次のようになります。
Chrome では、レンダラーや GPU プロセスに必要な少量のコードを引き続きベース モジュールに配置し、ブラウザなどの高価な機能のコードは機能モジュールに分割し、必要なときに読み込みます。この方法を使うことで、子プロセスに読み込まれる .dex サイズを 75% 減らし、最大 2.5 MB にすることができました。その結果、起動が速くなり、メモリ使用量も減りました。
このアーキテクチャによって、ブラウザ プロセスの最適化も可能になります。アプリケーションの初期化中にブラウザ プロセスのコードの大部分をバックグラウンド スレッドでプリロードした場合も起動時間を短縮でき、読み込み時間が 7.6% 高速になりました。ブラウザのコードが必要なアクティビティなどのコンポーネントが起動するときには、すでに読み込みが終わっています。機能モジュールへの機能の割り当てを最適化すると、オンデマンドで機能を読み込むことができます。これにより、機能が実際に使われるまで、メモリや読み込みのコストを節約できます。