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

케니스·2023년 1월 8일
0

안드로이드에서 구성변경(Configuration Change)가 일어날 때 ViewModel 객체는 Activity가 재생성 되더라도 파괴되지 않고 객체가 유지되는 것을 볼 수 있습니다. Activity가 파괴되고 사라질 때 생명주기 마지막 오버라이드 함수인 onDestroy()가 호출되고 이후 호출되는 ViewModel의 onCleared()에 여러가지 해제하는 작업들을 합니다.

하지만 여기서 하나 의문점은 Activity의 종료가 구성변경(Configuration Change)와 같은 재생성에 의한 종료인지 finish()를 통한 종료인지 ViewModel은 어떻게 알 수 있을까요?

만약 단순 구성변경(Configuration Change)로 발생한 Activity의 종료일 때 ViewModel의 onCleared()에서 여러 작업들을 해제한다면 사용자들은 잘못된 정보를 전달 받을 확률이 높습니다. 이 때문에 Activity가 재생성으로 인한 파괴(Destory)라는 것을 파악하고 전파해야 합니다. 어떻게 이런 동작들이 이루어지는지 ViewModel과 Activity의 내부 코드들을 차근차근 살펴보려고 합니다.

아래 그림은 Activity와 ViewModel의 라이프사이클을 순서를 나타내는 이미지입니다.

일단, 가장 먼저 ViewModel에서 onCleared()가 호출되는 함수를 따라가보겠습니다.

public abstract class ViewModel { 
    @SuppressWarnings("WeakerAccess")
    protected void onCleared() {
    }

    @MainThread
    final void clear() {
      ///..
      onCleared();
    }
}

ViewModel에서 onCleared()는 final 함수인 clear()에서 호출이됩니다. 이 clear()는 누가 호출하는지 따라가보면 ViewModelStore라는 객체에서 호출하는 것을 볼 수 있습니다.

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

ViewModelStore는 ViewModel들을 저장하기 위한 클래스입니다. ViewModelStore 인스턴스는 ViewModelStore의 Owner(ViewModelStoreOwner는 일반적으로 Activity, Fragment가 해당 됩니다.)에서 구성변경이 일어나서 파괴되고 재생성되어도 인스턴스를 유지해야합니다. 만약 ViewModelStoreOwner가 완전히 파괴되고 재생성되지 않는다면 clear()를 호출해서 더이상 ViewModel이 사용되지 않는다는 사실을 Activity나 Fragment에 알려야합니다.

ViewModelStore의 클래스 내부를 살펴보면 String Key값과 ViewModel Value를 가지는 해시맵을 가지고 있습니다.

이 클래스는 단순히 해시맵에 putget하기 위한 함수들을 제공합니다. 해시맵에 들어가는 키 값은 기본값으로는 androidx.lifecycle.ViewModelProvider.DefaultKey:modelClass.canonicalName로 생성합니다.

clear()를 보면 해시맵의 Value들을 재귀하여 ViewModel의 clear()를 호출하고 해시맵을 clear() 하는 것을 볼 수 있습니다. 결국 이 ViewModelStore 가 ViewModel의 clear()를 호출하는 것을 알아냈고 이 함수는 ComponentActivity에서 호출되는 것을 확인할 수 있습니다.

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();
              }
          }
      }
  });
}

ComponentActivity에서는 LifecycleEventObserver를 라이프사이클에 관찰자로 등록하고 onDestroy!isChangingConfigurations()가 두개 모두 만족할 경우 ViewModelStoreclear()가 호출되어 ViewModel까지 전달되는 것을 볼 수 있습니다.

public class Activity {
  boolean mChangingConfigurations = false;
  
  public boolean isChangingConfigurations() {
        return mChangingConfigurations;
  }
}

isChangingConfigurations()Activity 클래스의 내부 멤버변수를 리턴해주는 함수입니다. 이제 이 mChangingConfigurationstrue 혹은 false로 변경하는 곳이 어디인지를 찾으면 됩니다.

해답은 ActivityThread에 있었습니다. 이 클래스는 액티비티 매니저의 요청들을 어플리케이션 프로세스에서 메인스레드로 실행할 수 있게 Activity나 Broadcast 그리고 다른 작업들을 스케쥴링하고 실행하는 것을 관리해주는 클래스입니다.

이 클래스에 오버라이드된 activityLocalRelaunch()를 살펴보면 ActivityClientRecord에 기록된 activity에 mChangingConfigurationtrue로 설정해준 것을 확인할 수 있습니다.

@Override
public void handleRelaunchActivity(
  ActivityClientRecord tmp,
  PendingTransactionActions pendingActions
) {
  ActivityClientRecord r = mActivities.get(tmp.token);
  if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handling relaunch of " + r);
  if (r == null) {
    return;
  }

  r.activity.mConfigChangeFlags |= configChanges;
  r.mPreserveWindow = tmp.mPreserveWindow;

  r.activity.mChangingConfigurations = true;
}

