App Startup について

Jumpei Matsuda
12 min readJun 21, 2020

--

App Startup は Jetpack component の1つで、順序を指定したコンポーネント初期化の仕組みを提供します。

背景と問題点

現在、Firebase をはじめ、多くの 3rd party library が ContentProvider を利用した初期化を行っています。

ContentProvider を用いた初期化では、Application クラスへコード片の追加を要求することなく利用できるため、利用者側の設定ミスを減らすことが可能です。また SDK 側で初期化が完結出来る場合に非常に有用でした。

加えて、ContentProvider は Application の作成より前に初期化されます。Firebase をはじめとした基盤モジュールと各コンポーネントが分かれている中央集権型の初期化機構を持つサービスの場合にとっては、これ以上ない初期化タイミングとなります。

しかしながら ContentProvider には以下の問題点があります。

  • 初期化順序が定義できないため、依存関係がある場合は複数の ContentProvider でなく単一の ContentProvider に落とすなど、利用者側による理解と利用が必要
  • ContentProvider の数に対して線形に処理時間が増える

Jetpack App Startup ではこの問題に対し、順序指定可能な初期化機構を提供しています。具体的には App Startup が ContentProvider を提供してくれるため、開発者はその上で順序を指定しつつ各コンポーネントを初期化することになります。アプリ全体で初期化に利用する ContentProvider を App Startup の提供する ContentProvider ただ1つに留めることで、ContentProvider を複数用意した場合に必然的に発生する時間が極力抑えられます。

使い方

以下の3ステップで利用出来ます。

  • App Startup の依存の追加
  • Initializer の作成
  • Initializer の登録
依存の追加

次に Initializer を作成します。Initializer の型引数はその Initializer に関連する初期化済み Component が想定されています。 create(Context) で初期化を、 dependencies で依存先の Initializer を指定します。なければ emptyList で問題ありません。

最後に Initializer を AndroidManifest 経由で登録します。application タグ直下に provider タグを生やし、App Startup が使う ContentProvider に対して merge strategy (merge) を定義します。これで複数の flavor や 3rd party library で定義した meta-data がマージされます。

meta-data には Manifest placeholder が使えますが、Component ではないため Application や Activity で記述出来るショートカット記法はサポートされていません。

Initializer の登録

あとはアプリを起動するだけで、Initializer#create が実行されます。

初期化対象

生成及び初期化されるのは以下のどちらかの条件を満たした Initializer です。

  • AndroidManifest の meta-data に記述された Initializer
  • 上記 Initializer が依存している Initializer

また各 Initializer#create はただ一度だけ呼び出されます。

初期化順序

App Startup では事前に依存グラフを構築した上で処理されるわけではなく、実行時に meta-datadependencies から動的にグラフを構築します。したがってその実行順序は meta-datadependencies に依存します。

App Startup はまず ContentProvider metadata を使って Initializer を生成します。次にその initializer の dependencies を先に初期化し、依存を全て初期化し終えると自身を初期化します。したがって、meta-data 記述がなくとも dependencies に記述のある Initializer の初期化は担保されます。

meta-data の順序は記述順ではなく、 name の辞書順のようです(AOSP潜ったんですが見つけられず、実行順序から推測)。したがって Manifest merge 順序やビルドに関わらず不変です。また dependencies についてはその List 順序に依存します。

上記のような Initializer 依存グラフがあるとき、全ての終端 Initializer に到達するには以下のシーケンスが存在します。

  • Initializer1
  • InitializerF
  • InitializerA -> InitializerC -> Initializer0
  • InitializerA -> InitializerB ->InitializerD
  • InitializerA -> InitializerB ->InitializerE
<meta-data
android:name="${applicationId}.Initializer1"
android:value="androidx.startup" />
<meta-data
android:name="${applicationId}.InitializerF"
android:value="androidx.startup" />
<meta-data
android:name="${applicationId}.InitializerD"
android:value="androidx.startup" />
<meta-data
android:name="${applicationId}.InitializerE"
android:value="androidx.startup" />
<meta-data
android:name="${applicationId}.Initializer0"
android:value="androidx.startup" />

上記 meta-data の記述は全ての終端 Initializer を記述しているため、上記グラフ内の Initializer は最終的に全て初期化されます。実際に初期化される順序については以下の通りです。

  • Initializer0 : InitializerA, C, 0
  • Initializer1
  • InitializerD : InitializerA (no-op), B, D
  • InitializerE : InitializerA (no-op), B (no-op), E
  • InitializerF

