[Android] Activity LifeCycle

neoneoneo·2024년 3월 21일
0

android

목록 보기
4/16

Android Developers에서 제공하는 학습 자료를 참고합니다.
link

활동 수명 주기 단계

Activity의 생성에서부터 소멸(시스템의 그 리소스를 회수)하는 것까지를 아우르는 개념이다.
사용자가 액티비티를 탐색할 때 각 액티비티는 수명 주기의 여러 상태로 전환된다.
액티비티가 수명 주기 상태 변경에 적절하게 응답하지 않는다면, 앱은 이상한 버그를 발생시킬 거나 안드로이드 시스템 리소스를 너무 많이 사용하게 될 수 있다. 또는 사용자가 앱의 동작을 혼동하게 될 수도 있으므로 수명 주기를 이해하는 것은 개발에 있어서 중요한 부분이다.

수명 주기 메서드


알에서 태어나 죽음에 이르는 나비의 라이프사이클과 같이, 안드로이드 앱 또한 수명 주기를 가진다. 안드로이드 액티비티는 onCreate() 메서드로 시작하는데, 나비로 치면 '알'의 상태와 비슷하다고 볼 수 있다. 다만 나비와 달리 안드로이드 앱은 수명 주기 단계의 이동이 단방향이 아니라는 것이다.


안드로이드 앱에는 위 그림과 같은 액티비티 수명 주기 상태와 수명 주기 콜백 메서드가 존재한다.

onCreate() 메서드 확인

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
}

onCreate() 메서드에서는 액티비티의 일회성 초기화를 실행해야 하는데, onCreate() 메서드는 액티비티가 초기화된 직후(OS에서 메모리에 새로운 액티비티 객체를 만들 때) 한 번 호출이 된다.
onCreate()가 실행되면 활동이 생성되었다고 간주된다.
또한, onCreate() 메서드를 재정의할 때, 상위클래스 구현을 호출하여 활동 생성을 완료해야하는데, 이를 위해 액티비티 안에서 super.onCreate()를 즉시 호출해야한다. 이 사항은 다른 수명 주기 콜백 메서드의 경우에도 마찬가지로 적용이 필요하다.

onStart() 메서드 구현


onStart() 메서드는 onCreate() 직후에 호출된다.
onStart()가 실행되면 액티비티가 화면에 표시되며, onCreate()와 달리 시스템에서 활동의 수명 주기 동안 여러 번 호출할 수 있다.
onStart()는 onStop() 메서드와 상응하여 페어링된다. 사용자가 앱을 시작한 후 기기 홈 화면으로 돌아오면 액티비티가 중지되고, 더 이상 화면에 표시되지 않게 된다.

ovveride fun onCreate() {
	super.onCreate()
    Log.d(TAG, "onCreate Called")
}

override fun onStart() {
    super.onStart()
    Log.d(TAG, "onStart Called")
}
2024-02-20 10:30:00.231  5684-5684  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:30:00.278  5684-5684  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:30:39.020  5684-5684  MainActivity            com.example.dessertclicker           D  onStart Called

앱을 실행하면 처음에 onCreate()가 한 번 실행되고, 그 이후에는 onStart()가 실행된다. 이때 기기에서 홈 버튼을 누르고 다시 앱으로 돌아가면, 액티비티는 중단된 지점에서 다시 시작되고, onStart()가 다시 기록이 된다. 이때 onCreate()는 다시 호출되지 않는다.

다른 메서드들까지 확인해보기

ovveride fun onCreate() {
	super.onCreate()
    Log.d(TAG, "onCreate Called")
}

override fun onStart() {
    super.onStart()
    Log.d(TAG, "onStart Called")
}

override fun onResume() {
    super.onResume()
    Log.d(TAG, "onResume Called")
}

override fun onRestart() {
    super.onRestart()
    Log.d(TAG, "onRestart Called")
}

override fun onPause() {
    super.onPause()
    Log.d(TAG, "onPause Called")
}

override fun onStop() {
    super.onStop()
    Log.d(TAG, "onStop Called")
}

override fun onDestroy() {
    super.onDestroy()
    Log.d(TAG, "onDestroy Called")
}
2024-02-20 10:33:36.033  5789-5789  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:33:36.073  5789-5789  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:33:36.075  5789-5789  MainActivity            com.example.dessertclicker           D  onResume Called

