
[미리 읽고오면 좋은 글]

by viewModels호출하면 AAC ViewModel을 생성한다. 이렇게 생성된 ViewModel은 앱이 구성변경될 때, 데이터를 메모리 캐싱함으로써 보존해한다.
또한 이와 함께 쓰이는 SavedStateHandle의 경우, Parcelize로 직렬화된 데이터(Bundle사용)를 저장함으로써 앱의 비정상 종료를 대비한 데이터를 보존할 수 있게해준다. 또한 SavedStateHandle은 이전 Activity, Fragment, Composable함수들의 argument를 Bundle에 담아 화면간 데이터 전송 또한 가능하게 하는데, 이러한 원리들이 어떻게 가능한 것인지 내부 소스코드를 까보며 알아보려 한다.

이해를 위해 알아야할 선행 배경지식먼저 설명하고자 한다. (Activity나, Fragment나 구조는 비슷하므로 Fragment를 기준으로 설명한다.) Activity와 Fragment는 HasDefaultViewModelProviderFactory, ViewModelStoreOwner, SavedStateRegistryOwner의 구현체로써, AAC ViewModel이 SavedStateHandle의 Bundle을 사용한 상태관리를 가능하게 한다.


첫 번째로, defaultViewModelProviderFactory은 ViewModelProvider.Factory타입의 프로퍼티로 내부적으로 create() 메서드 호출을 통해 AAC ViewModel 생성에 핵심적인 역할을 한다. 이를 타고 들어가면 create메서드를 확인할 수 있다.

두 번째로, defaultViewModelCreationExtras는 CreationExtras타입의 프로퍼티로, 추후 AAC ViewModel생성 시 Bundle타입의 argument를 넘겨주는 역할을 한다.
ViewModelStoreOwner는 val viewModelStore: ViewModelStore 1개의 프로퍼티만을 포함한 인터페이스이다.

ViewModelStore를 타고 들어가보면 확인할 수 있듯, AAC ViewModel을 내부적 Map자료구조 형태로 보유하고 있는 객체이다.

SavedStateRegistryOwner도 ViewModelStoreOwner와 마찬가지로 val savedStateRegistry: SavedStateRegistry라는 1개의 프로퍼티를 포함한 인터페이스이다.

마찬가지로 SavedStateRegistry를 타고 들어가보면 Map자료구조 형태로 SavedStateProvider를 보유하는데, 이는 추후, 프로세스 비정상 종료/복구에 사용되는 SAM 인터페이스이다.


by viewModel<HomeViewModel>()과 같은 방식으로 AAC ViewModel을 생성하는 게 보편적이다. 이를 사용할 경우, 내부적으로 createViewModelLazy()를 호출하여 생성한다.

생성할 때 핵심은 2가지이다.
1. CreationExtras를 가져오되, 없을 경우, HasDefaultViewModelProviderFactory에 정의된 defaultViewModelCreationExtras를 가져온다.
2. ViewModelStoreOwner를 가져오되, 없을 경우, HasDefaultViewModelProviderFactory에 정의된 defaultViewModelProviderFactory를 가져온다.
by viewModel은 Fragment에서 호출되고, Fragment는 HasDefaultViewModelProviderFactory를 오버라이딩하고 있기에, 이를 ViewModel을 생성할 때, 즉, createViewModelLazy()를 호출할 ViewModelProvider.Factory를 조회할 수 있는 것이다.
[getDefaultViewModelCreationExtras]

위 코드의 핵심은 MutableCreationExtras()호출 및 Bundle에 담을 객체를 준비한단 점이다. 또한 if (getArguments() != null) { ... }을 통해, 이전 화면으로부터 받은 데이터를 set하는것도 확인할 수 있다. 타고 들어가보면 Fragment가 생성될 당시, 이전 화면에서 보내온 데이터를 캐싱해둔다는걸 확인할 수 있다.

[getDefaultViewModelProviderFactory]

AAC ViewModel을 생성하는 핵심이 흐름은 아래와 같다.
1. 주요한 2가지 값 주입으로 SavedStateViewModelFactory객체를 생성 및 반환한다.
ㄴ> 1.1. SavedStateRegistryOwner구현체를 주입.
ㄴ> 1.2. 이전 화면으로부터 받은 Bundle객체가 주입.
2. 메서드 반환타입을 함께 보면 유추가 가능하듯, SavedStateViewModelFactory는 ViewModelProvider.Factory의 구현체이다. 따라서, by viewModel()호출을 통해, SavedStateViewModelFactory.create()가 호출되어 ViewModel이 생성된다.
다만, 위 단계에서 짚고 넘어가야할 점이 1.1의 SavedStateRegistryOwner의 구현체이다. Fragment의 구현체를 보면 알겠지만, SavedStateRegistry객체의 생성을 SavedStateRegistryController에게 위임하고 있으며, 내부로 들어가 확인해보면, SavedStateRegistry 생성을 확인할 수 있다.


