Android

액티비티 (5) : 태스크와 백 스택

까망사과 2022. 5. 11. 19:00

태스크는 사용자와 상호작용하는 액티비티의 모음이다. 각 액티비티는 스택(백 스택)에 열린 순서대로 정렬된다.

예를 들어 이메일 앱에서 메시지 목록을 표시하는 액티비티가 있고 메시지를 선택하면 이를 표시하는 새 액티비티가 열린다고 하자. 이 새 액티비티는 백 스택에 추가된다. 그러고 나서 사용자가 뒤로 돌아가는 동작을 취하면 새 액티비티가 종료되고 백 스택에서 pop된다.

 

태스크의 수명주기와 백 스택

디바이스의 홈 화면은 대부분의 태스크의 시작 지점이다. 사용자가 앱 아이콘이나 앱 런처(또는 홈 화면)의 바로가기를 터치하면 해당 앱의 태스크가 포그라운드로 나온다. 해당 앱에 대한 태스크가 없다면 새 태스크가 생성되고 앱의 메인 액티비티가 백 스택에 루트 액티비티로서 열린다.

 

현재 액티비티가 다른 액티비티를 시작하면 새 액티비티는 백 스택에 push되고 포커스를 얻는다. 이전 액티비티는 백스택에 중단된 상태로 남아있다. 액티비티가 중단되면 시스템이 해당 액티비티의 현재 UI 상태를 유지한다. 사용자가 뒤로 가기 동작을 취하면 현재 액티비티가 백스택에서 pop(액티비티가 소멸됨)되고 이전 액티비티가 이전 UI 상태가 복구된 채로 재개된다. 백스택 안에 있는 액티비티는 재정렬되지 않고 push(시작될 때) 또는 pop(뒤로 가기로 인해 벗어날 때)되기만 한다. 이런 식으로 백 스택은 후입선출(LIFO) 구조로 동작한다.

 

사용자가 계속 뒤로가기 동작을 취하면 홈 화면(또는 태스크가 시작될 때 실행 중이던 액티비티)으로 돌아갈 때까지 백스택의 각 액티비티는 pop되어 이전의 것을 표시하기를 반복한다. 모든 액티비티가 백스택에서 제거되면 태스크는 더 이상 존재할 수 없다.

 

루트 런처 액티비티에 대한 뒤로가기 동작

루트 런처 액티비티란 ACTION_MAINCATEGORY_LAUNCHER가 함께 포함된 인텐트 필터가 선언된 액티비티이다.

이러한 액티비티는 앱 런처에서 앱으로 진입하는 진입점이며 태스크를 시작하는 데 사용되기 때문에 특별하다.

 

사용자가 루트 런처 액티비티에서 뒤로가기 동작을 취하면 디바이스가 실행하는 안드로이드 버전에 따라 시스템이 이벤트를 다르게 처리한다.

  • 안드로이드 11 이하
    루트 런처 액티비티가 종료된다.
  • 안드로이드 12 이상
    시스템은 루트 런처 액티비티를 종료하는 대신, 이와 태스크를 백그라운드로 이동시킨다. 이 동작은 홈 버튼을 누르는 등의 동작을 취하여 앱 밖으로 이동할 때의 기본 시스템 동작과 일치한다. 대부분의 경우 이 동작은 사용자가 앱을 완전히 재시작하지 않고 더 빠르게 앱을 재개할 수 있다는 것을 의미한다.
    뒤로가기 동작을 사용자 정의로 제공해야 한다면 onBackPressed()를 재정의하기보다는 AndroidX의 Activity API를 사용하기를 권장한다. AndroidX Activity API는 시스템의 뒤로 가기 동작을 방해하는 컴포넌트가 없으면 자동으로 적절한 시스템 동작으로 종속된다. 하지만 뒤로 이동을 처리하고 액티비티를 종료하도록 onBackPressed()를 재정의한다면 종료하는 대신 super.onBackPressed()를 호출해야 한다. 이를 호출하면 해당 액티비티와 태스크를 적절한 때에 백그라운드로 이동시키고 앱 전체에서 사용자에게 보다 일관성 있는 이동 경험을 제공한다.

 

