[Android] Activity Stack

neoneoneo·2024년 3월 28일
0

android

목록 보기
5/16

우리가 매일 같이 보고 조작하는 익숙한 사용 패턴이다.
안드로이드에서 위와 같은 일련의 행위가 수행될 때, 내부적으로 Activity stack이라는 개념이 활용된다.

Activity Stack?

이전 포스팅에서 Activity의 개념과 수명 주기 단계 및 상태에 대해 살펴보았듯,

안드로이드는 여러 개의 Activity가 일련의 생성, 일시중지, 소멸 등의 주기를 따라 상태가 변경되는데, 그 과정에서 앱이 우리 눈 앞에 보여지거나 안보여지거나 또는 백그라운드에서 무언가를 실행하거나 하는식으로 구동된다.

이때 실행되는 Activity Task는 Stack에 일시적으로 저장된다.

Task의 수명 주기 및 Back Stack

Activity Stack은 일종의 대기표 관리소이다.

  • 대기표는 Activity가 실행될 때 발권되며, 무조건 발권된 순서대로 쌓인다. 중간에 바꾸고 그런 거 없다.
  • 쌓인 대기표는 가장 나중에 쌓인 것부터 처리가 된다.

Activity를 자동차, Stack을 주차장이라고 생각해보자.

사진 속 장소는 내가 가장 좋아하는 평양냉면집 우래옥의 주차장(출처:네이버지도)인데, Stack을 설명하기 아주 좋은 실사례이다.

  1. 냉면을 먹기 위해 자동차를 가지고 우래옥에 갔다고 치자.
  2. 나는 자동차(Activity)를 주차장(Back Stack)에 주차해서 1번 대기표 받는다.
  3. 그 이후 내 친구 철수가 내 차 앞에 주차를 하고 2번 대기표를 받았다.
  4. 그 이후 내 친구 영희도 철수 차 앞에 주차를 하고 3번 대기표를 받았다.

냉면을 다 먹고 우리 셋은 차를 몰고 북악스카이웨이로 드라이브를 가려고 한다.
주차요원분께 나의 1번 대기표를 주면? 아저씨는 화를 낼 것이다.

  1. 가장 앞에 있는 영희의 3번 대기표를 드렸더니 영희의 차를 빼주었다.
  2. 그리고 철수의 2번 대기표를 드렸고, 철수도 차를 뺐다.
  3. 그리고 마지막으로 나의 1번 대기표를 드렸더니 미소와 함께 차를 빼주셨다.

이것이.. First In Last Out이다..

아니 주차장 그런거 말고 진짜 Stack으로 이해해보자면,

Activity1을 Start하면 Stack에 Activity1이 쌓인다.

Activity2, 3을 순차적으로 Start하면 Stack에도 시작된 순서대로 2, 3가 쌓인다.
(자꾸만 생각나는 철수와 영희의 차..)

이때 사용자가 Back 버튼을 누르면?

가장 상단에 있는 3이 사라지고, 이제 최상단 Activity는 2가 된다.

이러한 일련의 동작은 Activity의 Fragment에 대해서도 동일하게 작동한다.

아니? 나는 Home 버튼 누를건데?

Home 버튼을 눌러서 Home으로 나갈 때에도 Stack에는 현재 실행하고 있는 Activity에 가장 상단에 쌓이게 된다.

Stack의 가장 상단에 있는 Acitivity가 Foreground activity이다.

즉, 현재 실행 중인 Activity가 Stack의 최상단에 위치하게 된다.

Activity는 여러번 Stack에 쌓일 수 있다.

Stack 안에서의 정렬 처리는 불가능하기 때문에, 단일 Activity를 여러 번 Stack에 쌓는 식으로 처리한다.

만약 동일 Activity가 여러 번 Stack에 쌓이는 것을 막고싶다면 이 동작을 수정하면 된다고 한다.
(이번 글에서는 일단 기본적인 Stack의 동작 방식에 대해서만 정리해본다.)

그래서, Activity의 동작 방식을 Stack과 함께 설명하자면

  • Activity1이 Activity2를 시작하면?
    • Stack의 최상단에 2가 쌓인다.
    • 1은 중지되지만 시스템이 스크롤 위치와 입력된 텍스트와 같은 것들을 기억하고 상태를 유지한다.
    • 사용자가 2에 있는 동안 Back 버튼을 누르면 중지되었을 때의 상태가 복원된채로 1이 재개된다(Stack의 최상단에는 1이 위치하게 된다).
  • 사용자가 Home 버튼을 눌러 앱을 나가면, 해당 Activity는 중지되면서 Background로 전환된다.
    • 이때 시스템은 해당 Activity task 상태를 유지한다.
    • 사용자가 나중에 다시 해당 Activity를 실행시키려고 하면 해당 Activity는 Foreground로 전환되고 Stack의 맨 뒤의 Activity가 재개된다.
  • 사용자가 Back 버튼을 누르면 현재 Activity가 Stack에서 pop되어 제거된다.
    • 그리고 Stack 최상단에 있는 Activity가 재개된다.
  • 동일 Activity가 여러 번 Stack에 쌓일 수 있다.

