nashcft's blog

時々何か書く。

Jetpack ViewModel はいつ clear されるか

tl;dr

onPause
↓
onStop
↓
(onDestroyView)
↓
onCleared
↓
onDestroy

Jetpack ViewModel のドキュメント には Activity と紐つけた際の owner の lifecycle event と ViewModel の生存期間の関係についての図が載っている。

f:id:nashcft:20210307164204p:plain
https://developer.android.com/topic/libraries/architecture/viewmodel#lifecycle より引用

この図では ViewModel#onCleared の実行は Activity#onDestroy 実行より後のタイミングで起こるように描かれており、そのイメージから ViewModel 登場以来ずっと ViewModel#onClearedonDestroy より後に実行されるものと思っていたが、現在のコードを読んでみると実際はそうではなく onDestroy() の前に実行されることに気がついた。この記事では ViewModel 破棄の実際の流れを追っていく。適当な ViewModel の onCleared() が呼ばれる時の stacktrace を見ればわかる話なので、それで十分という人はここで読むのをやめて実際にそちらを見にいくと良い。

この記事の内容は以下の version で動作を確認している:

  • androidx.activity: 1.0.0-alpha01 および 1.2.0
  • androidx.fragment: 1.1.0-alpha01 および 1.3.0

確認に使ったコードの repository:

github.com

ViewModel が破棄される仕組み

ViewModel は生成時に ViewModelStore に登録される。この ViewModelStore は ViewModel を HashMap で保持しており、 ViewModelStore#clear が呼ばれた時に保持している ViewModel の clear() を実行し、保持している ViewModel を破棄する。 ViewModel#onClearedViewModel#clear 内で実行される。保持している ViewModel の clear が終わったら、 map を clear して参照を切る。

https://cs.android.com/androidx/platform/frameworks/support/+/2e1f4a8ef6eb4252735ed377398bc56f310406c0:lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStore.java

https://cs.android.com/androidx/platform/frameworks/support/+/636466bf953fa67e04ba56a6f1c04a8a14220595:lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java

ViewModelStore#clear が呼ばれる流れ: Activity 編

Activity は ViewModelStore#clear が呼ばれている箇所から遡って辿るとわかりやすいのでそのように説明する。

Activity が持つ ViewModelStoreclear() は、 androidx.activity.ComponentActivity (以下 ComponentActivity*1 ) の constructor で自身の Lifecycle に対して登録している LifecycleEventObserver の中の1つで呼ばれている。

    public ComponentActivity() {
        // ...
        getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    // Clear out the available context
                    mContextAwareHelper.clearAvailableContext();
                    // And clear the ViewModelStore
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });
        // ...
    }

https://cs.android.com/androidx/platform/frameworks/support/+/6f45d365ff22e50c82467ce0d6208433d5ec19d1:activity/activity/src/main/java/androidx/activity/ComponentActivity.java;l=238-251

上に該当する箇所を抜粋したが、 Lifecycle.Event.ON_DESTROY が通知された際、configuration change によるものではない場合に ViewModelStore#clear が実行されることがわかる。

この ComponentActivity (を継承した Activity) の Lifecycle に対して lifecycle event を送るのは誰かというと、 ReportFragment という event 通知用の内部 class である。

https://cs.android.com/androidx/platform/frameworks/support/+/ec4a052c29df9691bd35a936541e6ab46b97514b:lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.java