백그라운드 및 포그라운드 태스크

태스크는 사용자가 새 태스크를 시작하거나 홈 화면으로 이동할 때 백그라운드로 이동할 수 있는 응집 단위이다.

백그라운드에 있는 동안 태스크의 모든 액티비티는 중단되지만 백 스택은 온전하게 유지된다. 태스크는 다른 태스크가 실행되는 동안 단순히 포커스를 잃는 것뿐이다. 태스크는 포그라운드로 돌아와 사용자는 떠났던 액티비티를 다시 이어갈 수 있다.

 

백그라운드에서 여러 개의 태스크를 한 번에 유지할 수 있다. 하지만 사용자가 여러 백그라운드 작업을 동시에 실행하면 시스템이 메모리 확보를 위해 백그라운드 액티비티를 제거할 수 있으며 이로 인해 액티비티 상태가 손실될 수 있다.

 

여러 액티비티 인스턴스

백 스택 안의 액티비티는 재정렬되지 않으므로, 사용자가 1개 이상의 액티비티에서 특정 액티비티를 시작할 수 있다면 해당 액티비티의 새 인스턴스가 생성되고 백 스택에 push된다(이전 인스턴스를 꼭대기로 가져오지 않음). 그렇게 앱 안의 한 액티비티가 여러 번 인스턴스화될 수 있다(다른 태스크에서도). 사용자가 뒤로가기를 하면 액티비티의 각 인스턴스가 열린 순서대로 나타난다. 하지만 액티비티가 2번 이상 인스턴스화되지 않도록 동작 방식을 수정할 수 있다.

 

멀티 윈도우 환경

멀티 윈도우 모드에서 여러 앱이 동시에 실행될 때 시스템은 각 창에 대해 태스크를 별도로 관리한다. 각 창은 태스크를 여러 개 가질 수 있다. 

 

요약

액티비티와 태스크의 기본 동작을 요약하면 다음과 같다.

  • 액티비티 A가 액티비티 B를 시작하면 액티비티 A는 중단되지만 시스템은 이의 상태(스크롤 위치나 양식에 입력된 텍스트 등)를 유지한다. 사용자가 액티비티 B에서 뒤로가기를 하면 액티비티 A가 상태가 복구되어 재개된다.
  • 사용자가 홈 버튼을 눌러 태스크에서 벗어나면 현재 액티비티는 중단되고 이의 태스크가 백그라운드로 이동한다. 시스템은 태스크 안 모든 액티비티의 상태를 유지한다. 이후에 사용자가 태스크를 시작한 런처 아이콘을 선택하여 태스크를 재개하면 해당 태스크가 포그라운드로 나오고 백 스택의 꼭대기에 있는 액티비티가 재개된다.
  • 사용자가 백 버튼을 누르면 현재 액티비티가 백 스택에서 pop되고 소멸된다. 스택에 있는 이전 액티비티가 재개된다. 액티비티가 소멸되면 시스템은 해당 액티비티의 상태를 더 이상 유지하지 않는다. 위에서 설명한 것처럼 안드로이드 12 이상의 버전을 실행하는 경우 동작 방식이 다르다.
  • 액티비티는 여러 번 인스턴스화될 수 있다(다른 태스크에서도).

 


태스크 관리

안드로이드 시스템은 연속적으로 시작되는 모든 액티비티를 동일한 태스크의 백 스택에 배치한다. 이 방식은 대부분 앱에 효과적이지만 동작 방식을 수정해야 할 수 있다. 앱의 액티비티가 시작될 때 현재 태스크 안에 배치되지 않고 새 태스크를 시작하도록 할 수 있다. 또는 액티비티가 시작될 때 새 인스턴스를 생성하지 않고 기존 인스턴스를 백 스택의 꼭대기로 끌어올 수 있다. 또는 사용자가 태스크에서 벗어날 때 루트 액티비티를 제외한 모든 액티비티가 백 스택에서 제거되도록 할 수 있다.

 

런치모드 정의하기

