[안드로이드] Activity 수명 주기

1. Activity lifecycle

- 이해

액티비티의 수명 주기 콜백을 잘 구현하면 앱에서 다음과 같은 문제가 발생하지 않도록 예방할 수 있음

  • 사용자가 앱을 사용하는 도중에 전화가 걸려오거나 다른 앱으로 전환할 때 비정상 종료되는 문제
  • 사용자가 앱을 활발하게 사용하지 않는 경우 귀중한 시스템 리소스가 소비되는 문제
  • 사용자가 앱에서 나갔다가 나중에 돌아았을 때 사용자의 진행 상태가 저장되지 않는 문제
  • 화면이 가로 방향과 세로 방향 간에 회전할 경우, 비정상 종료되거나 사용자의 진행 상태가 저장되지 않는 문제

- 수명 주기 콜백

1) onCreate()

시스템이 먼저 활동을 생성할 때 실행되는 것으로, 필수적으로 구현해야 한다.
onCreate() 메서드에서 활동의 전체 수명 주기 동안 한 번만 발생해야 하는 기본 애플리케이션 시작 로직을 실행한다.

예를 들어 onCreate()를 구현하면 데이터를 목록에 바인딩하고, 활동을 ViewModel과 연결하고, 일부 클래스 범위 변수를 인스턴스화할 수 있다.
이 메서드는 savedInstanceState 매개변수를 수신하는데, 이는 활동의 이전 저장 상태가 포함된 Bundel 객체이다.
이번에 처음 생성된 활동인 경우 Bundle 객체의 값은 null이다.

lateinit var textView: TextView

// some transient(과도 현상) state for the activity instance
var gameState: String? = null

override fun onCreate(savedInstanceState: Bundle?) {
    // call the super class onCreate to complete the creation of activity like
    // the view hierarchy
    super.onCreate(savedInstanceState)

    // recovering the instance state
    // 이전 상태 복구 변수 
    gameState = savedInstanceState?.getString(GAME_STATE_KEY)

    // set the user interface layout for this activity
    // the layout file is defined in the project res/layout/main_activity.xml file
    setContentView(R.layout.main_activity)

    // initialize member TextView so we can manipulate(조종하다) it later
    textView = findViewById(R.id.text_view)
}

// This callback is called only when there is a saved instance that is previously saved by using
// onSaveInstanceState(). We restore some state in onCreate(), while we can optionally restore
// other state here, possibly usable after onStart() has completed.
// The savedInstanceState Bundle is same as the one used in onCreate().
// UI - 이전 상태 복구 
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    textView.text = savedInstanceState?.getString(TEXT_VIEW_KEY)
}

// invoked(호출되다) when the activity may be temporarily destroyed, save the instance state here
// 상태 저장
override fun onSaveInstanceState(outState: Bundle?) {
    outState?.run {
        putString(GAME_STATE_KEY, gameState)
        putString(TEXT_VIEW_KEY, textView.text.toString())
    }
    // call superclass to save any view hierarchy
    super.onSaveInstanceState(outState)
}

XML 파일을 정의하고 setContentView()에 전달하는 대신, 활동 코드에 새로운 View 객체를 생성하고 새로운 View를 ViewGroup에 넣어서 뷰 계층 구조를 빌드할 수 있다.
그런 다음 루트 ViewGroup을 setContentView()에 전달하여 그 레이아웃을 사용한다.

활동은 생성됨 상태에서 머무르지 않는다. onCreate() 메서드가 실행을 완료하면 시작됨 상태가 되고, 시스템이 연달아 onStart()와 onResume() 메서드를 호출한다.

2) onStart()

활동이 시작됨 상태에 들어가면 시스템은 이 콜백을 호출한다.
onStart()가 호출되면 활동이 사용자에게 표시되고, 앱을 활동을 포그라운드에 보내 상호작용할 수 있도록 준비한다.
예를 들어 이 메서드에서 앱이 UI를 관리하는 코드를 초기화한다.

onStart() 메서드는 매우 빠르게 완료되고, 생성됨 상태와 마찬가지로 활동은 시작됨 상태에 머무르지 않는다.
이 콜백이 완료되면 활동이 재개됨 상태에 들어가고, 시스템이 onResume() 메서드를 호출한다.

3) onResume()

활동이 재개됨 상태에 들어가면 포그라운드에 표시되고 시스템이 onResume()콜백을 호출한다.
이 상태에 들어갔을 때 앱이 사용자와 상호작용한다.
어떤 이벤트가 발생하여 앱에서 포커스가 떠날 때까지 앱이 이 상태에 머무른다.
예를 들어 전화가 오거나, 사용자가 다른 활동으로 이동하거나, 기기 화면이 꺼지는 이벤트가 이에 해당한다.