액티비티가 처음 시작되면 onCreate(), onStart(), onResume() 콜백이 순서대로 호출된다.
onResume() 메서드는 다시 시작할 대상이 없어도 시작 시 호출되는 것이 특징이다.

이후 앱에서 동작을 하고 기기에서 뒤로 버튼을 탭하면,

2024-02-20 10:34:41.974  5789-5789  MainActivity            com.example.dessertclicker           D  onPause Called
2024-02-20 10:34:42.411  5789-5789  MainActivity            com.example.dessertclicker           D  onStop Called

onPause(), onStop()이 순서대로 호출된다.
뒤로 버튼을 누르면 액티비티 또는 앱이 화면에서 사라지고 액티비티 스택의 뒤로 이동한다.

액티비티에서 finish() 메서드를 수동으로 호출하거나 사용자가 앱을 강제로 종료할 경우, 안드로이드 OS에서는 해당 액티비티를 닫을 수 있다.

다만, 액티비티는 사용자가 활동에서 벗어날 때마다 완전히 닫히지는 않는다. 포그라운드에서 화면에 표시되고 있다가, 더 이상 화면에 표시되지 않을 때 액티비티는 백그라운드에 배치된다. 사용자가 다시 액티비티로 돌아오면 다시 시작되어 화면에 표시된다(표시 수명 주기).

일반적으로 앱이 백그라운드에 있을 때 시스템 리소스와 배터리 수명을 보존하기 위해서라도 아주 활발하게 실행되지는 않는다. 액티비티 수명 주기와 콜백을 사용하면 앱이 백그라운드로 이동하는 시점을 알 수 있게 되고, 진행 중인 작업을 일시중지했다가 앱이 포그라운드로 전환될 때 작업을 다시 시작할 수 있다.

앱이 실행되는 상태에서 동작을 수행하고, 기기에서 홈버튼을 누르면,

2024-02-20 10:35:26.832  5789-5789  MainActivity            com.example.dessertclicker           D  onPause Called
2024-02-20 10:35:27.233  5789-5789  MainActivity            com.example.dessertclicker           D  onStop Called

앱이 완전히 종료되는 대신에 onPause(), onStop() 메서드가 호출되는 것을 볼 수 있다.

onPause()가 호출되면 더이상 앱에 포커스를 맞추고 있지 않게 되고, onStop() 이후에는 앱이 화면에 표시되지 않게된다.

이때 다시 앱으로 돌아가면,

2024-02-20 10:36:05.837  5789-5789  MainActivity            com.example.dessertclicker           D  onRestart Called
2024-02-20 10:36:05.839  5789-5789  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:36:05.842  5789-5789  MainActivity            com.example.dessertclicker           D  onResume Called

onRestart(), onStart()로 시작한 후 onResume()으로 앱이 재개된다.

참고로 onRestart()는 액티비티가 이미 만들어진 상태에서 onStop()이 호출될 때 Created 상태로 전환된다. 이때 Destroyed로 진입하지 않고 Started로 되돌아 가는 경우에만 시스템에서 호출한다. 즉, onRestart()는 액티비티가 처음으로 시작되지 않은 경우에만(앱을 백그라운드에서 포그라운드로 다시 시작하는 경우에만) 호출하려는 코드를 배치하는 위치이다.

이렇게되면 onStart(), onStop()은 사용자가 액티비티에서 나가거나 다른 액티비티로 이동할 때 여러번 호출된다는 것을 알 수 있게된다.

만약, 아래 상황처럼 액티비티가 화면에 부분적으로 표시되고는 있으나 포커스가 없는 경우라면 어떨까?

2024-02-20 10:36:42.886  5789-5789  MainActivity            com.example.dessertclicker           D  onPause Called

이럴 때에는 onPause()만이 호출된 것을 볼 수 있다.

이때 onStop()은 호출되지 않는데, 화면이 부분적으로나마 계속해서 표시되고 있기 때문이다. 다만 사용자 포커스가 액티비티에 없으므로 상호작용할 수는 없는 상태이다.

onPause()만 사용한 중단은 보통 액티비티로 돌아가거나, 다른 액티비티 또는 앱으로 이동하기 전에 잠시 지속된다. UI를 계속 업데이트하여 부분적으로 보이는 앱이 멈춘 것처럼 보이지 않도록 하는 것이 좋기 때문이다.