Task 관리

위에서 안드로이드가 Activity를 FILO Stack에 배치하여 Task와 Back Stack을 관리한다는 것에 대해 살펴보았다.

<activity> 매니페스트 요소의 속성과startActivity()에 전달하는 인텐트의 플래그를 사용하여 여러 작업을 수행 및 관리할 수 있다.

  • Activity의 새 인스턴스가 현재 Task과 연결되는 방식을 정의할 수 있다.

Task 관리에 사용되는 activity 속성들

  • launchMode
  • taskAffinity
  • allowTaskReparenting
  • clearTaskOnLaunch
  • alwaysRetainTaskState
  • finishOnTaskLaunch

주요 인텐트 플래그들

  • FLAG_ACTIVITY_NEW_TASK
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_SINGLE_TOP

먼저 launchMode에 대해서 살혀보자. 어떻게 Task를 관리할 수 있을까?

방법 1. 매니페스트 파일 사용

Activity 선언 시, 시작될 때 Task과 어떻게 연결되는지 지정한다.

이 방법은 launchMode 속성을 사용하며, 5개의 할당 가능한 모드가 있다.

standard

  • 기본 모드
  • 시작된 Task에 새 인스턴스를 만든다.
  • Activity는 여러 번 인스턴스화 될 수 있다.
  • 각 인스턴스는 서로 다른 Task에 속하며 한 Task에는 여러 인스턴트가 있을 수 있다.

singleTop

  • 인스턴스가 이미 현재 Task의 맨 위에 있다면, 시스템은 Activity의 새 인스턴스를 생성하는 대신, 새로운 인텐트를 보내어 Activity가 처리되도록 한다.
    • onNewIntent() 메서드를 호출하여 새로운 인텐트를 해당 Activity에 전달한다.

| D |
| C |
| B |
| A | - root activity

만약 Stack이 이렇게 쌓여있다고 가정해보자.

이때 Activity D에 대한 인텐트가 도착한다.

만약, D가 standard 모드라면?

| D | <- NEW!
| D |
| C |
| B |
| A | - root activity

D라는 새로운 인스턴트가 생성되고 최상단에 쌓인다.

만약, D가 singleTop모드라면?

| D |
| C |
| B |
| A | - root activity

Stack의 구조가 onNewIntent()를 통해 유지된다.

근데..! 만약 Activity B에 대한 인텐츠가 도착했다면?

| B | <- NEW!
| D |
| C |
| B |
| A | - root activity

B의 모드가 singleTop이여로 새로운 B 인스턴스가 Stack에 추가된다.

singleTask

  • 시스템은 새로운 Task의 root에서 Activity를 만든다.
  • 또는 어피니티가 동일한 기존 Task에서 Activity를 찾는다.
    • 만약 해당 Activity의 인스턴스가 이미 존재하는 경우라면? 새로운 인스턴스를 만들지 않고, onNewIntent() 메서드를 호출하여 기존 인스턴스로 전달한다.
  • 위의 다른 Activity들은 소멸된다.

어피니티란 매니페스트 파일에서 Activity에 지정된 속성 중 하나이다. android:taskAffinity 속성으로 지정한다.

| D |
| C |
| B |
| A | - root activity

여기에서 D의 모드가 singleTask 인 인텐트가 들어오면 Stack은 이렇게 바뀐다.

| D | - root activity

singleInstance

  • 동작은 singleTask와 동일하다.
  • 단, 시스템이 인스턴스를 보유한 Task로 다른 Activity를 실행하지 않는다.
    • Activity는 항상 Task의 유일한 단일 멤버이다.
  • 이 모드로 시작되는 Activity는 별도의 Task에서 열린다.

| D |
| C |
| B |
| A | - root activity

여기에서 D의 모드가 singleInstance 인 인텐트가 들어오면 Stack은 이렇게 바뀌는데,

| D | - root activity

singleTask와 다르게 아예 새로운 독립적인 공간에서 D를 실행한다.

singleInstancePerTask

  • Activity가 첫 번째(root) Activity로만 실행된다.
  • 하나의 Task에는 Activity의 인스턴스가 하나만 있을 수 있다.
  • singleTask 모드와 달리 FLAG_ACTIVITY_MULTIPLE_TASK 또는 FLAG_ACTIVITY_NEW_DOCUMENT 플래스가 설정되어 있으면 서로 다른 Task의 여러 인스턴스에서 시작될 수 있다.

