Activity Lifecycle 완벽 정복하기

H43RO·2021년 8월 15일
20

Android 와 친해지기

목록 보기
3/26
post-thumbnail

💡 Android 공식 문서를 참고하여 작성한 내용입니다
https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko

Activity Lifecycle 이 뭐야?

이전 포스팅에서 소개한 Activity 라는 친구는, 사람처럼(?) 태어나고 죽기까지의 생명 주기가 있다. 액티비티가 시작되고 완전히 종료되기 까지의 주기 안에서 그 액티비티의 상태가 계속하여 바뀌게 된다. 다른 액티비티에 의해 잠시 가려진다든가, 갑자기 전화가 걸려오는 등 모든 상황에 있어 액티비티의 상태가 변화한다.

그런데 만약 사용자가 어떤 게임을 하다가 전화가 걸려와 전화를 받고 다시 돌아오니, 게임의 진행 상태가 초기화되어 있다면 매우 곤란할 것이다. 혹은, 재미있는 필터 카메라로 셀카를 찍다가 우연히 멋진 풍경을 발견하여 풍경을 예쁘게 담아주는 카메라 앱으로 풍경을 촬영하려고 보니 이전에 실행하고 있던 필터 카메라 앱이 카메라를 점유하고 있어 풍경 촬영 앱이 동작하지 않는다면 너무나도 슬플 것이다.

따라서 안드로이드 프레임워크에서는 액티비티의 '상태가 변화'할 때마다 특정 동작을 수행할 수 있도록 여러 콜백 메소드를 제공한다. 이를 'Lifecycle Callback Method' 라고 한다. 이 콜백 메소드들을 활용하면 위에서 설명한 예제 시나리오들을 개선할 수 있다.

게임 앱의 '액티비티가 화면에서 잠시 멈출 때 호출되는 콜백 메소드'에 게임의 진행 상태를 저장하는 로직을 담아둔다면?

→ 전화를 받을 때 게임 앱의 해당 콜백 메소드가 호출되어 게임의 진행 상태를 저장할 수 있음

필터 카메라 앱의 '액티비티가 화면에서 가려질 때 호출되는 콜백 메소드'카메라 자원을 반환하는 로직을 담아둔다면?

→ 풍경 카메라 앱을 실행해도 카메라를 사용할 수 있도록 할 수 있음 (또는 반드시 그렇게 해야함!)

수명주기 콜백 메소드를 잘 구현하면, 다양한 문제를 예방할 수 있고 앱이 보다 안정적으로 동작할 수 있다. 따라서 안드로이드 앱을 개발함에 있어, 상당히 중요한 부분으로 작용된다. (그래서 그런지 실제로 테크 인터뷰 문제로도 빈출된다!)

Activity Lifecycle 동작 원리

아래는 공식 문서에서 Activity Lifecycle (콜백 메소드 호출 시점) 을 시각적으로 나타낸 다이어그램이다. 핵심 콜백 메소드로는 onCreate(), onStart(), onResume(), onPause(), onStop(), onDestroy() 이렇게 6가지가 존재한다. 액티비티가 새로운 상태게 들어가게 되면, 시스템은 각 콜백 메소드를 호출하게 된다.

대신, 모든 수명주기 메소드를 구현할 필요가 없을 때도 있다. 하지만 꼭 구현해야 하는 메소드 (onCreate) 도 있고, 안정적인 사용자 경험을 위해 수명 주기 메소드에 대하여 이해하고 필요한 수명 주기 메소드를 구현하는 것이 중요하다.

Activity Lifecycle Callback Method

그럼 이제, 각 콜백 메소드가 언제 호출되고 어떤 동작을 구현하면 좋은지에 대하여 하나씩 살펴보자!

1. onCreate()

시스템이 액티비티를 생성할 때 실행되는 녀석이다. 다른 콜백 메소드들과 다르게, 이 콜백메소드는 꼭 오버라이딩하여 구현해야 한다. 이 메소드에는 액티비티 전체 수명 주기 동안 딱 한 번만 동작되는 초기화 및 시작 로직을 실행할 수 있다. 예를들어 RecyclerView 에 데이터를 바인딩한다든가, 버튼에 View.OnClickListener 를 설정하는 등의 동작을 할 수 있다.

