BroadcastReceiver を unregisterReceiver する際に java.lang.IllegalArgumentException: Receiver not registered が出るので修正する


概要

Android アプリ開発で、(実装がまずくて)BroadcastReceiver の unregister 時に例外が起きたので対応内容を書きます。

java.lang.IllegalArgumentException: Receiver not registered

ContextImpl に register していない BroadcastReceiver インスタンス(null 含む)を unregister しようとするとこの例外が出ます。

Caused by: java.lang.IllegalArgumentException: Receiver not registered: jp.toastkid.music.MediaPlayerService
   at android.app.LoadedApk.forgetReceiverDispatcher(LoadedApk.java:1555)
   at android.app.ContextImpl.unregisterReceiver(ContextImpl.java:1789)
   at android.content.ContextWrapper.unregisterReceiver(ContextWrapper.java:769)
   at jp.toastkid.music.MediaPlayerService.unregisterReceivers(MediaPlayerService.kt:223)

            if (context == null) {
                throw new IllegalStateException("Unbinding Receiver " + r
                        + " from Context that is no longer in use: " + context);
            } else {
                throw new IllegalArgumentException("Receiver not registered: " + r); // <- ここ
            }

例えば、UI生成・表示時にはまだ register せず、 UIイベントが発生したら BroadcastReceiver のインスタンスを register し、
UIが破棄されるタイミングで unregister するようなコードを書いた際に、UIイベントを起こさないままUIを破棄すると起こります。

例:音楽プレイヤー機能の場合

  1. 曲の一覧とコントロールパネルのUIを表示
  2. 再生ボタンが押されたタイミングで BroadcastReceiver を register
  3. 画面を閉じる際に unregister

こういう画面を作った際、2をやらずに3を実行すると例外が発生する可能性があります。
具体的には「音楽聞きたいと思ったけど、やっぱりいいや」と思って、開いた画面をすぐ閉じた際にアプリがクラッシュします。

IllegalArgumentException は RuntimeException のサブクラスで、
クラスの使い方が正しくないことを示す実行時例外ですので、ソフトウェアのリリース前(本来なら実装フェイズ)に直しておくのが良いでしょう。


修正

ContextImpl には BroadcastReceiver に関する問い合わせ系の API がありません。
ある BroadcastReceiver が register されているのかどうかを知る方法はないので、
この例外を回避するには BroadcastReceiver を register したかどうかのフラグを何らかの形で持っておくしかなさそうです。

try-catch で IllegalArgumentException を捕捉して > /dev/null すればクラッシュはしなくなりますし、Q&Aサイトを見るとそういう回答もありました。

    override fun onDestroy() {
        try {
            unregisterReceiver(audioNoisyReceiver)
        } catch (e: IllegalArgumentException) {
            Timber.w(e)
        }

が、それでは問題の解決になっていないので別の方法を考えます。

今回は以下の方法で修正しました。BroadcastReceiver インスタンスが null / non-null で not-registered / registered を管理します。

  1. BroadcastReceiver を Nullable var にして自分で遅延初期化
  2. register する前に初期化する
  3. unregisterReceiver する前に null チェック
  4. onDestroy 等のタイミングでフィールドに null を再代入

1. BroadcastReceiver を Nullable var にして自分で遅延初期化

var かつ Nullable 型で宣言します。

    private var audioNoisyReceiver: BroadcastReceiver? = null

    private var playbackSpeedReceiver: BroadcastReceiver? = null

今回の方法では、BroadcastReceiver は lateinit ではなく普通の var で宣言する必要があります。
4のタイミングで null を再代入できるようにするためです。

また by lazy では null チェックをする際にインスタンスが生成されてしまうので、
register していないインスタンスを unregister しようとして同じ例外が起こります。

2. register する前に初期化する

インスタンスが null の場合のみインスタンスを生成するコードを書きます。

    private fun initializeReceiversIfNeed() {
        if (audioNoisyReceiver == null) {
            audioNoisyReceiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context, intent: Intent) {
                    mediaSession.controller.transportControls.pause()
                }
            }
        }
        if (playbackSpeedReceiver == null) {
            playbackSpeedReceiver = object : BroadcastReceiver() {
                override fun onReceive(context: Context, intent: Intent) {
                    val speed = intent.getFloatExtra(KEY_EXTRA_SPEED, 1f)
                    mediaPlayer.playbackParams = PlaybackParams().setSpeed(speed)
                }
            }
        }
    }

実装ミスが起きやすくて、正直あまりいい方法ではありません。

3. unregisterReceiver する前に null チェック

BroadcastReceiver を register していればインスタンスが null ではなくなっているので unregister します。
インスタンスが null ならその BroadcastReceiver は register されていない(はず)なので、unregister をスキップします。

    override fun onDestroy() {
        if (audioNoisyReceiver != null) {
            unregisterReceiver(audioNoisyReceiver)
        }
        if (playbackSpeedReceiver != null) {
            unregisterReceiver(playbackSpeedReceiver)
        }

4. onDestroy 等のタイミングでフィールドに null を再代入

基本的なことですが、忘れずにやっておきます。

    override fun onDestroy() {
        //...3 のコード

        audioNoisyReceiver = null
        playbackSpeedReceiver = null

これを忘れると、 ContextImpl が再生成された時に Receiver のインスタンスが残留してしまい、3の unregister 処理が実行されてしまいます。
そして、そのタイミングで java.lang.IllegalArgumentException: Receiver not registered が発生します。
再生成された Context には register していないからです。

終わりに

BroadcastReceiver を unregisterReceiver する際に java.lang.IllegalArgumentException: Receiver not registered が出ていたので、
私が修正した時の方法について書きました。よりよいやり方をご存知の方はコメント等でお教えくださいますと幸いです。