런치모드는 액티비티의 새 인스턴스가 현재 태스크와 연결되는 방식을 정의한다. 두 가지 방법으로 런치모드를 정의할 수 있다.

  • 매니페스트 파일 사용
    <activity> 요소의 launchMode 속성 값을 지정한다.
  • 인텐트 플래그 사용
    startActivity()에 전달할 Intent 객체에 플래그를 지정한다.

만약 두 가지 방법을 모두 사용한다면 인텐트 플래그가 우선시된다.

몇 가지 런치모드는 매니페스트 파일에서만 사용할 수 있으며 반대로 인텐트 플래그로만 사용할 수 있는 런치모드가 있다.

 

매니페스트 파일에서 런치모드 정의하기

<activity>launchMode 속성에 할당할 수 있는 값은 다음 5가지이다.

  • standard (기본값)
    이 액티비티가 시작된 태스크 안에 새 인스턴스를 생성하고 인텐트를 해당 인스턴스로 라우팅한다. 이 액티비티는 여러 번 인스턴스화될 수 있으며 각 인스턴스는 다른 태스크에 속할 수 있고 한 태스크는 여러 개의 인스턴스를 가질 수 있다.

  • singleTop
    standard와 거의 동일하지만 액티비티의 인스턴스가 이미 현재 태스크의 꼭대기에 있을 경우만 다르게 동작한다.
    이 경우 시스템은 액티비티의 새 인스턴스를 생성하지 않고 해당 인스턴스의 onNewIntent()를 호출하여 인텐트를 라우팅한다.

    예를 들어 한 태스크의 백 스택에 4개의 액티비티 A, B, C, D가 순서대로 있다 가정하자. 인텐트가 D로 향할 때 D의 런치모드가 standard이면 D의 새 인스턴스가 생성되어 백스택은 A-B-C-D-D로 구성될 것이다. 하지만 singleTop이면 이미 꼭대기에 있는 D의 인스턴스가 onNewIntent()를 통해 인텐트를 수신하므로 백스택은 그대로일 것이다. 그러나 인텐트가 B를 향한다면 B의 런치모드가 singleTop이더라도 새 인스턴스가 백스택에 추가된다.

    액티비티의 새 인스턴스가 생성되면 사용자는 백 버튼을 눌러 이전 액티비티로 돌아갈 수 있다. 하지만 기존 인스턴스가 새 인텐트를 처리하면 사용자는 백 버튼을 눌러도 새 인텐트가 onNewIntent()에 전달되기 전의 상태로 돌아갈 수 없다.
  • singleTask
    이 액티비티의 새 인스턴스를 새 태스크의 루트에 생성하거나 어피니티가 같은 태스크에 위치시킨다. 인스턴스가 기존 태스크에 루트로 이미 존재하면 새 인스턴스를 생성하는 대신 기존 인스턴스의 onNewIntent()를 호출하여 인텐트를 라우팅한다. 이때 이 액티비티보다 위에 있는 액티비티는 모두 소멸된다.

    액티비티가 새 태스크에서 시작되더라도 사용자가 백 버튼을 누르면 이전 액티비티로 돌아간다.
  • singleInstance
    singleTask와 거의 동일하지만 이 액티비티의 인스턴스를 가진 태스크에 다른 어떤 액티비티도 실행되지 않는다는 점만 다르다. 즉, 이 태스크에 이 액티비티의 인스턴스 하나만 존재한다. 이 모드로 시작되는 액티비티는 별도의 태스크에서 열린다.
  • singleInstancePerTask
    이 액티비티는 루트 액티비티로만 실행될 수 있다. 따라서 태스크에 해당 액티비티의 인스턴스가 하나만 존재하게 된다. singleTask와 달리 인텐트 플래그로 FLAG_ACTIVITY_MULTIPLE_TASK 또는 FLAG_ACTIVITY_NEW_DOCUMENT를 사용하면 다른 태스크들에서 여러 개의 인스턴스로 시작할 수 있다.

 

인텐트 플래그로 런치모드 정의하기

startActivity()에 전달하는 Intent 객체에 플래그를 포함하여 액티비티가 태스크에 연결되는 방식을 수정할 수 있다.

