이번에 안드로이드 액티비티 개념에 대해서 다시 공부할 일이 있었는데 해당 내용을 포스팅해놓으면 좋을거 같아서 포스팅하겠습니다. 해당 내용은 안드로이드 공식 홈페이지의 액티비티작업 및 백 스택을 참고하여 작성하였습니다.

액티비티

Android의 대표적인 구성 요소 중 하나인 Activity는 UI를 구성하는 기본 단위입니다.


액티비티의 3가지 상태

  1. 재개됨 (Resumed) : 액티비티가 화면 포그라운드에 있고 포커스를 갖습니다.
  2. 일시정지됨 (Paused) : 액티비티가 일부가 가려진 상태, 이때는 액티비티가 완전히 살아 있지만(Activity 객체가 메모리에 보관되어 있고, 모든 상태 및 멤버 정보를 유지하며 창 관리자에 붙어 있는 상태로 유지), 메로리가 부족한 경우 시스템이 중단시킬 수도 있습니다.
  3. 정지됨(Stopped) : 액티비티가 다른 액티비티에 완전히 가려진 상태, 이때는 액티비티가 완전히 살아 있지만(Activity 객체가 메모리에 보관되어 있고, 모든 상태 및 멤버 정보를 유지하며 창 관리자에 붙어 있는 상태로 유지), 메로리가 부족한 경우 시스템이 중단시킬 수도 있습니다.

액티비티 생명 주기(Lifecycle)

액티비티 프로그램의 상태에 따른 생명 주기를 갖는데 각각의 생명주기마다 호출되는 콜백 메서드가 따로 있습니다. 이 부분에 대해서 알아보도록 하겠습니다.

출처 : 구글 안드로이드 공식 홈페이지

  1. onCreate() : 액티비티를 생성, 액티비티의 필수 구성 요소를 초기화, setContentView()를 호출해서 액티비티에 UI를 정의할 수 있음
  2. onStart() : 액티비티가 보여지기 시작함
  3. onResume() : 액티비티가 시작되고 사용자와 상호작용하기 직전에 호출
  4. onPause() : 액티비티가 부분적으로 가려짐, 사용자가 액티비티를 떠난다는 신호, 현재 사용자가 세션을 넘어서 정보가 지속되어야 한다면 변경 사항을 커밋하기에 가장 적절한 콜백입니다. (사용자가 돌아오지 않을 수도 있음)
  5. onStop() : 액티비티가 더 이상 사용자에게 표시되지 않으면 호출됩니다.
  6. onRestart() : 액티비티가 중단되었다가 다시 시작되기 전에 호출됩니다.
  7. onDestroy() : 액티비티가 소멸되기 직전에 호출됩니다. 액티비티가 받는 마지막 호출입니다.

액티비티의 수명

  • Whole Lifecycle : onCreate() ~ onDestroy(), 액티비티 전체 생명 주기
  • Visible Lifecycle : onStart() ~ onStop(), 가시적으로 보이는 액티비티 생명 주기
  • Foreground Lifecycle : onResume() ~ onPause(), 포그라운드에 떠있는 액티비티 생명 주기

액티비티 상태 저장

화면이 회전하거나, onPause()나 onStop() 상태에서 시스템이 메모리를 필요로 하면 강제로 액티비티가 종료가 되고, 이로인해 기존에 있던 정보가 날라갈 수 있습니다. 이를 위해 데이터를 저장하여 액티비티가 시작될때 복원이 되게끔 처리해야 합니다.

- 화면 회전 -
화면이 회전이 되면 onPause() -> onStop() -> onDestroy() 가 호출이 되며 기존 액티비티가 소멸됩니다. 그리고 다시 onCreate() -> onStart()
-> onResume()을 호출하여 새로 액티비티를 생성하는데 이때 기존에 액티비티가 가지고 있던 정보가 사라질 수 있습니다.

저장 생명 주기 : onPause() -> onSaveInstanceState() -> onStop() -> onDestroy()

override fun onSaveInstanceState(outState: Bundle?) {
    super.onSaveInstanceState(outState)

    outState?.putString("sample", "savedData")
    outState?.putString("sample2", "savedData2")
}

복구 생명 주기 : onCreate() -> onStart() -> onRestoreInstanceState() -> onResume()

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

    val sample = savedInstanceState?.get("sample")
    val sample2 = savedInstanceState?.get("sample2")
}