여기까지 mChangingConfigurations의 변수를 통해 getViewModelStore().clear()가 실행여부를 밝혀냈습니다. 하지만 여기서 ViewModel의 생성도 결국 Activity에서 이루어지는데 Activity가 완전히 파괴되고 다시 만들어질 때 ViewModel의 인스턴스를 어떻게 유지할지에 대한 궁금증이 생깁니다. 여기에는 ViewModelStoreOwner에 해답이 있습니다.

public interface ViewModelStoreOwner {
    @NonNull
    ViewModelStore getViewModelStore();
}

ViewModelStoreOwner 인터페이스 주석 설명을 보면 이 인터페이스 구현체의 책임은 구성 변경중에도 ViewModelStore를 유지하고 이 범위가 파괴될 때 ViewModelStore.clear()를 호출하는 것이라고 적혀있습니다.

ViewModelStoreOwner의 구현체인 ComponentActivity의 코드를 확인해보겠습니다.

@NonNull
@Override
public ViewModelStore getViewModelStore() {
    if (getApplication() == null) {
        throw new IllegalStateException("Your activity is not yet attached to the "
                + "Application instance. You can't request ViewModel before onCreate call.");
    }    //This is true when invoked for the first time
    if (mViewModelStore == null) {
        NonConfigurationInstances nc =
                (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            // Restore the ViewModelStore from NonConfigurationInstances
            mViewModelStore = nc.viewModelStore;
        }
        if (mViewModelStore == null) {
            mViewModelStore = new ViewModelStore();
        }
    }
    return mViewModelStore;
}

코드를 살펴보면 getLastNonConfigurationInstane()를 호출하여 NoConfigurationInstance라는 클래스를 캐스팅해서 가져온 후 ComponentActivity의 ViewModelStore 멤버변수로서 할당해줍니다. Activity가 구성변경에 의해 재생성될 때 ComponentActivity에 있는 NonConfigurationInstance클래스는 이전 ViewModelState의 인스턴스를 포함하고 있습니다. Activity가 처음 생성될 때 NonConfigurationInstance는 null을 반환하고 그런 경우에는 ViewModelStore를 새로 생성합니다.

조금더 내부 코드를 살펴보겠습니다.

//Activity.java
@Nullable
public Object getLastNonConfigurationInstance() {
  return mLastNonConfigurationInstances != null
    ? mLastNonConfigurationInstances.activity : null;
}

//ComponentActivity.java
@Override
@Nullable
@SuppressWarnings("deprecation")
public final Object onRetainNonConfigurationInstance() {
  // Maintain backward compatibility.
  Object custom = onRetainCustomNonConfigurationInstance();

  ViewModelStore viewModelStore = mViewModelStore;
  if (viewModelStore == null) {
    // No one called getViewModelStore(), so see if there was an existing
    // ViewModelStore from our last NonConfigurationInstance
    NonConfigurationInstances nc =
      (NonConfigurationInstances) getLastNonConfigurationInstance();
    if (nc != null) {
      viewModelStore = nc.viewModelStore;
    }
  }

  if (viewModelStore == null && custom == null) {
    return null;
  }

  NonConfigurationInstances nci = new NonConfigurationInstances();
  nci.custom = custom;
  nci.viewModelStore = viewModelStore;
  return nci;
}

Activity가 구성변경이 발생하여 재생성될 때 ComponentActivity 의 onRetainNonConfigurationInstance 함수가 호출되고 NonConfigurationInstance(ComponentActivity.java)의 객체를getLastNonConfigurationInstance()를 호출하여 Activity의 mLastNonConfigurationInstances.activity를 반환받는데 이 객체는 안드로이드 시스템에서 재생성된 후의 액티비티를 전달한 것입니다.

Activity가 재생성후에 Activity.java클래스에서 mLastNonConfigurationInstances 객체는 attach()에서 lastNonConfigurationInstances를 전달받아 mLastNonConfigurationInstances에 할당합니다.

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
            IBinder shareableActivityToken) {
}

마무리

지금까지 ViewModel이 구성변경에도 인스턴스를 유지하는 방법과 onCleared()의 호출 조건을 알아보았습니다.

ViewModel의 인스턴스 유지 과정을 살펴보니 Activity에 대한 코드의 이해도도 증가되고 ViewModel의 구성방식들도 이해하게되어 좋았습니다. 안드로이드 개발자라면 ViewModel을 잘 활용하여 구성변경에도 데이터를 유지하여 사용자 경험을 증진시키는게 하나의 임무라고 생각합니다 😄 긴글 봐주셔서 감사합니다.

레퍼런스

profile
노력하는 개발자입니다.

0개의 댓글