사용할 수 있는 플래그는 다음과 같다.

  • FLAG_ACTIVITY_NEW_TASK
    액티비티를 새 태스크에서 시작한다. 시작하려는 액티비티를 이미 실행 중인 태스크가 있다면 해당 태스크는 마지막 상태가 복구되어 포그라운드로 옮겨지고 액티비티는 onNewIntent()에서 새 인텐트를 수신한다. 이 플래그를 사용하면 launchModesingleTask를 할당하는 것과 동일하게 동작한다.
  • FLAG_ACTIVITY_SINGLE_TOP
    시작하려는 액티비티가 백 스택의 꼭대기에 있는 것이면 기존 인스턴스가 onNewIntent()를 호출한다. 이 플래그를 사용하면 launchModesingleTop을 할당하는 것과 동일하게 동작한다.
  • FLAG_ACTIVITY_CLEAR_TOP
    시작하려는 액티비티가 현재 태스크에 있다면 그 위에 있는 다른 모든 액티비티가 소멸되고 이 인텐트는 재개된 액티비티 인스턴스의 onNewIntent()로 전달된다. 이 동작 방식은 launchMode 속성으로는 정의할 수 없다. FLAG_ACTIVITY_NEW_TASK와 함께 사용하면 다른 태스크에 있는 기존 액티비티를 찾아 인텐트를 수신할 수 있는 위치로 옮긴다.

    액티비티의 launchMode 속성 값이 standard이면 이 액티비티도 백 스택에서 제거된 다음 새 인스턴스가 실행되어 인텐트를 처리한다. launchModestandard이면 새 인텐트에 대해 항상 새 인스턴스가 생성되기 때문이다.

 

어피니티 처리

어피니티는 액티비티가 포함되기를 선호하는 태스크를 나타내는 값이다. 기본적으로 같은 앱 안의 모든 액티비티는 서로에게 어피니티를 가지고 있다. 따라서 같은 앱 안의 모든 액티비티는 기본적으로 같은 태스크에 포함되는 것을 선호한다. 하지만 액티비티에 대한 기본 어피니티를 수정할 수 있다. 다른 앱의 액티비티는 어피니티를 공유할 수 있거나, 같은 앱의 액티비티에 다른 태스크 어피니티를 할당할 수 있다.

<activity> 요소의 taskAffinity 속성을 통해 액티비티의 어피니티를 수정할 수 있다.

taskAffinity 속성은 문자열 값을 사용한다. 이 값은 <manifest> 요소 안에 선언된 기본 패키지 이름과는 다르게 고유해야 한다. 시스템이 이 이름을 사용하여 앱의 기본 태스크 어피니티를 식별하기 때문이다.

 

어피니티는 다음 두 상황에서 작용한다.

  • 액티비티를 실행하는 인텐트에 FLAG_ACTIVITY_NEW_TASK 플래그가 포함되어 있을 때
    기본적으로 새 액티비티는 startActivity()를 호출한 액티비티와 같은 태스크에 포함되며 같은 백스택에 push된다.
    하지만 startActivity()에 전달된 인텐트가 FLAG_ACTIVITY_NEW_TASK 플래그를 포함한다면 시스템은 새 액티비티를 포함할 다른 태스크를 찾는다. 새 액티비티의 어피니티와 같은 태스크가 이미 존재한다면 그 액티비티는 그 태스크에 포함된다. 그렇지 않다면 새 태스크가 시작된다.

    이 플래그로 인해 액티비티가 새 태스크를 시작하고 사용자가 홈 버튼을 눌러 그 태스크에서 떠난다면 다시 그 태스크로 돌아가는 방법이 있어야 한다. 몇 엔티티(알림 매니저 등)는 항상 자신의 일부가 아닌 외부 태스크에서 액티비티를 시작하므로 startActivity()에 전달하는 인텐트에 항상 FLAG_ACTIVITY_NEW_TASK를 포함한다. 이 플래그를 사용할 수 있는 외부 엔티티가 호출할 수 있는 액티비티가 있다면 런처 아이콘처럼 시작되는 액티비티에 돌아가는 독자적인 방법이 있어야 한다.
  • 액티비티의 allowTaskReparenting 속성 값이 true일 때
    이 경우 액티비티는 자신이 시작하는 태스크에서 어피니티와 같은 태스크가 포그라운드로 나올 때 그 태스크로 이동할 수 있다. 예를 들어 여행 앱에 선택한 도시의 기상 상태를 예보하는 액티비티가 있다 가정하자. 이 액티비티는 같은 앱의 다른 액티비티와 동일한 어피니티(기본 앱 어피니티)를 가지며 이 속성을 통해 상위 재지정(re-parenting)이 가능하다. 액티비티 중 하나가 일기 예보 액티비티를 시작하면 이 액티비티는 처음에는 기존 액티비티와 같은 태스크에 포함된다. 하지만 여행 앱의 태스크가 포그라운드로 나오면 일기 예보 액티비티는 그 태스크에 재할당되고 그곳에 표시된다.

 