API 21부터 파라미터가 2개인 onSaveInstanceState()와 onRestoreInstanceState()가 추가 되었습니다.

차이는 아래와 같습니다.

  • onSaveInstanceState
    • 파라미터 1개 : onPause() 다음에 무조건 실행
    • 파라미터 2개 : onPause() 다음에 화면 전환같은 상황에서 액티비티가 종료된 것인지 판단하여 실행
  • onRestoreInstanceState
    • 파라미터 1개 : onStart() 다음에 무조건 실행
    • 파라미터 2개 : onStart() 다음에 Bundle이 있을때만 실행
override fun onSaveInstanceState(outState: Bundle?, outPersistentState: PersistableBundle?) {
    super.onSaveInstanceState(outState, outPersistentState)
    
    // Do something
}


override fun onRestoreInstanceState(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
    super.onRestoreInstanceState(savedInstanceState, persistentState)

    // Do something
}

하지만 최근에 Android Jetpack으로 나온 LiveData는 생명주기에 맞게 옵저버 패턴으로 구현이 되어서 LiveData를 사용하는게 훨씬 좋습니다.


백 스택 (= Activity Task)

보통 안드로이드 앱에서는 단일 액티비티가 아닌 여러 액티비티를 이용하여 UI를 구성하게 되는데 이때 새로 열리는 액티비티와 기존의 열리는 액티비티는 각각 백 스택을 통해서 관리가 됩니다.

새로 액티비티가 생성이 되면 스택에 Push가 되면서 기존에 있던 액티비티는 아래로 감춰지게 됩니다. 하지만 Back 키를 눌러서 현재 액티비티를 종료시키면 종료된 액티비티가 POP이 되면서 그 아래에 있던 액티비티가 푸시가 됩니다.

사용 설정에 따라 다르지만 기본 설정에서는 액티비티를 호출할때마다 새로운 인스턴스를 만들어서 쌓습니다.

백 스택(=Task)는 동시에 여러개를 가질 수 있습니다.

아래 예제는 총 2개의 백 스택을 가지고 있는 상황에서 Activity 1이 포그라운드에 나오면서 TASK B가 백그라운드에 감쳐진 상황입니다.

다음은 Activity 2이 포그라운드에 나오면서 TASK A가 백그라운드에 감쳐진 상황입니다.

이렇게 백 스택(=TASK)별로 포그라운드에 있느냐 없느냐에 따라 상태가 바뀌는데 백그라운드에 있어도 기본적으로는 해당 TASK의 상태는 유지하고 있습니다.


액티비티의 기본 동작

  • 액티비티 A가 액티비티 B를 시작하면 액티비티 A는 중단되지만, 시스템이 그 상태를 (예: 스크롤 위치 및 양식에 입력된 텍스트 등) 보존합니다. 사용자가 액티비티 B에 있는 동안 뒤로 버튼을 누르면 액티비티 A가 재개되며 상태도 복원됩니다.
  • 사용자가 홈 버튼을 눌러 작업을 떠나면 현재 액티비티가 중단되고 해당 작업이 백그라운드로 들어갑니다. 시스템은 작업에 속한 모든 액티비티의 상태를 보존합니다. 사용자가 나중에 작업을 시작한 시작 관리자 아이콘을 선택하여 해당 작업을 재개하면, 그 작업이 포그라운드로 나오고 스택 맨 위에서 액티비티를 재개합니다.
  • 사용자가 Back 버튼을 누르면, 현재 액티비티가 스택에서 팝되고 소멸됩니다. 스택에 있던 이전 액티비티가 재개됩니다. 액티비티가 소멸되면, 시스템은 그 액티비티의 상태를 보존하지 않습니다.
  • 액티비티는 여러 번 인스턴스화할 수 있으며, 다른 작업에서도 이를 수행할 수 있습니다.

작업 관리

