[Android] 액티비티 생명주기

액티비티 상태와 생명주기 콜백

Activity의 모든 인스턴스는 생명주기를 갖는다. 그리고 생명주기 동안에 액티비티는 네 가지 상태, 즉 실행 재개(resumed), 일시 중지(paused), 중단(stopped), 존재하지 않음(non-existent)으로 상호 전환된다. 또한, 각 전환이 발생할 때 액티비티에 상태 변경을 알려주는 Activity 함수들이 있으며, 이 함수들은 안드로이드가 자동 호출한다.

액티비티 상태 다이어그램

액티비티 상태

상태 메모리에 있음? 사용자에게 보임? 포그라운드에서 실행?
존재하지 않음 아니오 아니오 아니오
중단 아니오 아니오
일시 중지 예(부분적)* 아니오
실행 재개

* 당시 상황에 따라 일시 중지된 액티비티의 전체 또는 일부가 사용자에게 보일 수 있다

‘존재하지 않음’ 상태

  • 액티비티가 아직 론칭되지 않았거나 소멸되었음(예를 들어, 사용자가 백 버튼을 눌러서)을 나타낸다.
  • 이 때문에 때로는 이 상태를 ‘소멸(destroyed)’ 상태라고도 한다. 이때 액티비티 인스턴스는 메모리에 존재하지 않으며, 사용자가 보거나 상호 작용하기 위한 뷰도 없다.

‘중단’ 상태

  • 액티비티 인스턴스가 메모리에 있지만, 이것의 뷰는 화면에서 볼 수 없다는 것을 나타낸다.
  • 액티비티가 처음 시작될 때 거쳐가는 상태이며, 액티비티 인스턴스의 뷰가 화면에서 완전히 가려졌을 때 언제든 다시 진입하는 상태다.
    • 예를 들어, 전체 화면을 사용하는 다른 액티비티를 사용자가 시작하거나 홈 버튼을 누를 때 등이다.

‘일시 중지’ 상태

  • 액티비티가 포그라운드(foreground)에서 작동하지는 않지만, 액티비티 인스턴스의 뷰 전체 또는 일부를 화면에서 볼 수 있음을 나타낸다.
    • 예를 들어, 이 액티비티 위에 새로운 대화상자나 투명 액티비티가 사용자에 의해 시작된다면 이 액티비티는 일부만 화면에 보이게 된다.
    • 만일 사용자가 다중 창 모드(분활 화면 모드)로 두 개의 액티비티를 같이 보고 있다면 액티비티 전체가 화면에 보일 수 있지만, 포그라운드에 존재하지 않을 수도 있다.

‘실행 재개’ 상태

  • 액티비티가 메모리에 있으면서 화면에서 전체를 볼 수 있고 포그라운드에 있음을 나타낸다.
  • 사용자가 현재 상호 작용하고 있는 액티비티가 바로 이 상태다.
  • ‘실행 재개’ 상태는 장치의 전체 시스템에 걸쳐 하나의 액티비티만 될 수 있다.
    • 즉, 한 액티비티가 ‘실행 재개’ 상태가 되면 직전에 실행 중이던 액티비티는 다른 상태로 바뀐다는 의미다.

상단의 액티비티 상태 다이어그램 이미지에 있는 함수들을 사용해 액티비티 생명주기의 전환 시점에 필요한 일을 처리할 수 있다. 이 함수들을 생명주기 콜백(lifecycle callback) 이라고 한다.

이미 생명주기 콜백 함수 중 하나인 onCreate(Bundle?)을 알고 있을 것이다. 액티비티 인스턴스가 생성되고 화면에 나타나기 전에 안드로이드 운영체제가 이 함수를 호출한다.

UI를 준비하기 위해 액티비티에서는 다음과 같이 onCreate(Bundle?) 함수를 오버라이드(override)한다.

  • 위젯을 인플레이트해 뷰 객체로 생성한 후 화면에 보여준다. (setContentView(Int)를 호출)
  • 인플레이트된 위젯의 객체 참조를 얻는다.
  • 사용자와의 상호 작용을 처리하기 위해 위젯에 리스너를 설정한다.
  • 외부의 모델 데이터를 연결한다.

액티비티 생명주기 로깅하기

로그 메세지 만들기

  • 안드로이드에서 android.util.Log 클래스는 공유되는 시스템 수준의 로그에 로그 메시지를 전달한다.
    • Log 클래스는 메시지를 로깅하기 위한 함수들을 갖고 있다.
  • 메시지의 내용은 물론, 메시지의 중요도를 나타내는 레벨(level)도 제어할 수 있다.
    • 안드로이드는 다섯 개의 로그 레벨을 지원

로그 레벨과 함수

로그 레벨 함수 용도
ERROR Log.e(…) 에러
WARNING Log.w(…) 경고
INFO Log.i(…) 정보성 메시지
DEBUG Log.d(…) 디버깅 출력이며 필터링할 수 있다.
VERBOSE Log.v(…) 개발 전용