이때 다시 앱으로 돌아가면 onResume()이 호출된다.
onResume(), onPause()는 모두 포커스와 관련이 있는 메서드들로 활동에 포커스가 있다면 onResume()이, 활동에 포커스가 없다면 onPause()가 호출된다.

다음으로는 onDestory()에 대해 알아보자.

구성 변경사항 살펴보기

구성 변경사항을 확인하기 위해 활동을 완전히 종료하고 다시 빌드하는 상황을 생각해보자. 예를들어 기기의 언어를 변경하여 텍스트 방향이나 문자열 길이에 맞게 전체 레이아웃을 변경해야 한다거나, 기기를 물리적 하드웨어에 연결하여 앱 레이아웃이 다른 디스플레이 크기나 레이아웃을 활용해야 한다거나, 기기 방향이 변경되어 레이아웃을 변경해야 하는 상황들이 있겠다.

만약 기기를 가로 모드로 회전하면,

2024-02-20 10:37:57.078  5987-5987  MainActivity            com.example.dessertclicker           D  onPause Called
2024-02-20 10:37:57.087  5987-5987  MainActivity            com.example.dessertclicker           D  onStop Called
2024-02-20 10:37:57.102  5987-5987  MainActivity            com.example.dessertclicker           D  onDestroy Called

onPause(), onStop(), onDestroy()가 순서대로 호출된다.

이때 유의해햐 하는 점은 데이터 손실이다.

앱을 켜서 회전하기 전에 액티비티에서 무언가의 동작을 하여 10이라는 숫자 값을 만들게 되었다고 하자. 그리고 화면을 회전하면,

2024-02-20 10:39:22.724  6087-6087  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:39:22.752  6087-6087  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:39:22.753  6087-6087  MainActivity            com.example.dessertclicker           D  onResume Called
2024-02-20 10:39:40.508  6087-6087  MainActivity            com.example.dessertclicker           D  onPause Called
2024-02-20 10:39:40.540  6087-6087  MainActivity            com.example.dessertclicker           D  onStop Called
2024-02-20 10:39:40.549  6087-6087  MainActivity            com.example.dessertclicker           D  onDestroy Called
2024-02-20 10:39:40.582  6087-6087  MainActivity            com.example.dessertclicker           D  onCreate Called
2024-02-20 10:39:40.584  6087-6087  MainActivity            com.example.dessertclicker           D  onStart Called
2024-02-20 10:39:40.585  6087-6087  MainActivity            com.example.dessertclicker           D  onResume Called

onDestroy()이후 onCreate()가 되는 것을 볼 수 있다. 이는 이전에 동작으로부터 만들어 놓은 10이라는 숫자 데이터의 손실을 야기하는 상황이다.

왜 값이 재설정되는가? 이를 수정할 수는 없는 것인가?를 이해하려면 컴포저블의 수명 주기를 이해해야한다.

앱의 UI는 처음에 컴포지션이라는 프로세스에서 구성 가능한 함수를 실행하여 빌드된다. 앱의 상태가 변경되면 리컴포지션이 예약되는데, 이는 상태가 변경되었을 수 있는 구성 가능한 함수를 Compose에서 다시 실행하고, 업데이트된 UI를 만드는 것을 의미한다.

이를 방지하기 위해 MutableStateOf를 사용하여 객체의 상태를 추적하여 Compose에 나타내야한다.

var revenue by remember { mutableStateOf(0) }

revenue 값이 변경되면, Compose는 리컴포지션을 위해 이 값을 읽는 모든 구성 가능한 함수를 예약한다. 다만 Compose는 리컴포지션 중에는 revenue의 상태를 기억하지만, 구성 변경 중에는 이 상태를 유지하지 않는다.

이럴 때에는 rememberSaveable을 사용하여 구성 변경 시의 값을 저장한다.

var revenue by rememberSaveable { mutableStateOf(0) }
...
var currentDessertImageId by rememberSaveable {
    mutableStateOf(desserts[currentDessertIndex].imageId)
}

이렇게하면 화면을 가로 모드로 회전해도 그 전에 만들어놓은 값을 rememberSaveable을 이용하여 저장하고, 액티비티가 다시 onCreate() 된 후에도 해당 값으로 복원하여 데이터를 보존할 수 있게 된다.


[TIL-240321]

profile
우당탕ㅌ앙개발기록

0개의 댓글