방해되는 이벤트가 발생하면 활동은 일시중지됨 상태에 들어가고, 시스템이 onPause() 콜백을 호출한다.

활동이 일시중지됨 상태에서 재개됨 상태로 돌아오면 시스템이 onResume() 메서드를 다시 한번 호출한다.
따라서 onResume()을 구현하여 onPause() 중에 해제하는 구성요소를 초기화하고, 활동이 재개됨 상태로 전환될 때마다 필요한 다른 초기화 작업도 수행해야 한다.

class CameraComponent : LifecycleObserver {

    ...

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun initializeCamera() {
        if (camera == null) {
            getCamera()
        }
    }

    ...
}

위의 코드는 LifecycleObserver가 ON_RESUME 이벤트를 수신하면 카메라를 초기화한다.
** 멀티 윈도우 환경에선 적절하게 사용해야 함

4) onPause()

시스템은 사용자가 활동을 떠나는 것을 나타내는 첫 번째 신호로 이 메서드를 호출한다. (하지만 해당 활동이 항상 소멸되는 것은 아님)
활동이 포그라운드에 있지 않게 되었다는 것을 나타낸다. (다만 사용자가 멀티 윈도우 모드에 있을 경우에는 여전히 표시 될 수도 있음)

onPause()메서드를 사용하여 Acitivity가 일시중지됨 상태일 때 계속 실행되어서는 안 되지만 잠시 후 다시 시작할 작업을 일시중지하거나 조정한다.
활동이 이 상태에 들어가는 이유는 여러가지가 있다.

  • onResume() 섹션에서 설명하였듯이, 일부 이벤트가 앱 실행을 방해한다.
  • Android 7.0(API 수준 24) 이상에서는 여러 앱이 멀티 윈도우모드에서 실행된다. 언제든지 그중 하나의 앱만 포커스를 가질 수 있기 때문에 시스템이 그 외에 모든 다른 앱을 일시중지시킨다.
  • 새로운 반투명 활동(예: 대화상자)이 열린다. 활동이 여전히 부분적으로 보이지만 포커스 상태가 아닌 경우에는 일시중지됨 상태로 유지된다.

멀티 윈도우 모드를 잘 지원하기 위해 UI 관련 리소스와 작업을 완전히 해제하거나 조정할 때는 onPause() 대신 onStop()을 사용하는 것이 좋다.

onPause()는 아주 잠깐 실행되므로 저장 작업을 실행하기에는 시간이 부족할 수 있다. 그러므로 onPause()를 사용하여 애플리케이션 또는 사용자 데이터를 저장하거나, 네트워크 호출을 하거나, 데이터베이스 트랜잭션을 실행해서는 안 된다.
onPause() 대신 onStop() 상태일 때 실행해야 한다.

활동이 일시중지됨 상태에서 재개됨 상태로 돌아오면 시스템은 Acitivity 인스턴스를 메모리에 남겨두고, 시스템이 onResume()을 호출할 때 인스턴스를 다시 호출한다.
이 시나리오에서는 최상위 상태가 재개됨 상태인 콜백 메서드 중에 생성된 구성요소는 다시 초기화할 필요가 없습니다.
활동이 완전히 보이지 않게 되면 시스템은 onStop()을 호출한다.

5) onStop()

활동이 사용자에게 더 이상 표시되지 않으면 중단됨 상태에 들어가고, 시스템은 onStop() 콜백을 홏출한다.
이는 예를 들어 새로 시작된 활동이 화면 전체를 차지할 경우에 적용된다.
시스템은 활동의 실행이 완료되어 종료될 시점에 onStop()을 호출할 수도 있다.

onStop() 메서드에서는 앱이 사용자에게 보이지 않는 동운 앱은 필요하지 않은 리소스를 해제하거나 조정해야 한다.
예를 들어 앱은 애니메이션을 일시중지하거나, 세밀한 위치 업데이트에서 대략적인 위치 업데이트로 전환할 수 있다.
onPasue() 대신 onStop()을 사용하면 사용자가 멀티 윈도우 모드에서 활동을 보고 있더라도 UI 관련 작업이 계속 진행된다.

또한 onStop()을 사용하여 CPU를 비교적 많이 소모하는 종료 작업을 실행해야 한다.
예를 들어 데이터베이스를 저장할 적절한 시기를 찾지 못했다면 onStop() 상태일 때 저장할 수 있다.

