AndroidでServiceを使っていると、タスク削除してもプロセスは生き残る場合がある

先日、お客様のとこで直感に反する挙動に遭遇したためメモ。気付いてしまえば当たり前だが、少々想像力が必要だった。

Androidには最近使ったアプリ(Recents)という機能があり、ここで他のアプリに戻ったり、現在バックグラウンドで保持されているアプリのタスクを削除することができる。
通常、タスク削除されたアプリはプロセスが終了する。

ごく簡単な実装をしてみた。下記のような、Applicationクラスでフラグを管理し、メインの画面が作成された回数を表示するもの。

class MyApp : Application() {

    var count = 0

}
class MainActivity : AppCompatActivity() {

    private val textView by lazy { findViewById<TextView>(R.id.text) }
    private val myApp by lazy { application as MyApp }

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        myApp.count++

        textView.text = "MyApp#count == ${myApp.count}"
    }
}

何も難しいことはない。ご想像どおりタスクを削除する度に新しいプロセスが作成され、下記のような挙動になる。

この時点の実装は下記のとおり。

github.com


では、下記のようにForeground ServiceをApplicationクラスで立ち上げてみる。

class MyApp : Application() {

    var count = 0

    // 新たに下記を実装
    override fun onCreate() {
        super.onCreate()
        startForegroundService(Intent(this, MyService::class.java))
    }

}

Serviceの中身は特段何も無い。

class MyService : Service() {

    override fun onBind(intent: Intent?): IBinder? {
        TODO("Not yet implemented")
    }

}

するとどうだろう。タスク削除後にアプリを起動し直しても、カウントは上がり続ける。

github.com


ここまでの実装過程を見ていれば、何が起きているかは一目瞭然。タスクとして保持すべきものが存在する場合、プロセスは当然同じものが利用されることになるわけだ。
すなわち、Activityのcloseは必ずしも onBackPressed でブロックしていれば完璧ではない。Activityが意図しない形でcloseされかつ、プロセスが終了していない、というパターンが存在することになる。

このようなパターンを考慮して実装するには、下記のような方法が思いつく:

  1. Serviceに android:stopWithTask="true" を設定する
  2. Service#onTaskRemoved を実装し、自身を終了ないし適切に処理する
  3. Activity自身のonDestroyで適切に後処理をする
  4. Application#registerActivityLifecycleCallbacksonActivityStopped を監視する

通常は1の実装で十分だろう。ユーザが明示的にタスクの終了を求めている場合、当該アプリの処理はすべて停止している状態が理想的だ。
実際に、Spotifyなどの著名なアプリのほとんどは音楽が再生中だった場合もユーザの手でタスクが削除された場合はForeground Serviceが停止する仕様となっている。