まず meta-data の記述順は無視され、name 辞書順である 0, 1, D, E, F と並びます。つまり Initializer0 の初期化から始まります。0 には 0 -> C -> A という依存があるため、依存グラフの根である A から順に初期化が実行されます。

次に meta-data に戻り、1 が、さらに D (A, B, D), E (A, B, E) と初期化されます。Initializer の初期化ロジックはただ1度しか実行されないため、Initializer A や B が2度初期化されることはありません。

最後に F を初期化し、その初期化プロセスを終了します。

全ての Initializer を初期化する必要十分条件は全ての終端 Initializer を meta-data として記述することです。ただ中間 Initializer を含めた全ての Initializer を記述したとしても、依存の順序は保証されるため問題なく動作します。ただし中間 Initializer を記述すると全体の順序が変わる可能性があります。(とはいっても、それで問題が起きるなら依存定義不足を意味するため問題はありません。また今回の例題では順序は変わりません。)

初期化した値の取り出し

2020/06/22 00:53 JST 追記: ここの記述は Lazy initialization も出来るといった流れになってしまっていますが、そもそも Lazy initialization の方が目的として設計されていることに注意してください。

App Startup で初期化した値は以下のようにして任意の場所で取り出すことができます。(正確には初期化されていない場合は初期化されてその値が、初期化されている場合はキャッシュされた値が返ります)

すでに初期化された component の取り出し

また初期化順序が保証されているため、依存先 Initializer であれば依存元 Initializer から参照することも可能です。

とはいってもアプリ全体でDI コンテナを使っている場合、AppInitializer から取り出すのは原則違反のように見えますし、App Startup内でのDIコンテナとして利用するだけに留める方が得策なように感じます。アプリ側で DI コンテナを使っていないなら、安全なシングルトンの取り出しとして利用出来るはずです。

注意点

高速化について

初期化自体の速度が上がるわけではないため、単に Application で行っている初期化を移設しただけでは特に高速化は図れません。例えば SDK を通して利用している 3rd party の ContentProvider を削除する必要がありますし、それで削れるのも ContentProvider の初期化にかかる時間です。

並列化について

初期化の並列性は提供されません。自分で Coroutine を起動する Initializer を用意する必要などがあります。

依存グラフについて

依存グラフは DAG であることが求められますが、循環している場合はクラッシュします。

暗黙的な依存について

アプリのカスタム Application クラスは暗黙的に App Startup の Initializers に依存することになります。つまり App Startup の Initializer から カスタム Application クラスを参照することはできません。

そのためアプリ初期化の多くを占める(であろう) DI コンテナの初期化を移すことは非常に難しいです。ただ DI コンテナであっても Application クラスと独立した Component (e.g. Dagger の Module) であれば問題なく移すことができます。

Initializer の初期化し忘れについて

meta-data を書きそびれると面倒くさそうだなというのが正直なところです。全ての Initializers を依存とする Initializer を用意しておいてそれだけを AndroidManifest に記述する、つまり meta-data ではなくコード側で管理する方法も考えられます。とはいってもこの方法でも書き忘れることは防げません(そういう Initializer を作成する APT は書けそうですね)。また AndroidManifest でもコード管理でも Linter を書くのはさほど難しくないように思えます。

存在しない Initializer について

ちゃんと(?)クラッシュします。

雑感

ContentProvider の乱立を抑えることができる点は非常に良い仕組みだと思います。一方で、自分の関わっている複数アプリに適用しようにも DI コンテナの初期化が横断的になり、あまりスマートに書けませんでした。

Initializer は登録しなければ自動初期化されず、自分で任意のタイミングで初期化することもできます。そのため小さいアプリであれば eager/lazy どちらもサポートされた薄い DI コンテナとして利用できる可能性はあります。 また自前の getInstance(Context) 祭りの代替になるかとも思いましたが、memory leak や排他処理に慣れていない人向けといったところでしょうか。

また SDK 提供側としては App Startup に気を払う必要はない というのが結論です。サポートするとなると App Startup への依存も増えてしまいますし、複雑な初期化が必要なケースは概ね User configurable なことが影響しているように思えます。しかし Content Provider を利用した初期化につてはユーザーに選択を委ねるべきでしょう。例えば ContentProvider での初期化方法を別 artifact で提供する、あるいは manifest merge strategy のサンプルを示すことで opt-in または opt-out 出来るようにすることが求められると思います。

--

--

Jumpei Matsuda

Just a curious software engineer working at a Japanese tech company.