이제 다시 원점으로 돌아와 이야기를 이어나가보자. 결국, StatedStateViewModelFactory객체를 생성한다는 것은 SavedStateRegistry객체를 생성한다는 것과 이전화면으로부터 받은 Bundle을 주입한다는걸 알 수 있다. SavedStateViewModelFactory생성자를 타고 들어가보면?

SavedStateRegistry, Bundle을 멤버에 캐싱해두는걸 확인할 수 있다. 이제 이들을 활용하여 아래와 같은 유추가 가능하다.
ViewModelProvider.Factory.create()호출 시, 활용하는 게 아닐까?
확인을 위해 SavedStateViewModelFactory.create()를 확인해보자.


두 번째 사진이 바로, 최종적으로 귀결되는 create()메서드이다.
위 코드에서 설명을 하자면 아래와 같다.
SavedStateHandle의 활용 여부에 따라 ViewModel 생성 방식이 달라짐.Bundle을 받아오지 하지 않았다면, AndroidViewModel을 생성SavedHandleController.handle을 통해SavedStateHandle타입 받아오고, newInstance를 호출(AAC ViewModel생성)
그렇다면 여기서 의문. 아까 createViewModelLazy로 어떻게 create호출까지 가는걸까? 이 또한 타고 들어가보면 ViewModelLazy클래스에서 이를 확인할 수 있다. 밑줄그어 표시해놓은 부분이 바로, 위에서 확인한 SavedStateViewModelFactory.create()하는 부분이다. 만약 이해가 잘 가지 않을 경우, 해당 포스팅 맨 위 사진에 있는 클래스 구조도를 확인하면 이해가 쉽다.

ViewModel에서 SavedStateHandle은 보통 아래와 같이 사용한다.
class SampleViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
) { ... }
위와 같은 작업만으로 어떻게 SavedStateHandle에 Bundle이 함께 전달될 수 있는걸까? 정답은 SavedStateHandle내부에 핵심 프로퍼티인 regular를 활용하여, savedStateHandle.get()을 하여 데이터를 조회하는데, 이 녀석에 값들 채우기 위해 생성자로부터 regular.putAll(...)하는 형태로 데이터를 받는 것이다.

그렇다면 이 생성자가 어떻게 호출되는지가 핵심이라고 볼 수 있는데, 이는 아까 AAC ViewModel생성할 때, SavedStateViewModelFactory.create() 내부 로직에 정답이 있다. 아래 3단계를 연속적으로 타고 들어가보자.



최종적으로 createHandle로 귀결하면 가장 하단부에 Map형태로 key-value를 가져와서 할당 후, SavedStateHandle(state)를 최종 반환하는 것을 볼 수 있다. 이로써 AAC ViewModel생성 시, SavedStateHandle객체를 생성하여 전달하는 것을 확인할 수 있다.
위 동작 원리1을 이해했다면 해당 과정 이해도 어렵지 않다.
보통 by hiltViewModel을 사용할 땐, navigation graph와 함께 사용하는 게 일반적이다. 이를 사용하기 위해선 아래 과정을 따른다.
NavHost를 사용한 navigation코드 정의NavController정의이때, rememberNavController() 호출 및 NavController객체를 생성하면 내부적으로 NavBackStackEntry객체가 만들어지는데, 이를 내부적으로 타고 들어가보면, 이전 Fragment처럼, ViewModelStoreOwner, SavedStateRegistryOwner, HasDefaultViewModelFactory를 구현하고있는 걸 확인할 수 있다.

즉, NavBackStackEntry는 Activity / Fragment처럼 이 곳을 기반으로 AAC ViewModel 생성을 확인할 수 있다. 또한 이는 composable환경이므로, CompositionLocalProvier를 활용해 하위 composable 함수들에 상태 전달이 가능하고, 그렇게 전달하고 있다.

이렇게 하위 composable함수로 전달한 Local...Owner들은 by hiltViewModel을 생성하는 곳엘 타고 들어가봤을 때, 그대로 사용되는 걸 확인할 수 있다.