위와 같은 액티비티 작업들은 인텐트 플래그와 manifest 내에 flag를 통해서 다르게 사용할 수 있습니다.

  • taskAffinity : 해당 액티비티가 쌓일 임의로 백 스택(=Task)를 지정할 수 있습니다. (singleTask나 singleInstance가 설정되어 있어야 합니다.)
  • launchMode : 시작 모드를 정의
    • standard : default 설정으로, 여러개의 인스턴스 생성이 가능합니다.
    • singleTop : 여러개의 인스턴스 생성이 가능하지만, 호출한 액티비티가 최상위 액티비티로 있는 경우에는 인스턴스 생성없이 기존의 액티비티로 대체됩니다.
    • singleTask : 한 번에 액티비티 인스턴스 한 개씩만 존재할 수 있습니다.
    • singleInstance : singleTask와 동일합니다. 단, 인스턴스가 있는 작업에 대해 시스템이 어떤 액티비티도 시작하지 않은 경우에는 예외, 이것으로 시작한 액티비티는 모두 별개의 작업에서 열립니다.
  • allowTaskReparenting : true로 설정이 되면 taskAffinity에 설정되어 있는 작업이 나타나면 그쪽으로 작업이 옮겨집니다.
  • clearTaskOnLaunch : true로 설정이 되면 사용자가 작업을 떠났다가 다시 돌아올 경우 루트 액티비티까지 스택을 지웁니다.
  • alwaysRetainTaskState : true로 설정이 되면 사용자가 오랜 시 작업을 떠나 있어도 스택에 있는 모든 액티비티를 유지합니다.
  • finishOnTaskLaunch : 전반적으로 clearTaskOnLaunch와 같지만 작업 전체가 아니라 하나의 액티비티에서 동작합니다.

시작 모드 정의

시작 모드를 사용하면 액티비티의 새 인스턴스가 어떤 방식으로 생성할지 정할 수 있습니다.

매니페스트 사용 : Androidmanifest.xml에서 launchMode에 옵션에 설정

  • standard : default 설정으로, 액티비티는 여러 번 인스턴스화 될 수 있고, 각 인스턴스는 서로 다른 작업에 속할 수 있으며, 한 작업에 여러 개의 인스턴스가 있을 수 있습니다.

  • singleTop : 만약 A라는 액티비티가 맨위에 있을때 A 액티비티를 다시 호출하면 기존에 A라는 액티비티가 이를 대체합니다.

  • singleTask : 한 번에 액티비티 인스턴스 한 개씩만 존재할 수 있으며 singleTask가 설정된 액티비티는 새로운 Task루트 액티비티로만 존재합니다. singleTask는 위에 다른 액티비티를 쌓을 수 있습니다.

  • singleInstance : 거의 singleTask와 동일합니다. singleInstance도 새로운 Task의 루트 액티비티로만 존재하지만, singleTask와 다르게 위에 다른 액티비티를 쌓을 수 없습니다.


인텐트 플래그

  • FLAG_ACTIVITY_NEW_TASK : singleTask와 동일한 동작, 한 번에 액티비티 인스턴스 한 개씩만 존재할 수 있습니다.
  • FLAG_ACTIVITY_SINGLE_TOP : singleTop과 동일한 동작, 만약 A라는 액티비티가 맨위에 있을때 A 액티비티를 다시 호출하면 기존에 A라는 액티비티가 이를 대체합니다.
  • FLAG_ACTIVITY_CLEAR_TOP : 시작되고 있는 액티비티가 이미 현재 작업에서 실행 중인 경우 새 인스턴스를 시작하는 대신 그 위에 있는 모든 다른 액티비티가 소멸되고 해당 액티비티의 재개된 인턴스로 전달됩니다.

유사성 처리

액티비티가 어느 작업에 소속되기를 선호하는지를 나타냅니다. 설정을 하지 않으면 기본적으로 모든 액티비티는 패키지 네임과 동일합니다.

Activity에 taskAffinity를 설정하면 미리 설정된 백 스택(=Task)으로 들어가게 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.byjw.myapplication">

    <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity
                android:name=".MainActivity"
                android:taskAffinity="task.a">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
                android:name=".Activity2"
                android:taskAffinity="task.b" />

        <activity
                android:name=".Activity3"
                android:taskAffinity="task.c" />

        <activity
                android:name=".Activity4"
                android:taskAffinity="task.d" />
    </application>

</manifest>

두 가지 옵션과 함께 사용할 수 있습니다.

  • FLAG_ACTIVITY_NEW_TASK : 이 옵션이 설정된 경우 동일한 taskAffinity 설정을 가진 백 스택이 있으면 해당 작업으로 들어가고 아니면 새 백 스택을 시작합니다.
  • allowTaskReparenting : true로 설정이 되어 있으면 포그라운드로 나오면 그 작업으로 이동할 수 있습니다.

FLAG_ACTIVITY_NEW_TASK와 함께 사용하면 아래와 같이 동일한 앱이지만 두 개의 별도의 작업이 나타나게 됩니다.