활동이 중단됨 상태에 들어가면 Activity 객체는 메모리 안에 머무리게 된다.
이 객체가 모든 상태 및 멤버 정보를 관리하지만 창 관리자와 연결되어 있지는 않다.
활동이 다시 시작되면 이 정보를 다시 호출한다.
최상위 상태가 재개됨 상태인 콜백 메서드 중에 생성된 구성요소는 다시 초기화할 필요가 없다.
또한 시스템은 레이아웃에 있는 각 View 객체의 현재 상태도 기록한다.

활동은 정지됨 상태에서 다시 시작되어 사용자와 상호작용하거나, 실행을 종료하고 사라진다. 활동이 다시 시작되면 시스템은 onRestart()를 호출한다. Activity가 실행을 종료하면 시스템은 onDestroy()를 호출한다.

6) onDestroy()

onDestroy()는 활동이 소멸되지 전에 호출된다.
시스템은 다음 중 하나에 해당할 때 이 콜백을 호출한다.
1. (사용자가 활동을 완전히 닫거나 활동에서 finish()가 호출되어) 활동이 종료되는 경우
2. 구성 변경(예: 기기 회전 또는 멀티 윈도우 모드)으로 인해 시스템이 일시적으로 활동을 소멸시키는 경우

활동이 소멸되는 이유를 결정하는 로직을 입력하는 대신 ViewModel 객체를 사용하여 활동의 관련 뷰 데이터를 포함해야 한다.
활동이 구성 변경으로 인해 다시 생성될 경우 ViewModel은 그대로 보존되어 다음 활동 인스턴스에 전달되므로 추가 작업이 필요하지 않다.
활동이 다시 생성되지 않을 경우 ViewModel은 onCleared() 메서드를 호출하여 활동이 소멸되기 전에 모든 데이터를 정리해야 한다.

onDestroy() 콜백은 이전의 콜백에서 아직 해제되지 않은 모든 리소스(예: onStop())를 해제해야 한다.

- 상태 저장

1) 인스턴스 상태

시스템이 이전 상태를 복원하기 위해 사용하는 저장된 데이터를 인스턴스 상태 라고 하며, 이는 Bundle 객체에 저장된 키-값 쌍의 컬렉션이다.
기본 적으로 시스템은 Bundle 인스턴스 상태를 활용하여 활동 레이아웃의 각 View 객체 관련 정보를 저장한다.
** Android 시스템이 활동에서 뷰의 상태를 복원하기 위해서는 android:id 속성으로 제공되는 고유 ID가 각 뷰마다 있어야 한다.

2) onSaveInstanceState()를 사용하여 간단하고 가벼운 UI 상태 저장

활동이 정지되기 시작하면 인스턴스 상태, Bundle에 상태 정보를 저장할 수 있도록 시스템이 onSaceInstanceState() 메서드를 호출한다.
이 메서드의 기본 구현은 EditText 위젯 내 텍스트 또는 ListView 위젯의 스크롤 위치와 같은 활동의 뷰 계층 구조에 대한 임시 정보를 저장한다.

활동의 추가적인 인스턴스 상태 정보를 저장하려면 onSaveInstanceState()를 재정의하고, 활동이 예상치 못하게 소멸될 경우 저장되는 Bundle 객체에 키-값 쌍을 추가해야 한다. onSaveInstanceState()를 재정의할 경우 기본 구현에서 뷰 계층 구조의 상태를 저장하고자 한다면 상위 클래스 구현을 호출해야 한다.
예:

override fun onSaveInstanceState(outState: Bundle?) {
    // Save the user's current game state
    outState?.run {
        putInt(STATE_SCORE, currentScore)
        putInt(STATE_LEVEL, currentLevel)
    }

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(outState)
}

companion object {
    val STATE_SCORE = "playerScore"
    val STATE_LEVEL = "playerLevel"
}

** 사용자가 활동을 명시적으로 닫는 경우 또는 finish()가 호출된 경우에는onSaveInstanceState()가 호출되지 않는다.

3) 저장된 인스턴스 상태를 사용하여 활동 UI 상태 복원

활동이 이전에 소멸된 후 재생성되면, 시스템이 활동에 전달하는 Bundle로부터 저장된 인스턴스 상태를 복구할 수 있다. onCreate() 및 onRestoreInstanceState() 콜백 메서드 둘 다 인스턴스 상태 정보를 포함하는 동일한 Bundle을 수신한다.