💡 Tip

onCreate()**savedInstanceState 라는 파라미터를 수신하는데, 이는 액티비티의 이전 상태가 저장**된 Bundle 객체에 해당된다. (처음 생성된 액티비티인 경우 null 을 담고 있음) 이를 통해 이전 상태를 복원하여 화면에 표시할 수 있다.

아래는 공식문서에서 보여주는 onCreate() 메소드 구현의 예시이다. 이 예시를 보면, 멤버 변수를 정의하거나 UI 를 구성하는 등의 동작을 하는 것을 알 수 있다. 특히, setContentView() 를 통해서 XML 레이아웃 파일을 념겨줌으로써 화면에 해당 레이아웃을 그리는 것을 주목하자. setContentView()onCreate() 에 종속적인 메소드이므로, 반드시 해당 콜백 메소드 안에 구현해야 한다.

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

onCreate() 의 동작이 끝났다고 해서 액티비티가 종료되는 것이 아니고, 액티비티가 'STARTED' 상태에 진입하게 되어 시스템은 onStart()onResume() 을 연달아 호출하게 된다. 이어서 onStart() 를 살펴보자.

2. onStart()

액티비티가 onCreate() 를 호출한 뒤 액티비티가 'STARTED' 상태에 진입하게 되면 onStart() 가 호출된다. 이 메소드가 호출되면 액티비티가 사용자에게 보여지고, 포그라운드 태스크로써 사용자와 상호작용할 수 있도록 준비한다.

onStart() 는 매우 빠른 속도로 실행되고, 액티비티가 'RESUMED' 상태에 진입함과 동시에 onResume() 메소드를 호출하게 된다.

3. onResume()

액티비티가 'RESUMED' 상태에 진입하게 되면 포그라운드에 액티비티가 표시되고 앱이 사용자와 상호작용을 할 수 있는 상태가 된다. 어떤 방해 이벤트 및 인터럽트 (전화가 오거나 화면을 슬립하는 등 이벤트) 가 발생하여 사용자의 포커스가 없어지지 않는 이상, 이제 앱은 'RESUMED' 상태에 머무르게 된다.

이 상태에서, 포그라운드에서 사용자에게 액티비티가 보여지는 동안 실행해야 하는 모든 기능을 활성화 할 수 있게 된다.

💡 여기서 잠깐!

그럼 만약 위에서 설명한 방해 이벤트 및 인터럽트가 발생하게 되면 어떤 일이 일어날까? 방해 이벤트가 발생하면, 액티비티는 'PAUSED' 상태에 들어가게 되어 시스템이 onPause() 콜백 메소드를 호출하게 된다.

전화를 다 받고 오는 등, 액티비티가 다시 화면에 보여지면서 'RESUMED' 상태로 돌아오게 되면 시스템이 다시 한 번 onResume() 메소드를 호출하게 된다. 따라서, onResume() 을 구현하여 onPause() 중에 해제되는 구성요소를 다시 초기화하고, 액티비티가 재개될 때마다 필요한 이외 초기화 작업을 수행하면 된다.

아래는 액티비티가 'RESUMED' 로 상태가 바뀔 때 발행되는 ON_RESUME 이벤트를 LifecycleObserver 가 수신하게 될 때 수행할 수 있는 동작 중 하나를 예시로 든 코드이다. ( 이 포스팅에선 LifecycleObserver 에 대해서 자세히 다루지 않겠다)

class CameraComponent : LifecycleObserver {

   ...

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

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    fun releaseCamera() {
        camera?.release()
        camera = null
    }
    ...
}

위 코드는 액티비티가 'RESUMED' 상태가 될 때마다 카메라를 초기화한다. 이유는, 타 앱과 시스템 리소스 공유를 위해 'PAUSED' 상태가 될 때 메모리에서 카메라를 릴리즈하기 때문이다. 따라서, 다시 액티비티로 돌아오는 경우 (액티비티가 재개되는 경우) 카메라가 메모리 상 존재하지 않기 때문에 다시 초기화를 해주는 것이다.

어떤 액티비티 상태에서 초기화 작업을 실행하든간에, 각각에 상응하는 수명 주기 이벤트를 사용해서 리소스를 해제해야 한다. 'STARTED' 이후에 무언가를 초기화 했다면, 'STOPPED' 이후에 이를 해제하거나 종료하면되고, 'RESUMED' 이후에 무언가를 초기화 했다면, 'PAUSED' 이후에 해제해주면 된다.

4. onPause()

사용자가 잠시 액티비티를 떠났을 때 (다른 액티비티에 포커스를 뒀을 때) 호출되는 콜백 메소드이다. 즉, 해당 액티비티가 포그라운드에 있지 않게 되었다는 것을 의미 한다. (예외적으로 한 화면에 두 앱을 동시에 구동할 수 있는 멀티 윈도우 모드에서는, 멀티 윈도우의 또다른 앱에 포커스를 두어도 해당 액티비티가 포그라운드에 있으면서 onPause() 가 호출된다)

따라서 보통 onPause() 에서는, 액티비티가 포그라운드에 없을 동안 계속 실행되어서는 안 되지만 언젠가 다시 시작할 작업을 일시중지하는 작업을 수행한다.

액티비티가 'PAUSED' 상태에 진입하게 되는 여러가지 루트들

  • 앱이 실행되던 중 방해 이벤트 및 인터럽트가 발생한 경우 (전화가 갑자기 오는 경우)
  • 위에서 말한 것 처럼, 멀티 윈도우 상 다른 앱에 포커스를 두는 경우
  • 화면에 반투명(?) 액티비티가 열리는 경우 e.g. 권한 요청 다이얼로그

또한, onPause() 메소드를 사용하여 배터리 수명에 영향을 미칠 수 있는 모든 시스템 리소스, 하드웨어 센서 등을 할당 해제하면 자원을 효율적으로 사용할 수 있다.

⛔️ 꼭 짚고 넘어가야 할 사실!

onPause()아주 잠깐 실행되기 때문에 무언갈 저장하는 작업을 실행하기엔 시간이 부족할 수 있다. 따라서 onPause() 내에서는 사용자 데이터 저장, 네트워크 호출, DB 트랜잭션 등을 실행해서는 안 된다. 이렇게 부하가 큰 작업들은 onStop() 에서 수행해야 한다.

액티비티가 다시 시작되거나, 사용자에게 완전히 보여지지 않는 이상 액티비티는 'PAUSED' 상태에 머무르게 된다. 이 때 다시 액티비티가 재개되면 메모리 상 남아있던 액티비티 인스턴스를 다시 불러와 onResume() 메소드를 호출한다.

만약 액티비티가 화면에 완전히 보이지 않게 되면, onStop() 이 호출된다.

5. onStop()

액티비티가 사용자에게 더 이상 표시되지 않으면 'STOPPED' 상태에 진입하고 시스템이 onStop() 콜백 메소드를 호출한다. 즉, 새로 시작된 액티비티가 화면 전체를 차지할 경우에 해당된다. 혹은 액티비티의 실행이 완료되어 종료될 시점에 onStop() 를 호출할 수도 있다.

이 메소드에서는 필요하지 않은 리소스를 해제하거나 조정해야 한다. 예를 들어 애니메이션을 일시중지하거나, GPS 사용 시 배터리를 아끼기 위해 위치 인식 정확도를 '세밀한 위치' 에서 '대략적인 위치' 로 전환할 수 있다.

🤔 UI 관련 중지 작업을 할 때는 onPause(), onStop() 중 어떤 것을 사용해야 할까?

onStop() 을 사용해야 멀티 윈도우를 사용하여 동시에 두 앱을 구동시키고 있어도 애니메이션 등 UI 구성 요소가 멈추지 않는다. 왜냐하면, 멀티 윈도우의 다른 앱으로 포커스를 조정하는 순간 onPause() 가 호출되기 때문이다. onPause() 내에 UI 중지 작업을 수행해버리면, 멀티 윈도우 상으로 액티비티는 화면에 표시되고 있는데도 UI 가 멈추어버리는 현상이 발생하게 된다. 이러한 이유로, UI 관련 마무리 작업은 onStop() 에서 수행하는 것이 낫다.

