[There is No Magic] ViewModel이 구성변경에 어떻게 살아남는지 알아보자!

0
post-thumbnail

이번 이직을 준비하게 되면서 당연시하게 코드를 작성했던 것들을 되돌아 보게 되었습니다.
굳이 캡슐화가 되어있는것을 내부를 들여다봐야하나? 라는 고민을 했었는데, 공부의 비용을 줄이기 위해 필요 없다고 단정지었을지도 모릅니다.

There is No Magic

어떤것에든 마법은 없으니 어떻게 동작하는지 확인해 봅시다.

일단 문제 제기


Android에서 화면을 전환하거나 할때 onDestroy() 호출한 뒤에 onCreate()가 됩니다.

onDestory가 됨에도 어떻게 ViewModel이 살아있을까요?

ViewModel Provider

class ScrollActivity : AppCompatActivity() {

 val viewModel by lazy { ViewModelProvider(this).get(ScrollViewModel::class.java) }
 }

가장 기본적으로 viewModel 인스턴스를 만들때 사용하는 코드입니다.

ViewModel Provider 생성자는 파라미터로 ViewModelStoreOwnerfactory를 받습니다.

우리는 일반적으로 첫번째 constructer 를 사용하여 ViewmodelProvieder를 만듭니다.

여기서 받는 파라미터는 ViewModelStoreOwner인것을 알수 있습니다.

그렇다면 AppCompatActivity가 ViewModelStoreOwner의 구현체라는 것인데요.

한번 확인해볼까요?

정확히는 AppCompatActivity의 조상 클래스인 ComponentActivity에서 찾아볼 수 있습니다.

우리는 Interface인 ViewmodelStoreOwner에서 만들어진 getViewModelStore()가 어디에서 쓰이는지 확인해보고자 합니다.

우리는 ComponentActivity의 245줄에서 현재 LifeCycle이 OnDestory이고, isChangingConfigrurations이 false 일때 ViewModelStore가 정리되는것을 알수 있습니다.

여기서 isChangingConfigrurations는 언제 true이고, ViewModelStore는 무엇일까요?

        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
                    //구성변경이 아닐경우, clear()함수를 호출
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });

isChangingConfigrurations


isChangingConfigrurations는 내부적으로 감춰져있기 때문에 cs.android로 확인 하셔야 합니다.


isChangingConfigrurations는 mChagingConfigurations의 값을 Return하는 메소드입니다.

그렇다면 중요한 것은 mChagingConfigurations의 값이 언제 변하는가겠죠?

mChagingConfigurations이 값이 변하는 순간은 ActivityThread#handleRelaunchActivity 에서 이루어 집니다.

해석을 해보면 ActivityThread#handleRelaunchActivity()가 호출될떄 ActivityClientRecord에서 token으로 기존에 저장되어있던 정보를 가져옵니다.
또한 ActivityClientRecord는 ActivityThread에서 Map으로 관리 됩니다.

그리고 handleRelaunchActivity()execute() 에서 실행이 됩니다.

이 이후로 메소드를 추적하기가 어려워져서 멈췄습니다만, 우리의 초점은 execute()가 언제 실행되는가? 입니다.

정답은 Trace를 통해 얻을수 있었습니다.

일반적으로 Activity가 호출되면 activityStart라는 trace()가 남습니다.

하지만 구성변경이 일어난다면, activityRestart가 찍힙니다.

물론 Method Trace를 통해 어디에서 호출되는지 확인해봤습니다.

하지만 우리는 이를 통해, Acitivity가 구성변경을 감지하여 Restart하는 분기가 있다는것을 알았으며,
ActivityThread# handleRelaunchActivity에서 mChagingConfigurations을 변경함에 따라 ViewModelStore를 clear하지 않는 것을 깨달았습니다.
또한 ActivityClientRecord에서 값을 꺼내오는것을 알았습니다.

ViewModelStore

ViewModelStore는 ViewModel을 저장하는 클래스입니다.

HashMap을 이용해 String값을 통해 Viewmodel을 관리하고 있으며, clear() 호출시 해당 mMap을 지워버립니다.