위 코드를 보면, by viewModel했을때보다 코드가 더 간결해 이해가 쉽다. 라인 수는 2줄이며, viewModel(...)반환을 확인할 수 있다. 이를 통해 아래와 같이 충분히 생각할 수 있다.
NavBackStackEntry에서 정의한HasDefaultViewModelProvider의 구현체에 따라 ViewModel 생성이 결정되겠군.NavBackStackEntry에서 정의한SavedStateRegistryOwner의 구현체가 composable환경에 맞게SavedState값 보존 로직이 정의돼 있겠군.

위 코드는 NavBackStackEntry의 구현체이며, 위에서 다룬 Fragment와 상당히 유사한것을 확인할 수 있다.
구성변경 및 Activity 재생성이 일어날 경우, onPause -> onStop -> onDestroy순으로 진행된다는걸 알텐데, 이때, Android Framework는 onRetainNonConfigurationInstance()를 onDestroy 이전에 호출시킴으로써 ViewModelStore의 값을 내부적으로 보관한다.

또한 ComponentActivity객체가 생성될 때, Lifecycle옵저빙을 등록한다. 이때의 코드를 보면, onDestroy될 때, 구성변경이 아닐 경우엔 ViewModelStore.clear()호출을 안한다는걸 확인할 수 있다.

그 후, 구성변경 후, 다시 돌아왔을 땐 데이터가 어떻게 보존될까? 정답은 위와 비슷한 위치한 ensureViewModelStore()로 인해 가능하다.


아까 구성변경 직전, onRetainNonConfigurationInstance()호출을 통해, NonConfigurationInstances객체 내부에 ViewModelStore를 메모리 캐싱해뒀었는데, 이를 다시 조회해오는 로직이다.
결국 이를 통해 구성변경이 발생해도 ViewModelStore는 유지되고 ViewModel또한 유지될 수 있는 것이다.
프로세스가 비정상 종료되는 경우(ViewModel이 더 이상 유지될 수 없는 경우),
Android는 상태 복원을 위해 두 개의 생명주기 경로를 사용한다.
onSaveInstanceStateonCreate or onRestoreInstanceStateSavedStateHandle 또한 바로 이 두 지점을 통해 프로세스 종료 전 상태 저장 → 프로세스 재시작 후 상태 복원을 수행한다. 이제 각 단계에서 어떤 일이 일어나는지 내부 흐름을 따라가 보자.
[OnSaveInstanceState]
해당 과정은 아래 클래스를 순차적으로 타고 들어간다.
- Android App 모듈
- Activity
- ActivityThread
- PendingTransactionActions.StopInfo
- ActivityClient
- IActivityClientController
- Android System 모듈
- ActivityClientController
- ActivityRecord

내부에 Bundle타입 파라미터인 outState를 보면 접두사로 out이 붙어있다. 이는 call by reference방식으로 해당 객체를 만든 호출부에서 최종적으로 사용하겠다는 의미이다. (메서드 내부에선 outState에 사용될 값을 모은다). 이는 즉, Activity.onSaveInstanceState()가 반환되면 이를 호출하는 ActivityThread가 outState값을 활용 및 데이터 저장을 시작한다는 의미이기도 하다.
ActivityThread를 타고 들어가면 handleStopActivity()가 존재한다. 이는 생명주기 onStop의 전체를 관리하는 오케스트레이터 역할을 한다.

위 코드에서 performStopActivityInner를 타고 들어가보면 최 하단에 callActivityOnStop()을 호출하는걸 볼 수 있다.

handleStopActivity()로 들어가보자. 코드를 보면, 내부적으로 Activity.onStop()을 호출하는걸 볼 수 있다. 또한 우리가 잘 알고 있듯, Activity.onSaveInstanceState()를 호출하는것 또한 볼 수 있다. (Android P 이전엔 onSavedInstanceState()가 onStop이전에 호출된다)

callActivityOnSaveInstanceState 또한 같은 원리로, 위 메서드 호출 이후 호출된다.

이렇게, callActivityOnStop()이 반환되면 handleStopActivity()내부의 performStopActivityInner() 또한 반환된다. 그 이후엔, 프로세스 비정상 종료 상태 저장을 위해, StopInfo객체를 사용해 Bundle을 저장하는걸 확인할 수 있다.
이렇게 handleStopActivity()가 반환되면, 스레드 작업을 통한 IPC통신을 시작하는 reportStop()메서드가 실행된다. 그 내부에선 StopInfo를 반환하는 Runnable타입의 getStopInfo()스레드를 시작하는걸 볼 수 있는데, 이는 이전에 담았던 pendingActions.setStopInfo(stopInfo);를 조회하는 코드이다. 이로써 Binder Buffer로 상태를 저장하기 위한 작업이 시작된다.

