Mokelab Blog

#55 AndroidXのApp Startupを使ってみる

Androidのコンテンツプロバイダー(ContentProvider)は、アプリのプロセスが起動した際、ApplicationのonCreate()より前にインスタンスが作られます。

近年では、この性質を利用してプロセスが起動したタイミングで初期化を行うライブラリが増えてきました。

「Applicationクラスで初期化メソッドを呼んで」とお願いしても忘れる開発者はたくさんいます。 コンテンツプロバイダーはライブラリとして追加した際、AndroidManifest.xmlに自動で追加できるので初期化忘れを防止することができます。

何が問題か

便利そうなコンテンツプロバイダーによる初期化ですが、問題となりそうな点が3つあります。

ライブラリを追加しただけでコンテンツプロバイダーが追加される

初期化処理が自動で追加される反面、追加されるコンテンツプロバイダー数が増えていくと、プロセスが落ちている状態からのアプリ起動がどんどん遅くなってしまいます。

簡単な例で確認してみましょう。次のように初期化を担うコンテンツプロバイダーが3つあったとします。

class MyProvider1: ContentProvider() {
  override fun onCreate(): Boolean {
      println("onCreate: MyProvider1")
      Thread.sleep(1000)
      return true
  }
  ...
}

class MyProvider2: ContentProvider() {
  override fun onCreate(): Boolean {
      println("onCreate: MyProvider2")
      Thread.sleep(2000)
      return true
  }
  ...
}

class MyProvider3: ContentProvider() {
  override fun onCreate(): Boolean {
      println("onCreate: MyProvider3")
      Thread.sleep(3000)
      return true
  }
  ...
}

初期化の順序を確認するため、Applicationクラスも作ります。

class MyApplication : Application() {
  override fun onCreate() {
      super.onCreate()
      println("onCreate: MyApplication")
      Thread.sleep(1000)
  }
}

そしてAndroidManifest.xmlに登録します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.mokelab.demo.startup">

  <application
    ...
    android:name=".MyApplication">
    <activity android:name=".MainActivity">
    </activity>

    <provider
      android:authorities="${applicationId}.provider1"
      android:name=".MyProvider1"
      android:exported="false"/>
    <provider
      android:authorities="${applicationId}.provider2"
      android:name=".MyProvider2"
      android:exported="false"/>
    <provider
      android:authorities="${applicationId}.provider3"
      android:name=".MyProvider3"
      android:exported="false"/>
  </application>
</manifest>

このアプリを実行してみます。ログが出力されている時刻に注目してください。

2021-01-11 17:50:14.598 6762-6762/com.mokelab.demo.startup I/System.out: onCreate: MyProvider1
2021-01-11 17:50:15.600 6762-6762/com.mokelab.demo.startup I/System.out: onCreate: MyProvider2
2021-01-11 17:50:17.603 6762-6762/com.mokelab.demo.startup I/System.out: onCreate: MyProvider3
2021-01-11 17:50:20.610 6762-6762/com.mokelab.demo.startup I/System.out: onCreate: MyApplication
2021-01-11 17:50:21.655 6762-6762/com.mokelab.demo.startup I/System.out: onCreate: MainActivity

MainActivityonCreate()が実行されるまで7秒もかかっています。初期化に数秒かかるコンテンツプロバイダーは無いと思いますが、 「初期化に使われるコンテンツプロバイダーが増えるとアプリ起動時間が長くなる」ことが体感できたかと思います。

初期化順序の制御が難しい

現時点でのManifest Mergerはbuild.gradleのdependenciesの上から順にマージしているようです。つまり、

dependencies {
  ...
  implementation project(":libs:mylibrary1")
  implementation project(":libs:mylibrary2")
  implementation project(":libs:mylibrary3")  
}

dependencies {
  ...
  implementation project(":libs:mylibrary1")
  implementation project(":libs:mylibrary3")
  implementation project(":libs:mylibrary2")  
}

で、AndroidManifest.xmlに追加される<provider>の順序が変わってしまうことになります。 もし、mylibrary3の初期化を行うために、mylibrary2の初期化が必要である場合、後者はエラーになってしまいます。 ここでの例はシンプルなものですが、ライブラリ間の依存関係が複雑になっていた場合、コンテンツプロバイダー間の初期化順を制御するのは困難でしょう。

初期化方法を変える時は一苦労

コンテンツプロバイダーによる初期化は多くの場合、デフォルト値での初期化が実施されます。この初期化方法を変更したい場合はライブラリによって対応がまちまちです。

WorkManagerの場合、まず次のようにコンテンツプロバイダーをAndroidManifest.xmlから削除します。