여기서 put() 이 실행되는 시점은 get()을 할때 실행이 되는데요.

  val viewModel by lazy { ViewModelProvider(this).get(ScrollViewModel::class.java) }

이미 ViewModel이 있다면 첫번째 가정문에서 반환이 되고, 없을경우 mViewModelStoreput()하게됩니다.

이를 통해 우리는 ViewModelStore의 역할은 ViewModel을 저장하고, ViewModel을 꺼내는것을 알게되었습니다.
그리고 다음번에 이전 Activity에서 어떻게 ViewModelStore를 건네받는지 살펴보겠습니다.

Configuration Change

Configuration Change가 일어날때 ensureViewModel() 메서드에서 ViewModelStore를 가져오게되는데요.
ensureViewModel()이 주제의 가장 핵심이 되는 코드 입니다.

ensureViewModel()에서 사용되는 getLastNonConfigurationInstance()에서는 onRetainNonConfigurationInstance()에서 호출한 값을 반환한다고 되어 있습니다.

여기서 주석을 보면 구성변경이 일어났을때, 이곳에서 새 인스턴스가 반환된다고 되어있어서 한번 추적하기로 했습니다.

  /**
     * Called by the system, as part of destroying an
     * activity due to a configuration change, when it is known that a new
     * instance will immediately be created for the new configuration.  You
     * can return any object you like here, including the activity instance
     * itself, which can later be retrieved by calling
     * {@link #getLastNonConfigurationInstance()} in the new activity
     * instance.
     *
**/
    public Object onRetainNonConfigurationInstance() {
        return null;
    }

그리고 이것을 호출하는 retainNonConfigurationInstances()
Caller인 performDestroyActivity에서 ActivityRecord 가 객체인 파라미터 r에 넣어집니다.

    NonConfigurationInstances retainNonConfigurationInstances() {
        Object activity = onRetainNonConfigurationInstance();
        HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
        FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();

        // We're already stopped but we've been asked to retain.
        // Our fragments are taken care of but we need to mark the loaders for retention.
        // In order to do this correctly we need to restart the loaders first before
        // handing them off to the next activity.
        mFragments.doLoaderStart();
        mFragments.doLoaderStop(true);
        ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();

        if (activity == null && children == null && fragments == null && loaders == null
                && mVoiceInteractor == null) {
            return null;
        }

        NonConfigurationInstances nci = new NonConfigurationInstances();
        nci.activity = activity;
        nci.children = children;
        nci.fragments = fragments;
        nci.loaders = loaders;
        if (mVoiceInteractor != null) {
            mVoiceInteractor.retainInstance();
            nci.voiceInteractor = mVoiceInteractor;
        }
        return nci;
    }
  void performDestroyActivity(ActivityClientRecord r, boolean finishing,
            int configChanges, boolean getNonConfigInstance, String reason) {
        Class<? extends Activity> activityClass = null;
        if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
        activityClass = r.activity.getClass();
        r.activity.mConfigChangeFlags |= configChanges;
        if (finishing) {
            r.activity.mFinished = true;
        }

        performPauseActivityIfNeeded(r, "destroy");

        if (!r.stopped) {
            callActivityOnStop(r, false /* saveState */, "destroy");
        }
        if (getNonConfigInstance) {
            try {
                r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();

결과적으로 performDestoryActivity가 일어날때, ActivityClientRecordlastNonConfigurationInstance를 저장하는것을 확인 할수 있습니다.

그림으로 확인하자

아래는 구성변경이 일어났을때 ViewmodelStore 를 어떻게 가져오는지 정리한 다이어그램입니다.

아래는 구성변경이 일어났을때 현재 ViewModelStore를 어떻게 저장하는지 정리한 다이어그램입니다.


참고

Android ViewModel이 내부적으로 작동하여 구성 변경에도 살아남는 방법

ViewModel이 화면회전에도 데이터를 유지할 수 있는 이유

profile
쉽게 가르칠수 있도록 노력하자

0개의 댓글