자.. 이게 무슨 말인가 하면..

App : 지도
Activity1 : 지도 표시 기본 뷰
Activity2 : 도시 정보 뷰

이렇게 구성된 App에서, singleInstancePerTask 모드로 Activity1을 설정한다고 가정해보자.

  1. 사용자가 지도앱을 실행하면 Activity1이 열린다.
    (새로운 Task의 root Activity)
  2. 사용자가 지도에서 도시를 선택하면 Activity2가 시작된다.
    (1과 2는 동일한 Stack에 있다)
  3. 사용자가 다른 도시를 선택하여 2가 다시 시작되면, 이전에 있었던 2의 인스턴스는 종료되고 새로운 2의 인스턴스가 시작된다.

이를 스택으로 표현하면,

| 2-2 |
| 2-1 |
|  1  | - root activity

이 경우 1은 하나의 Stack 안에서 한 번만 실행될 수 있다.
2는 여러 인스턴스가 Stack 내에 있을 수 있다.

공식 문서에서 제공하는 예시를 살펴보며 방법1을 마무리해보자..!

첫 번째 Stack을 보면 1, 2이 들어가있다.

이때, 모드가 singleTask인 Y가 Stack에 추가된다면?

Y만 가져오는 것이 아니라, background task에 있는 Y, X를 떠서 가져온다. 즉, 전체 Stack을 가져온다.

그 이후 Back 버튼을 누르면 가장 상단에 있는 Activity부터 pop되어 종료된다.

방법 2. 인텐트 플래그 사용