백 스택 삭제

태스크를 오랫동안 떠나 있으면 시스템이 해당 태스크의 루트 액티비티를 제외한 모든 액티비티를 제거한다. 태스크로 다시 돌아오면 오직 루트 액티비티만 복구된다. 이렇게 동작하는 이유는 오랜 시간이 지나면 이전 작업을 그만두고 새 작업을 진행하기 위해 태스크로 돌아올 가능성이 크기 때문이다.

 

다음 속성을 사용하면 이 동작을 수정할 수 있다.

  • alwaysRetainTaskState
    태스크의 루트 액티비티에서 이 속성 값이 true이면 장시간이 경과돼도 태스크는 백스택 안의 모든 액티비티를 유지한다.
  • clearTaskOnLaunch
    태스크의 루트 액티비티에서 이 속성값이 true이면 사용자가 태스크를 떠났다가 돌아올 때마다 루트 액티비티까지 모두 삭제한다. 즉, alwaysRetainTaskState와 정반대로 동작한다. 아주 잠깐 동안만 태스크를 떠났다가 돌아와도 항상 태스크의 초기 상태로 돌아오게 된다. 
  • finishOnTaskLaunch
    clearTaskOnLaunch와 비슷하지만 태스크 전체가 아닌 하나의 액티비티에서만 동작한다. 이 속성으로 인해 루트 액티비티를 포함한 어떤 액티비티라도 종료될 수 있다. 값이 true이면 액티비티는 현재 세션에 대한 태스크의 일부로 유지된다. 사용자가 태스크를 떠났다가 돌아오면 그 태스크는 더 이상 존재하지 않는다.

 

마지막 두 속성은 FLAG_ACTIVITY_RESET_TASK_IF_NEEDED가 설정되어있지 않으면 무시된다.

 

태스크 시작

android.intent.action.MAIN 액션과 android.intent.category.LAUNCHER 카테고리를 포함하는 인텐트 필터를 선언하면 액티비티를 태스크의 진입점으로 설정할 수 있다. 매니페스트 파일 안에서 다음과 같이 작성하면 된다.

<activity ... >
    <intent-filter ... >
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    ...
</activity>

 

이러한 종류의 인텐트 필터를 사용하면 액티비티의 아이콘과 라벨이 앱 런처에 표시된다.

이를 통해 사용자가 액티비티를 실행하고 그 액티비티가 생성하는 태스크로 돌아올 수 있다.

 

사용자는 태스크를 떠나고 나중에 이 액티비티 런처를 사용하여 다시 돌아올 수 있어야 한다. 이러한 이유로 singleTasksingleInstance(액티비티를 태스크의 루트로만 시작하는 런치모드)는 위 필터가 선언된 액티비티에서만 사용해야 한다.
예를 들어 인텐트가 singleTask 액티비티를 실행하여 새 태스크를 시작한다 하자. 새 태스크에서 작업을 하다가 홈 버튼을 누르면 태스크가 백그라운드로 이동하여 보이지 않게 된다. 위 필터가 없다면 태스크가 앱 런처에 표시되지 않기 때문에 사용자가 그 태스크로 돌아갈 수 있는 방법은 없다.

 

사용자가 액티비티로 돌아갈 수 없게 하려면 <activity> 요소의 finishOnTaskLaunch 속성 값을 true로 설정하면 된다.