각 로깅 함수는 두 개의 시그니처(signature)를 갖는다.

  1. 하나는 태그 문자열과 메시지 문자열로 된 두 개의 인자를 받고
    • 일반적으로 태그 문자열에는 클래스 이름을 값으로 갖는 TAG 상수를 지정한다. 이렇게 하면 메시지의 근원을 알기 쉽다.
  2. 다른 하나는 이 두 인자에 Throwable 인스턴스를 추가로 받는다.
    • Throwable 인스턴스는 앱이 발생시킬 수 있는 특정 예외에 관한 정보를 쉽게 로깅할 수 있게 한다.

TAG 상수 추가하기 (MainActivity.kt)

1
2
3
4
5
6
7
import ...

private const val TAG = "MainActivity"

class MainActivity : AppCompatActivity() {
...
}

참고로, 이처럼 .kt 파일 내부에서 클래스 바깥쪽에 선언한 변수를 코틀린에서는 최상위 수준 속성이라고 한다. 최상위 수준 속성은 다음 두 가지 상황에 사용할 수 있다.

  1. 특정 클래스의 인스턴스를 생성하지 않고 바로 사용할 수 있으므로 애플리케이션이 실행되는 동안 속성값을 계속 보존해야 할 때다.
  2. 애플리케이션 전체에서 사용하는 상수를 정의할 때 유용하다.

안드로이드에서 로깅하는 방법

1
2
3
4
5
6
7
8
9
// DEBUG 로그 레벨로 메시지를 로깅한다
Log.d(TAG, "Current question index: $currentIndex")

try {
val question = questionBank[currentIndex]
} catch (ex: ArrayIndexOutOfBoundsException) {
// 스택에 저장된 예외의 기록과 함께 ERROR 로그 레벨로 메시지를 로깅한다
Log.e(TAG, "Index was out of bounds", ex)
}

생명주기 함수를 추가로 오버라이드하기(MainActivity.kt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class MainActivity : AppCompatActivity() {
...

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate(Bundle?) called")
setContentView(R.layout.activity_main)
...
}

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

override fun onResume() {
super.onResume()
Log.d(TAG, "onResume() 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")
}

...
}

Log.d(…)를 호출해 메시지를 로깅하기 전에 오버라이드되는 슈퍼 클래스 함수를 호출한다는 점에 유의하자. 오버라이드하는 각 콜백 함수에서는 오버라이드되는 슈퍼 클래스 함수를 호출하는 코드가 맨 앞에 있어야 한다. 각 함수에 override 키워드가 있는 이유는 오버라이드하는 함수가 슈퍼 클래스에 있는지 컴파일러에게 확인하라고 요청하기 위해서다.

액티비티 생명주기가 사용자 액션에 어떻게 응답하는지 살펴보기

GeoQuiz 앱이 설치 및 실행될 때는 생명주기 함수 onCreate(Bundle?), onStart(), onResume()가 차례대로 호출되고 MainActivity 인스턴스가 생성된다. 즉, MainActivity 인스턴스가 ‘실행 재개’ 상태가 된다(메모리에 로드되고, 사용자에게 보이며, 포그라운드에서 작동함).

앱 설치 및 실행 시, onCreate(Bundle?), onStart(), onResume()가 차례대로 호출된다.

일시적으로 액티비티 떠나기

홈 버튼을 누르면 MainActivity는 onPause(), onStop() 호출을 받지만, onDestroy()는 호출되지 않는다. 그러면 MainActivity는 어떤 상태일까?

홈 버튼을 누르면 onPause(), onStop()가 호출된다.

장치의 홈 버튼을 누르면 안드로이드 운영체제에 '나는 다른 작업을 하려고 한다. 그런데 현재의 액티비티 화면에서 볼일이 다 끝나지 않았으므로 다시 돌아올 수 있다.'라고 알리는 셈이 된다. 따라서 안드로이드 운영체제는 현재 액티비티를 일시 중지했다가 중단시킨다. 즉, GeoQuiz 앱에서 홈 버튼을 누른 후에는 MainActivity의 인스턴스가 ‘중단’ 상태가 된다(메모리에는 있지만 사용자에게는 보이지 않으며, 포그라운드에서 실행되지 않는다). 그러나 사용자가 나중에 GeoQuiz 앱으로 돌아오면 안드로이드 운영체제는 재빨리 MainActivity 인스턴스를 다시 시작한다.

다시 앱을 키면 onRestart(), onStart(), onResume()이 차례로 호출된다.

오버뷰 화면(overview screen)에서의 동작은?

아래의 이미지와 같이 오버뷰 화면의 각 카드는 이전에 사용자가 사용했던 앱을 나타낸다. 오버뷰 화면은 ‘최근 앱 화면’ 또는 '태스크 매니저’라고도 한다. 여기서는 개발자 문서에서 얘기하는 '오버뷰 화면’이라고 칭한다.

오버뷰 화면에서 태스크 클릭 시에는 onRestart(), onStart(), onResume()이 호출