startActivity()`를 호출할 때 새 Activity가 현재 Task와 연결되는 방식을 선언하는 FLAG를 인텐츠에 포함시킨다.

FLAG_ACTIVITY_NEW_TASK

  • singleTask 모드와 동일한 동작을 수행한다.
  • Activity를 새로운 Task에서 시작한다.
  • 해당 Activity가 이미 실행 중이라면?
    • Task가 마지막 상태가 복원된 상태로 포그라운드로 이동하고 Activity는 onNewIntent()로 새로운 인텐트를 수신한다.
  • 가장 자주 사용된다.

FLAG_ACTIVITY_SINGLE_TOP

  • singleTop 모드와 동일한 동작을 수행한다.
  • 해당 Activity가 이미 실행 중이라면?
    • 기존 인스턴스는 Activity의 새 인스턴스를 생성하는 대신 onNewIntent() 호출을 수신한다.

FLAG_ACTIVITY_CLEAR_TOP

  • 동일하게 동작하는 launchMode는 없다.
  • 해당 Activity가 이미 실행 중이라면?
    • 해당 Activity의 새로운 인스턴스를 실행한다.
    • 그 위에 있는 다른 모든 활동을 소멸시킨다.
    • 인텐트는 onNewIntent()를 통해 Activity의 재개된 인스턴스로 전달된다.
  • 가장 자주 사용된다.

자.. 이게 무슨 말인고..하면,

아까 위에서 사례로 서술한 지도 앱을 다시 떠올려보자.

Task 1 :
| 2-2 |
| 2-1 |
|  1  | - root activity

Task에 Stack이 이렇게 쌓여있다.

이때 사용자가 지도 앱을 다시 시작하면 1이 시작될 것이다.

(새로운 Task2가 생성되고 1이 root가 된다)
이미 지도 앱이 실행 중이며 새로운 Task에 1 인스턴스를 열게 된다.

  • 이때 FLAG_ACTIVITY_CLEAR_TOP 플래스가 사용된다.
Task 2 :
|  1  | - root activity

즉, Task2에는 새로운 1 인스턴스를 생성하지 않고 이전에 Task1에서 실행 중이던 1 인스턴스를 Task2로 가져온다.(Task1에도 여전히 존재)

어피니티 처리

어피니티 : Activity가 소속되기를 '선호'하는 Task

위에서 한 번 언급했는데, <activity> 요소의 taskAffinity 속성을 사용하여 어피니티를 수정할 수 있다.

어피니티는 다음 두 가지 상황에서 사용될 수 있다.

1. Activity를 실행하는 인텐트에 FLAG_ACTIVITY_NEW_TASK가 포함되어 있는 경우

기본적으로 새로운 Activity는 startActivity()를 호출한 Activity의 Task로 시작된다(동일한 Stack으로 push).

그러나,

startActivity()에 전달된 인텐트에FLAG_ACTIVITY_NEW_TASK가 포함되어 있으면?

=> 새로운 Activity를 수용할 다른 Task를 찾는다.

이때 새 Activity와 동일한 어피니티를 가진 기존 Task가 있다면?

=> Activity는 해당 Task로 시작된다

  • 아니면 새로운 Task로 시작되고.

다만, 이 플래그로 Activity가 새로운 Task로 시작되었을 때 다시 예전 Task로 돌아가야할 필요가 있다면 어떻게 할까?

=> startActivity()에 전달하는 인텐트에 FLAG_ACTIVITY_NEW_TASK를 정의한다.

자.. 이것을 메신저 앱을 예시로 이해해보자..

  1. 사용자가 지도 앱을 보고 있는데, 메신저 앱에서 새로운 메시지를 받았다.
  2. 메신저 앱은 새로운 메시지를 푸시 알림으로 표시한다.
  3. 사용자가 알림을 탭하여 메신저 앱으로 이동하려고 시도한다.
  4. 알림 관리자가 새로운 메시지에 대한 알림을 처리하기 위해 FLAG_ACTIVITY_NEW_TASK 플래그를 사용하여 메신저 앱의 Activity를 시작한다.
  5. FLAG_ACTIVITY_NEW_TASK로 인해 새로운 Task에 메신저 앱의 새로운 Activity가 시작된다.
  6. 사용자가 메신저 앱으로 이동하여 새로운 메시지를 확인한다.
  7. 그 후 홈 버튼을 눌러 홈 화면을 이동한다.
  8. 나중에 사용자가 다시 메신저 앱을 시작하려면?
    • 이전에 시작된 Task로 돌아가야한다.
    • FLAG_ACTIVITY_NEW_TASK로 시작된 Task로 다시 돌아갈 수 있는 독립적인 방법이 필요하다!

2. Activity의 allowTaskReparenting 속성이 true로 설정된 경우

  • Activity는 시작된 Task에서 해당 작업이 포그라운드로 나올 때 어피니티가 있는 Task로 이동할 수 있다.

이건 무슨 말이냐면,

Task 1:
| B |
| A | - root activity
Task 2:
| C | - root activity (현재 화면에 실행 중)

이렇게 Stack이 있다고 치자,

이때 사용자가 B를 시작하면 이렇게 된다.

Task 1:
| A | - root activity
Task 2:
| B |
| C | - root activity (현재 화면에 실행 중)

B는 Task2로 이동되었다. 이는 allowTaskReparenting 속성이 true라서 가능한 것이다!

Back Stack 삭제하기

사용자가 장시간 Task를 떠나면 시스템은 root를 제외한 모든 Activity를 Task에서 삭제한다.

이러한 동작도 속성으로 수정할 수 있다.

alwaysRetainTaskState을 true로 설정

  • Task의 root activity에서 이 속성을 "true"로 설정하면 위에서 언급한 Back Stack 삭제가 발생하지 않는다.
  • 장시간이 지나도 Stack을 그대로 유지한다.

clearTaskOnLaunch을 true로 설정

  • alwaysRetainTaskState와 반대이다.
  • 사용자가 Task를 떠났다가 다시 돌아올 때마다 root activity외의 Task들이 삭제된다.
  • FLAG_ACTIVITY_RESET_TASK_IF_NEEDED를 설정해야 최종적으로 적용된다.

finishOnTaskLaunch을 true로 설정

  • clearTaskOnLaunch와 비슷하지만 Task 전체가 아닌 단일 Activity에서 작동한다.
  • activity는 현재 세션에서만 Task의 일부로 유지된다.
  • 사용자가 Task를 떠났다가 다시 돌아오면 이 Task는 더 이상 존재하지 않게 된다.

내가 만든 앱의 Stack을 확인해보자

터미널에 adb shell "dumpsys activity activities"을 입력하면 된다고 해서 해봤는데,

뭔가.. 무시무시한게 나왔다.. 스크롤도 꽤 길다.. 이거.. 뭐가 나왔다고 거기서.. 겁나 험한게..

내가 위에서 공부하면서 상상한 Stack은 이런 모습이 아녔는데..

다행히도 "dumpsys activity activities | grep -i Hist" 이렇게 입력하면 Stack에 대한 정보만 필터링 할 수 있다고 한다!

이런 Sign in 화면이 있고 별도로 Sign up 화면이 있을때,
Sign in -> Sign up -(back)-> Sign in 화면으로 이동을 하면 스택은 어떻게 쌓이고 처리될까?

Hist 부분이 쌓인 스택이라고 보면 된다.

  • Sign in -> Sign up -(back)-> Sign in
    • 윗 부분이 Sign up에서 찍은 내용이다. 가장 위에 SignUpActivity가 있고, 그 아래에 SignInActivity가 있다.
  • Sign in -> Sign up -(back)-> Sign in
    • 아랫 부분은 Sign in에서 찍은 내용이다. 가장 위에 SignInActivity만 남아있다.

최하단의 HIST #0은 기본 반찬격인 launcher 항목인 것 같다.


reference

공식 문서


[TIL-240328]

0개의 댓글