Android
AndroidDay 8

onSaveInstanceStateについてちゃんと知る

Android Advent Calendar 2017 8日目の記事です。(遅れてしまいましたすみません...)

Activityの再生成に備えるため、onSaveInstanceStateを利用してViewやFragment、メンバ変数などを保存して復元するということが多々あります。

再生成については、AndroidManifestのconfigChangeで対応するのか、FragmentならsetRetainInstanceを設定するのかなど、画面によって適切な選択をする必要があり、何気なく使っていたのですが仕組みをちゃんと理解すべく書いていこうと思います。

onSaveInstanceStateはいつ呼ばれるか

onPauseの直後に呼ばれる。

12-09 22:57:13.727 D/Lifecycle: onCreate
12-09 22:57:13.879 D/Lifecycle: onStart
12-09 22:57:13.889 D/Lifecycle: onResume
--------------- 画面回転など -----------------------
12-09 22:57:19.375 D/Lifecycle: onPause
12-09 22:57:19.375 D/Lifecycle: onSaveInstanceState
12-09 22:57:19.379 D/Lifecycle: onStop
12-09 22:57:19.379 D/Lifecycle: onDestroy
12-09 22:57:19.434 D/Lifecycle: onCreate
12-09 22:57:19.507 D/Lifecycle: onStart
12-09 22:57:19.508 D/Lifecycle: onRestoreInstanceState
12-09 22:57:19.510 D/Lifecycle: onResume

参考:

onCreateとonRestoreInstanceState

onSavedInstanceStateで保存した値は、onCreateもしくはonRestoreInstanceStateで復元が可能。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        score = savedInstanceState.getInt(STATE_SCORE);
        level = savedInstanceState.getInt(STATE_LEVEL);
    } 
    ...
}

@Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        score = savedInstanceState.getInt(STATE_SCORE);
        level = savedInstanceState.getInt(STATE_LEVEL);
    }

onRestoreInstanceStateはonStartの直後に呼ばれるが、常に呼ばれるわけではなく、保存後にActivityが破棄された次のライフサイクルのタイミングでのみ呼ばれます。
onRestoreInstanceStateが呼ばれる場合には、onCreateも呼ばれているはずでなので、復旧のタイミングを分けたい場合に使い分ける。
onRestoreInstanceStateではnullチェックが不要です。

参考:

Bundleはどこに保存される?

Bundleは前述のタイミングでonSavedInstanceStateが呼ばれた後、アプリのプロセスからSystemServerのプロセスのActivityRecord(各Activityのキャッシュのようなもの)に保存されます。

SystemServerプロセスは前面に出ているアプリや、通話アプリのような永続化プロセスよりも優先度が高くなっているため、相当な異常がない限りはkillされません。

参考:

保存しない方が良いもの

BundleはSystemServerのプロセスのメモリを使用するので、大きいデータを保存するとSystemServerのメモリが大きくなり、メモリ確保のために、バックグラウンドのActivityなどがkillされやすくなってしまいます。

Architecture Components のViewModelがこの問題に対するアプローチとして用意され、Activityよりも長いライフサイクルで存在して状態を保持したりといったことができる。

参考:

どのように保存されるか

ActivityクラスのonSaveInstanceStateは以下のようになっていて、ViewやFragment, Autofillの状態が保存されています。

protected void onSaveInstanceState(Bundle outState) {
    outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());

    outState.putInt(LAST_AUTOFILL_ID, mLastAutofillId);
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    if (mAutoFillResetNeeded) {
        outState.putBoolean(AUTOFILL_RESET_NEEDED, true);
        getAutofillManager().onSaveInstanceState(outState);
    }
    getApplication().dispatchActivitySaveInstanceState(this, outState);
}

mWindow.saveHierachyStateによってViewツリーのルートから状態を保存していきます。

カスタムViewなどを作っているとViewもonSavedInstanceStateやonRestoreInstanceStateを持っていることに気づきますが、ActivityのonSaveInstanceStateが走ったタイミングで、このsaveHierachyStateから段階的に子ViewのonSaveInstanceStateが走るようになっています。

View.javaを見るとsaveHierachyStateはdispatchSaveInstanceStateを呼び出すことで子ViewのonSaveInstanceStateを呼び出してParcalableに状態を保存していきます。

View.java
public void saveHierarchyState(SparseArray<Parcelable> container) {
    dispatchSaveInstanceState(container);
}

protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
        mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
        Parcelable state = onSaveInstanceState();
        if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
            throw new IllegalStateException(
                    "Derived class did not call super.onSaveInstanceState()");
        }
        if (state != null) {
            // Log.i("View", "Freezing #" + Integer.toHexString(mID)
            // + ": " + state);
            container.put(mID, state);
        }
    }
}

@CallSuper
@Nullable protected Parcelable onSaveInstanceState() {
    mPrivateFlags |= PFLAG_SAVE_STATE_CALLED;
    if (mStartActivityRequestWho != null || isAutofilled()
            || mAutofillViewId > LAST_APP_AUTOFILL_ID) {
        BaseSavedState state = new BaseSavedState(AbsSavedState.EMPTY_STATE);

        if (mStartActivityRequestWho != null) {
            state.mSavedData |= BaseSavedState.START_ACTIVITY_REQUESTED_WHO_SAVED;
        }

        if (isAutofilled()) {
            state.mSavedData |= BaseSavedState.IS_AUTOFILLED;
        }

        if (mAutofillViewId > LAST_APP_AUTOFILL_ID) {
            state.mSavedData |= BaseSavedState.AUTOFILL_ID;
        }

        state.mStartActivityRequestWhoSaved = mStartActivityRequestWho;
        state.mIsAutofilled = isAutofilled();
        state.mAutofillViewId = mAutofillViewId;
        return state;
    }
    return BaseSavedState.EMPTY_STATE;
}

再生成によって、Viewがうまく表示されないことがあれば、この辺を追いかければ良いのかなと思います。

参考:

コラム

ActivityRecord.javaをみていたら以下のような変数があり、

Bundle  icicle;         // last saved activity state

Icepickライブラリ由来これなんだなーってなって楽しくなってたら昔はsavedInstanceStateはicicleって名前だったのですね。歴史の勉強になりました。

参考: