라떼는말이야

[Android] 실습으로 알아보는 액티비티 생명주기 : Android activity lifecycle 본문

안드로이드

[Android] 실습으로 알아보는 액티비티 생명주기 : Android activity lifecycle

MangBaam 2021. 12. 9. 02:33
반응형

시작하기 전에...

본 포스트에서 사용한 코드와 코드랩은 밑의 링크에서 다운 받아 사용할 수 있습니다.

단일 액티비티(MainActivity.kt)로 구성되어 있는 앱입니다.

직접 따라하며 익혀도 되고 빠르게 개념만 익혀가도 좋습니다 😎👍

 

앞 부분은 로그를 남기는 방법에 대한 설명이니 생명 주기에 대한 내용을 바로 보시려면 초록색으로 "액티비티 라이프 사이클" 라고 적힌 부분까지 쭉 내려가면 됩니다!

 

안드로이드

 

GitHub - google-developer-training/android-kotlin-fundamentals-starter-apps: android-kotlin-fundamentals-starter-apps

android-kotlin-fundamentals-starter-apps. Contribute to google-developer-training/android-kotlin-fundamentals-starter-apps development by creating an account on GitHub.

github.com

코드랩

 

Android Kotlin Fundamentals: Lifecycles and logging

In this codelab, you learn about the activity and fragment lifecycle.

developer.android.com

 


 

안드로이드를 개발하려면 생명 주기를 알아야 한다

 

안드로이드를 개발하면서 가장 먼저 마주치는 것은 액티비티일 것이다.

무작정 책이나 강의의 실습을 따라하다 보면 대부분의 코드를 onCreate()에서 작성하는 경우가 많다.

그러나 안드로이드에서 액티비티의 생명주기를 파악하는 것은 아주 중요한 문제이다. 모바일 기기와 같이 메모리, cpu 등의 자원이 제한된 환경에서 다양한 어플리케이션들이 한정된 자원을 효율적으로 나눠 쓰기 위해서 생명 주기가 존재하는 것이기 때문에 이 글을 작성하게 되었다.

위와 같은 그림을 많이 봤을 것이다.

실제로는 더 많은 콜백 함수들이 있지만 onCreate(), onRestart(), onStart(), onResume(), onPause(), onStop(), onDestroy() 의 콜백 함수가 가장 핵심적이다.

 

생명 주기 확인하기 (로그 찍는 방법)

로그란 앱이 실행하는 동안에 콘솔에 짧은 메시지를 기록할 수 있는 기능이다. 앱이 실행되는 동안 액티비티의 생명주기에 따라 각각의 콜백 함수가 실행될 것이다. 콜백 함수 내부에 로그를 삽입한다면 각 콜백 함수의 실행 시점을 알 수 있다.

안드로이드 스튜디오의 Logcat

로그를 확인하려면 안드로이드 스튜디오 하단의 Logcat 탭을 확인하면 된다.

 

Log를 사용한 방법

Log.i("MainActivity", "onCreate Called")
Log.d("MainActivity", "onCreate Called")
Log.w("MainActivity", "onCreate Called")
Log.e("MainActivity", "onCreate Called")

Log를 사용하면 로그를 남길 수 있다.

첫 번째 인자인 "MainActivity"는 TAG이다.  Logcat에는 몰라도 될 로그까지 굉장히 많이 찍히고 빠르게 올라가기 때문에 첫 번째 인자로 입력한 TAG로 결과를 확인할 수 있다.

또, Log 뒤에 i, d, w, e가 붙는데 각각은 information, debug, warning, error 를 의미한다. (특히 w는 파란색, e는 빨간 글씨로 표시된다)

위 코드의 결과로 Logcat에 로그가 찍힌 것을 알 수 있다.

위와 같이 TAG 만 검색해서 찾을 수도 있고, E/TAG 와 같이 어떤 형태인지 지정할 수도 있다.

Timber를 사용한 방법

Timber는 Log와 마찬가지로 로그를 찍기 위한 라이브러리이다. Log 대신 Timber를 사용하는 이유는 크게 다음과 같다.

  • 태그를 일일이 지정하지 않고, 클래스 이름(MainActivity와 같은)을 기반으로 로그를 남긴다.
  • 안드로이드 앱 개발 과정에서 로그가 필요하지만 릴리즈 시점에서는 필요하지 않다. Log를 사용하면 이를 직접 비활성화 해야 하지만 Timber는 그러지 않아도 된다.
  • 크래쉬-레포팅 라이브러리와 통합할 수 있다.

1. build.gradle(Module:app)에 Timber 추가 (https://github.com/JakeWharton/timber#download 에서 최신 버전 확인)

implementation 'com.jakewharton.timber:timber:5.0.1'

추가 이후 Sync Now 버튼을 눌러 그래들을 재빌드한다.

2. Application 클래스 생성

Application 클래스는 앱 전체 전역 상태를 포함하는 베이스 클래스이며 OS와 상호 작용하는 메인 오브젝트이다.

Manifest를 확인해보면 각종 activity 들의 부모로서 application이 존재한다. 이처럼 안드로이드 프로젝트가 생성되면 기본으로 Application 크래스가 생긴다.

Timber는 앱 전체에서 사용되며 Timber를 사용하기 위해서는 한 번 초기화 작업이 필요하기 때문에 Application 클래스에서 초기화하는 것이 적합하다.

주의: 위 설명만 보고 앱 전체에서 사용할 전역 변수를 Application 클래스에서 선언할 생각을 할 수 있지만 이러한 읽기 및 쓰기 가능한 정적 변수(var 등으로 선언되는)는 오류를 발생시킬 수 있다.

ClickerApplication이라는 이름으로 코틀린 클래스를 하나 생성한다.

위와 같이 Application을 상속하고, onCreate() 내부에 Timber.plant(Timber.DebugTree())를 작성한다. Timber를 초기화하는 과정이다.

package com.example.android.dessertclicker

import android.app.Application
import timber.log.Timber

class ClickerApplication: Application() {
    override fun onCreate() {
        super.onCreate()

        Timber.plant(Timber.DebugTree())
    }
}

AndroidManifest.xml를 열어서 다음과 같이 <application 부분에 android:name=".clickerApplication"과 같이 name 속성을 부여한다.

 

Timber 사용 방법

위에서 설명한 Log를 사용한 방법과 거의 유사하다. 다만 TAG값을 따로 받지 않는다.

MainActivity를 열어서 다음과 같이 생명 주기의 콜백 함수 내부에 Timber로 작성한다.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Timber.i("onCreate called")

		// ...
        
    }

    override fun onStart() {
        super.onStart()
        Timber.i("onStart Called")
    }

    override fun onResume() {
        super.onResume()
        Timber.i("onResume Called")
    }

    override fun onDestroy() {
        super.onDestroy()
        Timber.i("onDestroy Called")
    }

    override fun onStop() {
        super.onStop()
        Timber.i("onStop Called")
    }

    override fun onRestart() {
        super.onRestart()
        Timber.i("onRestart Called")
    }

    override fun onPause() {
        super.onPause()
        Timber.i("onPause Called")
    }

 


액티비티 라이프 사이클

위 그림을 다시 한번 가져와봤다. 그림과 설명을 비교하며 확인하면 이해가 좀 더 쉬울 것이다. 비슷한 성격을 가진 콜백 함수끼리 그룹 지어서 설명한다.

 

onCreate()

onCreate()는 액티비티가 실행될 때 최초에 1번 실행된다. 좀 더 정확히 말하면 액티비티가 메모리에 올라갈 때 실행된다.

그렇기 때문에 어떤 변수를 초기화하거나 뷰와 코드를 연결하거나 이벤트 리스터를 부착하는 등의 작업을 onCreate()에서 하게 된다.

onDestroy()

onCreate()가 Create 된 시점(메모리에 올라간 시점)에 실행된다면 onDestroy()는 메모리에서 제거된 시점에 실행된다. 그렇기에 사용 중이던 메모리 자원을 이 시점에 해제하기 적합하다.


onRestart()

onRetart()는 최초 앱 실행 시에는 실행되지 않는 함수이다. 하지만 액티비티가 보이지 않은 시점 이후에 다시 보이기 시작한 시점에 onRestart()가 실행된다. 그렇기에 최초에는 실행하지 않고 싶을 때 적절히 사용할 수 있다.

예를 들어 사용자에게 앱 평가를 부탁하는 UI를 띄우는 경우 앱을 켜자 마자 실행하는 것보다 사용자가 앱 사용 중에 띄우는 것이 더 적합할 것이다.

바로 밑에서 onStart()를 확인할텐데 onStart()는 화면에 보여지는 시점에 실행되는데, onRestart()화면에 보여지기 직전에 실행된다.


onStart()

onStart()가 실행되는 시점은 화면에 보여지는 시점이다. 즉, 액티비티가 다른 뷰에 의해 가려졌다가 다시 나타나는 경우에 onStart()는 다시 실행된다. 최초에 1번 실행되는 onCreate()와는 다른 특징이다.

onStop()

onStart()가 화면에 보여지는 시점에 실행된다면 onStop()화면에 보이지 않은 시점에 실행된다.


onResume()

onResume()은 액티비티가 포커즈를 받은 경우. 즉, 사용자와 상호 작용 할 수 있는 상태가 된 시점에 실행된다. onStart()와 마찬가지로 다른 뷰 뒤에 가려지거나 백그라운드로 이동하는 등 사용자와 상호 작용 할 수 없는 상태가 되었다가 다시 앞으로 나오게 되면 onResume()은 다시 실행된다.

onResume 이라는 이름 때문에 헷갈릴 수 있는데 최초에 실행시에도 onResume()은 실행된다.

onPause()

onResume()이 포커즈를 받은 경우 실행되었다면, onPause()포커즈를 잃은 시점에 실행된다.


 

상황별 라이프 사이클 확인

1. 앱을 최초로 실행시킨 경우

앱 최초 실행

앱을 최초로 실행하게 되면 위와 같은 결과를 볼 수 있다. (I/MainActivity 로 필터링 했다)

onCreate() -> onStart() -> onResume() 순으로 실행된다.

  1. 최초에 한 번 onCreate() 실행
  2. 앱이 화면에 보이는 시점에 onStart() 실행
  3. 사용자가 (버튼을 클릭하는 등) 상호 작용을 할 수 있도록 준비된 시점에 onResume() 실행

 

2. 앱을 실행한 후 닫은 경우

앱 실행 후 닫기

앱을 종료하니 위에서 기록된 onResume 이후로 onPause() -> onStop() -> onDestroy() 순으로 실행된다.

  1. 사용자가 상호 작용을 할 수 없는 시점에 onPause() 실행
  2. 앱이 화면에 보이지 않게 되면서 onStop() 실행
  3. 앱이 메모리에서 해제되면서 onDestroy() 실행

 

3. 앱을 닫은 후 다시 켠 경우

앱 다시 열기

앱을 다시 열었더니 onCreate() -> onStart() -> onResume() 이 다시 실행된다.

1번 상황과 같은 과정

 

4. 기기의 홈 버튼을 눌러 앱을 백그라운드로 보낸 경우

홈 버튼을 눌러 백그라운드로 보내기

앱을 아예 껐을 때와 다르게 onDestroy()가 실행되지 않고 onPause() -> onStop() 이 실행된다.

  1. 액티비티가 포커즈를 잃으면서 onPause() 실행
  2. 화면에 더 이상 보이지 않게 되면서 onStop() 실행
  3. 아직 백그라운드에 있으면서 메모리를 차지하고 있기 때문에 onDestroy()는 실행되지 않음
  4. 그러나 이 상태에서 시간이 오래 지나거나 다른 앱을 사용하면서 메모리가 부족한 경우 OS가 메모리를 확보하기 위해 백그라운드에서 실행 중이던 앱을 정리할 수 있다. 이 경우에 onDestroy()가 실행되며 메모리에서 제거될 수 있다.

 

5. 최근 실행 앱에서 앱을 다시 활성화한 경우

최근 앱에서 다시 실행

onRestart() -> onStart() -> onResume() 순으로 실행된다.

  1. 백그라운드에 있던 앱이 포그라운드로 나오면서 화면에 보이기 직전에 onRestart() 최초 실행
  2. 화면에 보이는 시점에 onStart() 실행
  3. 앱이 포커즈를 받은 시점에 onResume() 실행

 

6. ✨ (공유 버튼으로) 화면의 일부만 가려진 경우

화면이 일부만 가려진 경우
화면이 일부만 가려진 경우 Log

위와 같이 화면이 일부만 가려지는 경우가 있다.

onPause() 만 실행된다.

  1. 액티비티가 포커즈를 잃으면서 (사용자와 상호 작용 할 수 없는 상태) onPause() 실행
  2. 하지만 아직 화면이 완전히 가려진 것이 아니므로 onStop()은 실행되지 않음

 

7. 6번 이후 공유 하거나 공유 화면을 닫은 경우

onResume() 실행된다.

  1. 화면이 다시 포커즈를 받았으므로 onResume() 실행
  2. 화면이 완전히 가려 onStop() 된 것이 아니므로 onStop()의 짝인 onStart() 역시 실행되지 않음

 

8. ✨ 화면을 회전한 경우

화면을 회전한 경우

onPause() -> onStop() -> onDestroy() -> onCreate() -> onStart() -> onResume() 순으로 실행된다.

  1. onPause() -> onStop() -> onDestroy()는 액티비티가 종료되는 과정과 동일한 순서이다.
  2. onCreate() -> onStart() 0> onResume()은 액티비티가 새로 시작되는 과정과 동일한 순서이다.
  3. 위와 같이 실행되는 이유는 화면이 회전하게 되면 모든 부분을 다시 새로 그려야 하기 때문이다.
  4. onDestroy() 이후 onCreate()가 다시 실행되기 때문에 가지고 있던 상태를 모두 잃게 된다.
  5. 이를 방지하기 위해 Manifest.xml에 설정하거나 특정 메소드를 사용할 수도 있지만 가장 좋은 방법은 뷰모델과 라이브 데이터를 사용하는 것이다.
  6. 뷰모델은 액티비티의 생명 주기에 영향을 받지 않기 때문에 앱이 실행되는 동안 유지되어야 하는 값과 상태 데이터를 뷰모델로 분리해서 관리할 수 있다. 추후에 다른 포스트에서 다룰 예정이다.

이상 여기까지 안드로이드 액티비티의 생명 주기에 대한 설명이었다. 이 포스트에서 예제로 사용한 앱은 단일 액티비티로 구성되어 있기 때문에 액티비티와 앱을 혼용하여 사용했는데 정확히 말하면 액티비티라고 하는 것이 맞다.

앱(액티비티)이 실행/종료되는 시점, 앱(액티비티)이 보이는/보이지 않는 시점, 앱(액티비티)이 포커즈를 얻는/잃는 시점 으로 그룹지어 생각하면 아주 어려운 내용은 아닌 듯 싶다.

 

반응형
Comments