onCreate() 메서드는 시스템이 활동의 새 인스턴스를 생성하든, 이전 인스턴스를 재생성하든 상관없이 호출되므로 읽기를 시도하기 전에 번들 상태가 null인지 반드시 확인해야 한다. null일 경우, 시스템은 이전에 소멸된 활동의 인스턴스를 복원하지 않고 새 인스턴스를 생성한다.
예:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState) // Always call the superclass first

    // Check whether we're recreating a previously destroyed instance
    if (savedInstanceState != null) {
        with(savedInstanceState) {
            // Restore value of members from saved state
            currentScore = getInt(STATE_SCORE)
            currentLevel = getInt(STATE_LEVEL)
        }
    } else {
        // Probably initialize members with default values for a new instance
    }
    // ...
}

onCreate() 중에 상태를 복원하는 대신, onRestoreInstanceState()를 구현하는 방법을 선택할 수 있다.
이는 시스템이 onStart() 메서드 다음에 호출합니다.
시스템은 복원할 저장 상태가 있을 경우에만 onRestoreInstanceState()를 호출한다. 따라서 Bundle이 null인지 확인할 필요가 없다.

override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState)

    // Restore state members from saved instance
    savedInstanceState?.run {
        currentScore = getInt(STATE_SCORE)
        currentLevel = getInt(STATE_LEVEL)
    }
}

** 항상 onRestoreInstanceState()의 상위 클래스 구현을 호출하여 기본 구현에서 뷰 계층 구조의 상태를 복원할 수 있도록 한다.

2. Activity 간 이동

- 다른 활동에서 활동시작하기

활동이 새로 시작하려는 활동에서 결과를 받기를 원하는지 여부에 따라 startActivity()나 startActivityForResult() 메서드 중 하나를 사용하여 새 활동을 시작한다.
두 가지 경우 모두 Intent 객체를 전달한다.

Intent 객체는 시작하고자 하는 활동을 정확히 나타내거나, 실행하고자 하는 작업의 유형을 설명한다.

1) startActivity()

새로 시작된 활동이 결과를 반환할 필요가 없을 경우 현재 활동이 startActivity() 메서드를 호출하면 이를 시작할 수 있다.

val intent = Intent(this, SignInActivity::class.java)
startActivity(intent)
val intent = Intent(Intent.ACTION_SEND).apply {
    putExtra(Intent.EXTRA_EMAIL, recipientArray)
}
startActivity(intent)

인텐트에 추가된 EXTRA_EMAIL은 이메일이 전송되어야 할 이메일 주소의 문자열 배열이다. 이메일 애플리케이션이 이 인텐트에 응답하면 애플리케이션은 추가로 제공된 문자열 배열을 읽은 다음, 이를 이메일 작성 양식의 '수신' 필드에 배치합니다. 이 상황에서 이메일 애플리케이션의 활동이 시작되고 사용자가 작업을 끝내면 내 활동이 재개된다.

2) startActivityForResult()

활동이 종료될 때 결과를 반환받고자 할 수도 있습니다. 예를 들어 사용자가 연락처 목록에서 어떤 사람을 선택할 수 있도록 하는 활동을 시작할 수 있다.
이렇게 하려면 startActivityForResult(Intent, int) 메서드를 호출해야 한다.
결과는 onActivityResult(int, int, Intent) 메서드를 통해 반환된다.

class MyActivity : Activity() {
    // ...

    override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
            // When the user center presses, let them pick a contact.
            startActivityForResult(
                    Intent(Intent.ACTION_PICK,Uri.parse("content://contacts")),
                    PICK_CONTACT_REQUEST)
            return true
        }
        return false
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
        when (requestCode) {
            PICK_CONTACT_REQUEST ->
                if (resultCode == RESULT_OK) {
                    startActivity(Intent(Intent.ACTION_VIEW, intent?.data))
                }
        }
    }

    companion object {
        internal val PICK_CONTACT_REQUEST = 0
    }
}

- 활동 조정

한 활동이 다른 활동을 시작하면 두 개 활동 모두 수명 주기가 전환된다.

활동 A가 활동 B를 시작할 때 발생하는 작업 순서는 아래와 같다.
1. 활동 A의 onPause() 메서드가 실행된다.
2. 활동 B의 onCreate(), onStart() 및 onResume() 메서드가 순차적으로 실행된다.
(이제 사용자 포커스는 활동 B에 있다)
3. 그런 다음, 활동 A가 더 이상 화면에 표시되지 않는 경우 이 활동의 onStop() 메서드가 실행된다.

좋은 웹페이지 즐겨찾기