<provider
  android:name="androidx.work.impl.WorkManagerInitializer"
  android:authorities="${applicationId}.workmanager-init"
  tools:node="remove" />

そしてApplicationクラスを作り、Configuration.Provider を実装します(このコードは公式ドキュメントにあるものです)。

class MyApplication() : Application(), Configuration.Provider {
  override fun getWorkManagerConfiguration() =
    Configuration.Builder()
      .setMinimumLoggingLevel(android.util.Log.INFO)
      .build()
}

App Startup

コンテンツプロバイダーを使った初期化の問題を解決し、アプリ開発者に初期化の制御権を戻すためのライブラリとして、JetpackにApp Startupというライブラリが追加されました。 初期化の制御権をライブラリ側ではなくアプリ開発者側に移動させるためのライブラリで、このライブラリを使ったからといってアプリの起動が自動で早くなることは無い点に注意しましょう。公式ドキュメントはこちらです。

アプリにApp Startupを追加するには、いつものようにdependenciesにライブラリを追加します。

dependencies {
  implementation 'androidx.startup:startup-runtime:1.0.0'
}

そして、初期化処理を行うためのクラスを作ります。Initializer<T>を継承させます。

class MyInitializer1: Initializer<Boolean> {
  override fun create(context: Context): Boolean {
      println("create: MyInitializer1")
      Thread.sleep(1000)
      return true
  }

  override fun dependencies(): List<Class<out Initializer<*>>> {
      return emptyList()
  }
}

create()内で初期化処理を行います。そして初期化後のオブジェクトが存在する場合、それを返すこともできます。 初期化がstaticメソッドのみなライブラリの場合、返すオブジェクトが存在しないのでBooleanの値など適当なものを返しておきます。

dependencies()では、事前に初期化しておきたい別のInitializerがあればリストで指定します。無い場合はemptyList()を返しておきます。

Initializerを作ったら、AndroidManifest.xmlに登録します。

<provider
  android:name="androidx.startup.InitializationProvider"
  android:authorities="${applicationId}.androidx-startup"
  tools:node="merge">
  <meta-data android:name="com.mokelab.demo.startup.MyInitializer1"
      android:value="androidx.startup"/>
</provider>

meta-dataでInitializerを指定します。nameとvalueが逆のようにも見えますが、これは複数のInitializerを登録できるようにするためのようです。また、providerにはtools:node="merge"をつけておきます。 これはFlavorなどで追加されたandroidx.startup.InitializationProviderをひとつにまとめるために使います。

最後に、ライブラリが追加する<provider>を、tools:node="remove"を使ってAndroidManifest.xmlから抜きます。例えばMyInitializer1がMyProvider1〜3の初期化を担っていた場合、次のように記述します。これを忘れると初期化が2度実行されることとなり、ライブラリによってはクラッシュの原因となります。

<provider
  android:authorities="${applicationId}.provider1"
  android:name=".MyProvider1"
  tools:node="remove"/>
<provider
  android:authorities="${applicationId}.provider2"
  android:name=".MyProvider2"
  tools:node="remove"/>
<provider
  android:authorities="${applicationId}.provider3"
  android:name=".MyProvider3"
  tools:node="remove"/>

これで、ライブラリが追加したコンテンツプロバイダーによる初期化を抑制し、Initializerで意図した順序で初期化を行うことができるようになります。

Initializerのcreateで返したオブジェクトはアプリ内で次のように取得することができます。

val obj = AppInitializer
  .getInstance(applicationContext)
  .initializeComponent(MyInitializer1::class.java)

遅延初期化

Initializerで作成したオブジェクトの取得メソッドがinitializeComponent()となっていました。このメソッドは「もし該当Initializerが初期化されていなければ、初期化し結果を返す」という動きをします(初期化済みだったらキャッシュを返します)。 App Startupを使うと、初期化をこのinitializeComponent()まで遅延させることができます。ようやく、アプリの起動が早くなりそうな要素がでてきました。

遅延初期化を行うには、該当のInitializerに対し、次のようにtools:node="remove"をつけます(コードを読むと、meta-data自体を抜いてしまってもよさそう)。

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="merge">
    <meta-data android:name="com.mokelab.demo.startup.MyInitializer1"
        tools:node="remove"/>
  </provider>

そして、初期化が必要な場面でinitializeComponent()を呼びます。

AppInitializer
  .getInstance(applicationContext)
  .initializeComponent(MyInitializer1::class.java)

まとめ

JetpackのApp Startupがなぜ必要なのかと、使い方を説明しました。公式ブログにもある通り、起動にかかる時間を測定しながらこのライブラリを使うかどうか判断しましょう。

本サイトではサービス向上のため、Google Analyticsを導入しています。分析にはCookieを利用しています。