ReportFragment は、static method の injectIfNeededIn() で渡した Activity の、 Android Framework の方の FragmentManager にこの Fragment を add することで、 Activity の Lifecycle*2 に対して lifecycle event を通知する機能を持っている。実際の通知の仕組みは API level 29 以上とそれ未満とで異なり、API level 29 以上では通知機能を実装した Application.ActivityLifecycleCallbacks を Activity に登録して、 29 未満では ReportFragment の各 lifecycle の callback method を通して通知を実現している。

    public static void injectIfNeededIn(Activity activity) {
        if (Build.VERSION.SDK_INT >= 29) {
            // On API 29+, we can register for the correct Lifecycle callbacks directly
            LifecycleCallbacks.registerIn(activity);
        }
        // Prior to API 29 and to maintain compatibility with older versions of
        // ProcessLifecycleOwner (which may not be updated when lifecycle-runtime is updated and
        // need to support activities that don't extend from FragmentActivity from support lib),
        // use a framework fragment to get the correct timing of Lifecycle events
        android.app.FragmentManager manager = activity.getFragmentManager();
        if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
            manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
            // Hopefully, we are the first to make a transaction.
            manager.executePendingTransactions();
        }
    }

    // ...

    @RequiresApi(29)
    static class LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
        // ...
    }

この ReportFragmentLifecycle.Event.ON_DESTROY を Activity に通知するのは ActivityLifecycleCallbacks#onActivityPreDestroyed または ReportFragment#onDestroy である。片方はそのままな名前をしているのでわかったようなものだが、これらが実行されるのは android.app.ActivityperformDestroy 中、前者は dispatchActivityPreDestroyed() で、後者は mFragments.dispatchDestroy() の先であり、 onDestroy の前となる。

    final void performDestroy() {
        dispatchActivityPreDestroyed();  // 筆者註: API >= 29 の場合はここで ViewModel が破棄される
        mDestroyed = true;
        mWindow.destroy();
        mFragments.dispatchDestroy();  // 筆者註: API < 29 の場合はここで ViewModel が破棄される
        onDestroy();
        EventLogTags.writeWmOnDestroyCalled(mIdent, getComponentName().getClassName(),
                "performDestroy");
        mFragments.doLoaderDestroy();
        if (mVoiceInteractor != null) {
            mVoiceInteractor.detachActivity();
        }
        dispatchActivityPostDestroyed();
    }

https://cs.android.com/android/_/android/platform/frameworks/base/+/eff0b0673c1604477b815cf1bcf001b1df56ef75:core/java/android/app/Activity.java;l=8240-8253

ComponentActivityonCreateReportFragment#injectIfNeededIn に自身を渡しており、それによって上記のように Lifecycle に対して event 通知を受け、 finish の際に ON_DESTROY を受け取ったところで ViewModel を破棄している、という流れになる。

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        // ...
        super.onCreate(savedInstanceState);
        mActivityResultRegistry.onRestoreInstanceState(savedInstanceState);
        ReportFragment.injectIfNeededIn(this);
        // ...
    }

https://cs.android.com/androidx/platform/frameworks/support/+/6f45d365ff22e50c82467ce0d6208433d5ec19d1:activity/activity/src/main/java/androidx/activity/ComponentActivity.java;l=288-300

ViewModelStore#clear が呼ばれる流れ: Fragment 編

androidx.fragment.app.Fragment の場合は起点はわかりやすいが辿る経路が長いのと実際に呼ばれてる場所がちょっと不思議なのでとりあえずスタート地点から。

Fragment に紐つく ViewModelStoreclear() が呼ばれるきっかけになるのは FragmentActivity#onDestroy, つまり Fragment#onDestroy が呼ばれるのと同じである。そこから FragmentController -> FragmentManager -> SpecialEffectsController -> FragmentStateManager と進んでいって、この FragmentStateManager#moveToExpectedStateFragmentStateManager#destroy に入る。この経路自体は特に重要ではないので適当に端折ったが、実際に追ってみたい場合は stacktrace を出してそれに沿って読んでみると良い。

この FragmentStateManager#destroy の中、その先で Fragment#onDestroy が実行される mFragment.performDestroy() の直前にある mFragmentStore.getNonConfig().clearNonConfigState(mFragment), ここで Fragment に紐つく ViewModelStore の clear が行われる。

    void destroy() {
        // ...
        boolean beingRemoved = mFragment.mRemoving && !mFragment.isInBackStack();
        boolean shouldDestroy = beingRemoved
                || mFragmentStore.getNonConfig().shouldDestroy(mFragment);
        if (shouldDestroy) {
            // ...
            if (beingRemoved || shouldClear) {
                mFragmentStore.getNonConfig().clearNonConfigState(mFragment);
            }
            mFragment.performDestroy();
            // ...
        } else {
            // ...
        }
    }