그럼 조금만 더 들어가, BinderIPC 통신으로 해당 Bundle을 write하는 작업까지 들어가보자. 이를 위해선 PendingTransactionActions.getStopInfo가 반환하는 StopInfo클래스(Runnable구현)한걸 우선 볼 필요가 있다.
이제부터 AndroidStudio로 볼 수 없기에 AndroidCodeSearch를 통해 보여준다

ActivityClient모듈로 타고 들어가보자

[여기서부터 중요 포인트!]
위 코드에서getActivityClientController()의 반환타입을 보면IActivityClientController으로 이는 Interface이다. 왜 그럴까? 이는 App Process모듈 부분과, Android System 모듈의 경계선이기 때문이다. 즉, Android App모듈은 시스템 모듈 구현체를 알 필요가 없기에 인터페이스만 알고있는 것이고, Android System에선 구현체를 정의해, Android App모듈에 이를 제공하고 있는 것이다. 즉, 위 부분이 바로,BinderIPC통신을 진행하는 구간인 것이다. 아래 패키지명으로 구분해놓았다.
| ...app/ActivityClient.java | ...android/server/wm/ActivityClientController.java |
|---|---|
![]() | ![]() |
따라서 이제부턴 Android System의 코드 부분을 서술한다.
이렇게 ActivityClientController.activityStopped() 타고타고 들어가면, AcitivityRecord.activityStopped()까지 최종적으로 도달할 수 있는데,

위 코드에서 setSavedState()를 통해 프로세스 복원에 필요한 데이터를 마지막으로 저장한다.

저장된 상태 보존 데이터 저장 위치가 변경되었다. 정확히 언제부턴진 모르겠으나, 공식 홈페이지 근거,
Disk->Memory로 해당 값 저장 위치가 바뀌었다.
[OnRestoreInstanceState]
위 과정을 이해했다면, 복원하는 과정도 크게 어렵지 않다. 이 또한 대략적으로 아래 순서로 진행된다.
우선, 액티비티가 재시작되면 ActivityTaskManagerService.startActivityFromRecents 호출을 시작한다.

그 후, 상태값 보존을 위해 ActivityRecord.mIcicle에 이전에 저장했던 mIcicle 값을 조회하여 LaunchActivityItem이란 모델에 이를 담는다

만들어진 LaunchActivityItem을 토대로, ActivityThread호출을 위해, BinderIPC 통신을 아래와 같이 시작한다.

아래 코드를 통해, ActivityThread.handleLaunchActivity를 호출할거란걸 알 수 있다. 또한 handle...이라는 네이밍은 이전에도 봤듯이 익숙하다.

아래, Activity.Thread에서 중요한 부분은 performLaunchActivity()이다.

performLaunchActivity()를 타고 들어가면 코드가 매우 길다. 하지만 아래와 같은 부분을 찾을 수 있다.

즉, 위와 같은 코드로, Activity.onCreate()호출 시, 프로세스 비정상 종료를 위해 저장했던 Bundle을 전달할거란 걸 알 수 있다. 비슷한 원리로, Activity.onCreate()에서 소비되지 못한 Bundle은 OnRestoreInstanceState()에서 또한 위와같은 방식으로 전달될거란 합리적인 추론을 할 수 있다.
아직 만나보지 않은 이슈이기도 하지만, AAC ViewModel을 생성할 때, SavedStateViewModelFactory를 만드는데, 이때 내부적으로 createHandle을 호출하여 SavedStateHandle객체를 생성한다. 이때, 이 메서드의 내부 생성 원리를 봤을 때, 아래와 같은 주석을 볼 수 있다.

위 사진에서 defaultState는 이전 화면으로부터 받은 Bundle을, restoredState는 프로세스 강제 종료에 따른 상태 보존 Bundle을 의미하는데, 후자가 존재할 경우, 전자를 무시한다고 주석에 써져있다.
by viewModels()/hiltViewModel()로 ViewModel을 만들 때, 내부적으로 SavedStateViewModelFactory가 이전 화면의 Bundle(arguments)을 주입하며, SavedStateHandle와 함께 구성한다.
구성 변경(예: 회전) 시에는 onRetainNonConfigurationInstance()로 ViewModelStore가 유지되어, ViewModel의 메모리 상태가 그대로 보존된다.
프로세스 종료 대비는 onSaveInstanceState로 상태를 Bundle에 저장하고, 재시작 시엔 onCreate/onRestoreInstanceState를 통해 SavedStateHandle이 복원된다.
단, 복원용 Bundle(restoredState)가 존재하면 이전 화면에서 받은 Bundle(defaultState)은 무시될 수 있으므로(주석 명시), 초기값/인자 처리 로직 설계에 주의가 필요하다.