오버뷰 화면에서 GeoQuiz 태스크를 클릭하면 MainActivity가 화면에 나타난다. 이때 로그캣 창의 메시지를 보면 onRestart(), onStart(), onResume()이 호출되었음을 알 수 있다. 하지만 onCreate(…)는 호출되지 않았는데 홈 버튼을 누른 후에 MainActivity는 ‘중단’ 상태가 되었기 때문이다. 따라서 MainActivity 인스턴스는 여전히 메모리에 있으므로 다시 생성될 필요가 없다. 그리고 오버뷰 화면에서 선택되면 액티비티만 다시 시작되어('일시 정지’이면서 화면에 볼 수 있는 상태) 실행이 재개된다(포그라운드로 '실행 재개’되는 상태).

액티비티는 ‘일시 중지’ 상태에 머물러 있을 수도 있는데, 이때는 일부만 화면에 보이거나(예를 들어, 투명한 백그라운드를 갖거나 더 작은 화면 크기를 갖는 다른 액티비티가 '일시 중지’된 액티비티 화면 위에 있을 때) 또는 전체 화면이 보일 수도 있다(다중 창 모드일 때).

다중 창 모드(multi window mode)에서의 동작은?

    

아래 창에 열린 다른 앱을 클릭하고 로그캣의 메시지를 보면 GeoQuiz의 MainActivity에서 onPause()가 호출되었음을 알 수 있다. 즉, MainActivity는 현재 ‘일시 중지’ 상태다.

그리고 위의 창에 열린 GeoQuiz를 클릭하면 MainActivity의 onResume()가 호출된다. 이제는 MainActivity가 ‘실행 재개’ 상태가 되었기 때문이다.

액티비티 끝내기

장치의 백 버튼을 누른 후 로그캣의 메시지를 확인해보자. MainActivity의 onPause(), onStop(), onDestroy()가 호출되었을 것이다. MainActivity의 인스턴스가 존재하지 않는 상태다(메모리에 없고 화면에도 보이지 않으며, 포그라운드에서도 동작하지 않음).

백 버튼을 누르면 액티비티 인스턴스가 소멸되어 onPause(), onStop(), onDestroy()가 호출된다.

장치의 백 버튼을 눌렀다는 것은 앱의 사용자가 해당 액티비티를 끝냈다는 의미다. 달리 말해, 안드로이드 운영체제에 '나는 이 액티비티를 다 사용했으므로 더 이상 필요 없다.'라고 알리는 셈이다. 그러면 안드로이드 운영체제는 해당 액티비티를 소멸시키고 메모리로부터 모든 흔적을 지운다. 이것이 바로 장치의 제한된 리소스를 절약하는 안드로이드의 방식이다.

또한, 오버뷰 화면에서 해당 앱의 카드를 옆으로 밀어내도 앱을 끝낼 수 있으며, 코드에서는 Activity.finish()를 호출해 액티비티를 끝낼 수 있다.

오버뷰 화면에서 앱 종료 시, onDestroy() 호출

액티비티 회전시키기

장치를 회전하면 onPause(), onStop(), onDestroy(), onCreate(…), onStart(), onResume()이 차례로 호출된다.

장치 회전 시, MainActivity가 죽었다가 다시 살아난다!

위 메시지를 보면 알 수 있듯, 장치를 회전하면 보고 있던 MainActivity 인스턴스는 소멸되었다가 다시 새로운 인스턴스로 생성된다. 현재 인스턴스의 currentIndex에 저장된 값이 메모리에서 지워지므로, 장치를 회전하면 그 당시 사용자가 어떤 문제를 보고 있었는지 GeoQuiz가 모르게 된다는 의미다.

장치가 회전될 때 안드로이드는 완전히 새로운 MainActivity 인스턴스를 생성한다. 따라서 onCreate(Bundle?)에서 currentIndex이 값이 0으로 초기화되므로 사용자는 첫 번째 문제를 다시 보게 된다.

장치 구성 변경과 액티비티 생명주기

장치를 회전하면 장치 구성(device configuration) 이 변경된다. 장치 구성은 각 장치의 현재 상태를 나타내는 특성들의 집합이다. 장치 구성을 이루는 특성에는 화면 방향, 화면 밀도, 화면 크기, 키보드 타입, 도크(dock) 모드, 언어 등이 있다.

일반적으로 앱에서는 서로 다른 장치 구성에 맞추기 위해 대체 리소스를 제공한다. 장치마다 다른 화면 밀도를 고려해 여러 화살표 아이콘을 프로젝트에 추가했을 때 이미 이런 예를 보았다.

런타임 구성 변경(runtime configuration change) 이 생길 때는 새로운 구성에 더 잘 맞는 리소스들이 있을 수 있다. 따라서 안드로이드는 현재의 액티비티 인스턴스를 소멸시키고 새로운 구성에 가장 적합한 리소스를 찾는다. 그리고 그런 리소스를 사용해서 해당 액티비티의 새 인스턴스를 다시 빌드한다. 예로 장치의 화면 방향이 가로 방향으로 변경될 때 안드로이드가 찾아 사용할 대체 리소스를 생성할 수 있다.

가로 방향 레이아웃 activity_main.xml (land) 생성

방향에 따른 레이아웃이 나온다


    

댓글