https://cs.android.com/androidx/platform/frameworks/support/+/cfc31d1abe7bd6c064a8780add0033853b1a3c91:fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java;l=747-750

ここで NonConfig と呼ばれる FragmentManagerViewModel は、端的に言うと Activity の ViewModelStore に登録される ViewModel で、それぞれの Fragment に対応する ViewModelStore の管理や Activity の再生成時に retain される Fragment の保持といった役割を持っている。 clearNonConfigState() では、与えられた Fragment に対応する ViewModelStore と子の NonConfig の clear および管理下からの削除を行っている。

    void clearNonConfigState(@NonNull Fragment f) {
        if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
            Log.d(TAG, "Clearing non-config state for " + f);
        }
        // Clear and remove the Fragment's child non config state
        FragmentManagerViewModel childNonConfig = mChildNonConfigs.get(f.mWho);
        if (childNonConfig != null) {
            childNonConfig.onCleared();
            mChildNonConfigs.remove(f.mWho);
        }
        // Clear and remove the Fragment's ViewModelStore
        ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
        if (viewModelStore != null) {
            viewModelStore.clear();
            mViewModelStores.remove(f.mWho);
        }
    }

https://cs.android.com/androidx/platform/frameworks/support/+/a48dae920b797f8e4c14aa9c74277fdf85137e76:fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerViewModel.java;l=186-202

Fragment まわりのコードはややこしくて読むのに苦労するが、とりあえず FragmentStateManager#destroy の中で Fragment#performDestroy (= Fragment#onDestroy の実行) の直前に紐ついてる ViewModel の clear が行われていることがわかった。

雑多な話題

なんでドキュメントは嘘ついてるの?

実装当初は強ち嘘というわけではなかった。もともと ViewModelStore#clearFragmentActivity, Fragment それぞれの onDestroy の中で実行されていて、super#onDestroy を override した method の最後で呼んだ場合は呼ばれる順番として onDestroy -> onCleared と捉えることもできた。

いつから現在の挙動に変わったの?

動作確認している version で察しがつく人もいると思うが、 release version では AndroidX として一番最初である activity:1.0.0-alpha01 / fragment:1.1.0-alpha01 からである。 Commit のレベルでは 2018-10-23 に commit された以下の2つ:

これを挙動の変更と言えるかは難しくて、 Activity のCLにつけられたコメントを見ると、 super.onDestroy() を呼ぶタイミングがアプリ開発者に委ねられてしまっていることを問題視していて、現在のような挙動を常にすることを保証したかったように読み取れる。

Through discussions with adamp@, we wanted to make a firm decision on exactly when this will run as the final LifecycleObserver going out. onDestroy() does not offer the same guarantee since developers can call it at any point in their onDestroy() method.

実際 onPause ~ onDestroy の super method を最初に呼ぶ流派と最後に呼ぶ流派があるようだし*3、またうっかり super.onDestroy() を呼び忘れることによって clear されないということも起こりうるので、フレームワークの仕組みとしては現在の形の方が正しいのかもしれない。そうするとやっぱりドキュメントの図は嘘をついてることになってしまうので直すべきでは? となるが...

書いてて気づいたが Fragment の destroy と Fragment に紐つく ViewModel の clear はまだ FragmentActivity#onDestroy に依存しているので、 super.onDestroy() の呼び忘れで実行されないということがありうる。呼ばれる順序は常に同じにすることができたけど、そもそも呼ばれないという問題はまだ解消できていないようだ。

viewModelScope について

viewModelScopeViewModel#clear 実行時、 ViewModel#onCleared が呼ばれる前に cancel される。

https://cs.android.com/androidx/platform/frameworks/support/+/20d4085eedeb1f83964a802cf6286bfbdba57498:lifecycle/lifecycle-viewmodel-ktx/src/main/java/androidx/lifecycle/ViewModel.kt