또한, CPU 를 비교적 많이 소모하는 작업을 종료해야 한다. 예를들어 어떠한 정보를 DB 에 저장할 타이밍을 모르겠다면 onStop() 상태일 때 저장해주면 된다. 아래는 이 동작을 코드로 옮겨본 것이다.

override fun onStop() {
    // call the superclass method first
    super.onStop()

    // save the note's current draft, because the activity is stopping
    // and we want to be sure the current note progress isn't lost.
    val values = ContentValues().apply {
        put(NotePad.Notes.COLUMN_NAME_NOTE, getCurrentNoteText())
        put(NotePad.Notes.COLUMN_NAME_TITLE, getCurrentNoteTitle())
    }

    // do this update in background on an AsyncQueryHandler or equivalent
    asyncQueryHandler.startUpdate(
            token,     // int token to correlate calls
            null,      // cookie, not used here
            uri,       // The URI for the note to update.
            values,    // The map of column names and new values to apply to them.
            null,      // No SELECT criteria are used.
            null       // No WHERE columns are used.
    )
}

💡 Tip

액티비티가 'STOPPED' 상태에 들어가더라도 액티비티 객체는 메모리 안에 머무른다. 그런데 시스템이 더 우선순위가 높은 프로세스를 위해 메모리를 확보해야하는 경우, 이 액티비티를 메모리 상에서 죽이게 된다. 하지만 그럼에도 Bundle 에 View 객체 상태를 그대로 저장해두고, 사용자가 이 액티비티로 다시 돌아오게 되면 이를 기반으로 상태를 복원한다. (꽤나 잘 돌아간다)

만약 사용자가 다시 액티비티로 돌아오게 되면, 'STOPPED' 상태에서 다시 시작되어 onRestart()onStart()onResume() 이 연달아 호출되며 'RESUMED' 상태로 변화하여 다시 포그라운드 액티비티로써의 태스크를 시작하며 사용자와 상호작용을 시작한다.

사용자에 의해, 혹은 시스템에 의해 액티비티가 완전히 실행 종료될때면 시스템은 onDestroy() 콜백 메소드를 호출하게 된다.

이제 마지막으로 onDestroy() 메소드에 대하여 알아보자.

6. onDestroy()

액티비티가 완전히 소멸되기 전에 이 콜백 메소드가 호출된다. 아래와 같은 경우 액티비티가 완전히 소멸된다.

  1. finish() 가 호출되거나 사용자가 앱을 종료하여 액티비티가 종료되는 경우
  2. 화면 구성이 변경되어 (기기 회전 등) 일시적으로 액티비티를 소멸시키는 경우

액티비티가 종료되는 경우 onDestroy()마지막 라이프사이클 콜백 메소드가 된다. 만약 위의 2번 사유로 인해 호출된거라면, 시스템이 즉시 새롭게 변경된 액티비티 인스턴스를 생성하여 onCreate() 를 호출한다.

⛔️ 꼭 짚고 넘어가야 할 사실!

만약 onDestroy() 가 호출되기 까지 해제되지 않은 리소스가 있다면, 모두 여기서 해제해줘야 한다. Memory Leak (메모리 누수) 의 위험이 있다.

이후, 액티비티는 메모리 상 완전히 소멸되게 된다.


포스팅과 무관한 움짤과 함께 마치며

이번 포스팅에선 안드로이드의 Lifecycle 개념에 대하여 알아보고, 각 Cycle 마다 호출되는 콜백 메소드들의 종류도 알아보았다. 앱이 한정적인 리소스를 사용하는 만큼, 앱 개발자들은 리소스 관리에 대하여 철저히 고려해야 하며 메모리 누수의 위험을 사전에 방지해야 한다. 이를 위해 Lifecycle 개념은 필수적으로 익혀야 하며, 상황과 용법에 맞게 적절한 콜백 메소드를 구현하는 것이 중요하다.

profile
어려울수록 기본에 미치고 열광하라

1개의 댓글

comment-user-thumbnail
2022년 6월 28일

onDestroy()가 실행돼도 할당된 자원들은 자동해제가 안되나요?

답글 달기