FragmentActivityLifecycleRegistry には2回 ON_DESTROY が通知されるけど

上で説明した ComponentActivity 由来の経路と FragmentActivity#onDestroy で実行されている mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) で計2回 ON_DESTROYLifecycleRegistry に送られている。

1回目の event 通知時の ViewModelStore#clear で保持していた ViewModel は全て切り離されるので ViewModel#onCleared が2回呼ばれるようなことはない。

まとめ

Activity (API >=29)

ViewModel cleared at com.github.nashcft.app.ActivityViewModel.onCleared(ActivityViewModel.kt:23)
        at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
        at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
        at androidx.activity.ComponentActivity$4.onStateChanged(ComponentActivity.java:261)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
        at androidx.lifecycle.LifecycleRegistry.backwardPass(LifecycleRegistry.java:284)
        at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:302)
        at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
        at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:68)
        at androidx.lifecycle.ReportFragment$LifecycleCallbacks.onActivityPreDestroyed(ReportFragment.java:224)
        at android.app.Activity.dispatchActivityPreDestroyed(Activity.java:1498)
        at android.app.Activity.performDestroy(Activity.java:8241)
        at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1344)
        at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:5096)
        at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:5140)
        at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:44)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

Activity (API < 29)

ViewModel cleared at com.github.nashcft.app.ActivityViewModel.onCleared(ActivityViewModel.kt:23)
        at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
        at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
        at androidx.activity.ComponentActivity$4.onStateChanged(ComponentActivity.java:261)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
        at androidx.lifecycle.LifecycleRegistry.backwardPass(LifecycleRegistry.java:284)
        at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:302)
        at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
        at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:68)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:144)
        at androidx.lifecycle.ReportFragment.onDestroy(ReportFragment.java:134)
        at android.app.Fragment.performDestroy(Fragment.java:2782)
        at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1451)
        at android.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1576)
        at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1637)
        at android.app.FragmentManagerImpl.dispatchMoveToState(FragmentManager.java:3046)
        at android.app.FragmentManagerImpl.dispatchDestroy(FragmentManager.java:3026)
        at android.app.FragmentController.dispatchDestroy(FragmentController.java:248)
        at android.app.Activity.performDestroy(Activity.java:7394)
        at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1306)
        at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4443)
        at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:4476)
        at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:39)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Fragment

ViewModel cleared at com.github.nashcft.app.FragmentViewModel.onCleared(FragmentViewModel.kt:23)
        at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
        at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
        at androidx.fragment.app.FragmentManagerViewModel.clearNonConfigState(FragmentManagerViewModel.java:199)
        at androidx.fragment.app.FragmentStateManager.destroy(FragmentStateManager.java:769)
        at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:350)
        at androidx.fragment.app.SpecialEffectsController$FragmentStateManagerOperation.complete(SpecialEffectsController.java:742)
        at androidx.fragment.app.SpecialEffectsController$Operation.cancel(SpecialEffectsController.java:594)
        at androidx.fragment.app.SpecialEffectsController.forceCompleteAllOperations(SpecialEffectsController.java:329)
        at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3116)
        at androidx.fragment.app.FragmentManager.dispatchDestroy(FragmentManager.java:3091)
        at androidx.fragment.app.FragmentController.dispatchDestroy(FragmentController.java:334)
        at androidx.fragment.app.FragmentActivity.onDestroy(FragmentActivity.java:322)
        at com.github.nashcft.app.MyFragmentActivity.onDestroy(MyFragmentActivity.kt:43)
        at android.app.Activity.performDestroy(Activity.java:8245)
        at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1344)
        at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:5096)
        at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:5140)
        at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:44)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

*1:余談だが androidx.core にも ComponentActivity が存在し、 androidx.activity.ComponentActivity の直接の親 class となっている

*2:実際には getLifecycle() で取得できる Lifecycle が LifecycleRegistry である必要がある

*3:要出典