앱을 처음 실행했을 때 마주하는 화면부터 Activity를 이해하고, 안드로이드의 물리 키에 따라 액티비티들이 어떤 순서로 작동하는지에 대해 알고자 정리하는 글 (feat.백 버튼, 홈 키, 최근 앱 키)
0. When launching App..
안드로이드 앱을 실행했을 때, 우리의 눈에는 먼저 하나의 화면이 보여질 것이다.
이것을 우리는 Activity가 화면에 활성화되고 있는 것으로 부르기로 했다(..).
우리가 웹에서 윈도우창을 여러개 띄우듯 많은 Activity들을 띄우고 이동할 수 있지만 모바일 애플리케이션의 특성상 한 화면에는 한 개의 액티비티만 보이며 화면 단위로 움직이며 사용자에게 보여진다. (Android 7.0 부터는 화면 분할을 할 수 있는 멀티 윈도우 환경을 지원한다.)
처음 실행했을 때 첫 번째로 보이는 것은 첫 번째로 실행되는 액티비티일 것이고 사용자의 행동에 의해 다른 액티비티로 이동하면, 이동한 액티비티의 화면이 사용자에게 보여지게 된다. 그렇다면 이전에 실행된 액티비티들은 어떻게 되는 것일까?
1. 그 많던 Activity들은 어디로 갔을까
A task is a stack of activities
이렇게 실행된 액티비티들은 Task라는 곳에 존재하게 된다.
설령 서로 다른 앱에 속한 액티비티들이라 하더라도 이 액티비티들은 같은 태스크에 있을 수 있다. 예를 들어 어떤 애플리케이션에서 카카오톡 공유를 위해 카카오톡 애플리케이션을 실행했다면, 다른 애플리케이션으로 화면이 전환되었지만 이 액티비티들은 같은 태스크에 있음으로써 사용자에게 자연스러운 사용자 경험을 제공할 수 있는 것이다.
이처럼 task는 사용자에 의해 실행된 activity들의 모음이다. 이 액티비티들이 생성되면서 인스턴스들은 back stack이라는 곳에 저장된다. 백 스택은 이처럼 액티비티들의 인스턴스를 가지고 있기에, 같은 액티비티의 여러개의 인스턴스를 저장할 수 있고 같은 액티비티를 여러개 시작할 수 있다는 의미를 갖는다.
2. How does back stack work?
살짝 자바스크립트를 공부했을 때를 떠올려, 안드로이드의 백 스택은 마치 call stack과 같은 원리를 가진다.
콜 스택은 어떤 함수들이 현재 실행되고 있고, 어떤 함수들이 그 함수 안에서 호출됐는지 keeping track 하기 위해 만들어진 메커니즘이다. 함수를 호출 했을 때, 콜 스택에 추가가 되고, 함수가 실행되고 호출 된 함수로부터 호출된 어떤 함수 또한 그 위로 차곡히 쌓이게 되는데 이 작동원리와 비슷하다.
앱 실행 후, 순서대로 실행된 액티비티 인스턴스들은 백 스택에 아래의 그림처럼 저장된다.
처음 실행된 Activity1은 foreground에 나올 것이고, 차례 대로 Activity2, Activity3을 실행한다면 차례대로 백 스택에 push된다. 맨 위에 쌓여있는 액티비티 인스턴스를 top이라고 표현하고, 현재의 top은 마지막에 실행한 Activity3이 되는 것이다. Activity3은 현재 포커스를 갖고있고 사용자와 상호작용하고 있는 액티비임을 알 수 있다.
그렇다면 쌓여있는 액티비티들은 무얼하고 있을까?
이전 Activity1, Activity2은 백 스택에 남아있지만 중지된 상태를 갖는다. 이 때, 사용자가 기기의 back button을 누르면 현재 보여지고 있는 Activity3은 맨 위에서 pop되어 제거되고 Activity2의 UI가 복원된다. 백 버튼을 누르는 것은 마치 액티비티를 종료하는 finish() 함수와 같은 역할을 하고, 사용자가 원래 있던 곳을 다시 보여준다. 사용자가 뒤로가기 버튼을 누르면 현재 활동이 제거되고 저장되어 있던 이전 활동이 다시 시작되는 것이다.
액티비티 안에 띄워지는 fragment들 또한 마찬가지이다. 액티비티 위에 조각처럼 띄어지는 프래그먼트 또한 더 이상 존재하지 않을 때까지 계속 pop 되어지다 프래그먼트가 속한 액티비티 또한 pop되어 백 스택에서 제거된다.
즉, back stack은 이 처럼 LIFO(Last In First Out) 후입선출 방식의 객체구조로 작동한다.
3. Push/Pop 될 때의 LifeCycle
백 스택에서 액티비티들이 푸쉬/팝이 일어나는 순간에 액티비티 수명 주기 상태도 변화한다.
위의 예제에서 Activity2에서 Activity3를 실행시켰을 때, Activity3의 인스턴스가 백 스택에 푸쉬되고, Activity2의 수명 주기 상태는 STOPPED이 된다. 대신 UI에 대한 상태는 보존된다.
백 버튼을 눌러서 현재 사용자가 마주하고 있는 Activity3는 팝되어 백스택에서 제거될 때는, DESTROYED 상태가 된 후 메모리에서 제거되고, 제거 후 탑의 위치가 된 Activity2는 RESUMED 상태가 되어 다시 실행된다. 이 때, 저장되었던 Activity2의 UI상태가 복구된다.
4. 홈 키 또는 최근 앱 키를 눌렀을 때
Now, if instead of hitting the Back button, you were to hit the Home button, the whole task is put in the background.
백 버튼을 눌렀을 때는 사용자에게 보여지고 있는 액티비티가 백 스택에서 pop되어 제거된 다는 것은 알았다. 그렇다면, 홈 키 또는 최근 앱 키를 눌렀을 때는 어떻게 될까?
예를 들어 A1, A2, A3, A4라는 4개의 액티비티를 가진 태스크1이 있다고 하자. 사용자가 A4와 상호작용하는 과정에서 홈 키를 누르면 A4이 존재하는 태스크 자체가 background로 들어간다. 그리고 애플리케이션 launcher로 이동하고 새로운 앱을 실행하면 새로운 태스크2의 첫 번째 액티비티인 root activity가 표시된다. 이 태스크는 B1, B2 2개의 액티비티를 가지고 있다고 하면,
A1 - A2 - A3 - A4 - (홈키) - HOME - (새로운 앱 실행) - B1 - B2
이런 과정이 될 것이다. 사용자가 홈에서 이전 앱을 다시 실행시키면 background로 들어갔던 태스크1이 다시 foreground로 돌아오고 백 스택의 top이었던 A4가 사용자에게 보이게 된다.
(...) - B1 - B2 - (홈키) - A4
이 때, 백 버튼을 누르면 A4는 pop되고 전 액티비티가 표시된다.
(...) - B1 - B2 - (홈키) - A4 - (백 버튼) - A3
최근 앱 키를 눌렀을 때는, 현재 백그라운드로 들어가 있는 태스크들의 목록을 볼 수 있다. 목록의 테스크 중 상호작용하고 싶은 테스크를 선택하면 해당 포그라운드로 전환된다.
정리하자면, 백 버튼을 눌렀을 때는 백 스택 내 쌓여있는 액티비티의 인스턴스 중 top부터 pop되어 제거되며 바로 이전 활동이 나타난다. 홈 버튼을 눌러 홈 화면으로 이동 할 때는 task 응집 단위로 백그라운드로 이동하게 된다. 최근 앱 키를 눌렀을 때는 이렇게 백그라운드에 저장된 앱들의 목록을 볼 수 있으며 포그라운드로 상호작용하고 싶은 앱을 눌러 다시 불러올 수 있다.
이렇게 태스크의 스택 내에 존재하는 액티비티들은 모두 묶여서 백그라운드와 포그라운드를 태스크 단위로 자유롭게 이동할 수 있다. 이 때문에 안드로이드에서 멀티 태스킹이 가능한 것이다.
여러 앱을 실행시켜 여러 작업을 동시에 백그라운드에 대기할 수 있다. 하지만 이렇게 여러 앱을 실행시켜놓으면 많은 태스크들때문에 메모리 저장 공간이 점차 부족해질 것이고, 안드로이드 시스템은 메모리 부족 상태가 되면 메모리를 복구하기 위해 자동으로 태스크를 제거한다. 아무 태스크를 제거하는 것은 아니고 백그라운드에 있는 태스크의 인스턴스를 가장 오랫동안 포그라운드로 돌아오지 않은 태스크 먼저 제거하기 시작한다.
5. Recycling Activity
if you're calling start activity with the activity you're already on.
모바일 디바이스 특성상 제한된 메모리를 효율적으로 사용하는 것이 관건일 때가 있다. 한 스크롤이 끝나는 기점으로 뷰를 재사용하는 recyclerView와 같은 안드로이드 컨텍스트처럼 액티비티또한 재사용할 수 있다.
앞에서 백 스택은 액티비티들의 인스턴스를 가지고 있는 자료구조라고 했다. 백 스택 내 push, pop 작업을 하면서 같은 액티비티를 실행한다면 중복된 인스턴스가 계속 생성될 것이다. 한 액티비티가 여러개의 인스턴스를 저장하는 이는 메모리 소모로 이어질 수 있을 것이다.
여기서 새 인스턴스가 생성되어 push되는 것 대신에 기존 인스턴스를 재사용할 수 있다. 또는 root 액티비티를 제외한 모든 활동을 백 스택에서 제거되도록 할 수도 있다. 액티비티의 흐름을 조작하며 새 인스턴스가 현재 작업과 연결되는 방식을 정의할 수 있는 두 가지 방법을 재사용과 제거를 초점으로 보자.
1. manifest 파일 사용
android:launchMode = ["standard" | "singleTop" | "singleTask" | "singleInstance"]
- singleTop
실행한 액티비티의 인스턴스가 이미 현재 태스크의 top이라면 새 인스턴스를 생성하지 않고, 기존의 top을 재사용한다. onNewIntent() 메서드를 호출하여 인텐트를 기존 인스턴스로 라우팅한다. 예를 들어 A1-A2 백 스택에서 기본 standard 실행 모드라면, A2의 인텐트가 도착한다면 새 인스턴스가 실행되어 A1-A2-A2로 push 될 것이다. 하지만 실행모드가 singleTop이라면 A4의 기존 인스턴스는 onNewIntent()를 통해 도착한 인텐트를 받고, A2의 기존 인스턴스가 top이기 때문에 스택은 A1-A2로 유지가 된다. 만약 A1의 인텐트가 도착하면 A1-A2-A1로 새 인스턴스가 스택에 추가된다.
- A2를 singleTop으로 설정, A1-A2 상태에서 A2를 호출 시 A1-재사용된A2
-A2를 singleTop으로 설정, A2-A1 상태에서 A2를 호출 시 A2-A1-A2 - singleTask
singleTask로 지정된 액티비티는 해당 태스크에서 하나만 존재한다. singleTop과 마찬가지로 재사용의 개념이 들어가는데, 태스크 안에서 해당 액티비티가 존재하는 경우 인스턴스를 새로 생성하지 않고 onNewIntent를 호출하여 재사용한다. 하지만 여기서 affinity 라는 개념이 들어가는데, A1-A2-A3-A4-A3 를 차례로 실행하는 태스크에서 A3이 singleTask이고 모두 동일한 어피니티일경우, A1-A2-A3-A4 까지 활성화되다가, A4에서 A3을 실행시키면 A4는 pop되어지고 A3을 재사용한다.
만약 A3가 signleTask이고 A1-A2가 같은 어피니티이고, A3-A4가 같은 어피니티인것처럼 어피니티가 다른 경우에 위에처럼 실행시키면, 처음 A1-A2-A3-A4를 차례로 실행할 때 A3을 실행하는 시점에서 새로운 태스크를 생성하고 해당 A3 액티비티는 새 태스크의 root가 된다.
- 같은 affinity인 경우 A3을 singleTask로 설정,
A1 - A2-A3-A4-(뒤로가기)-재사용된A3
- 다른 affinity인 경우 A3을 singleTask로 설정,
A2-A2 / A3-A4-(뒤로가기)-재사용된A3 - android:clearTaskOnLaunch
홈 화면에서 다시 앱을 시작할 때 root 액티비티를 제외한 모든 액티비티를 작업에서 제거할지 여부를 나타낸다. 이 값이 true인 경우 사용자가 마지막으로 수행한 작업과 상관없이, 그리고 백 버튼 또는 홈 버튼을 눌러 작업 중인 화면을 떠났는지 여부와 상관없이 root 액티비티로 이동한다.
예를 들어 홈 화면에서 액티비티인 A1를 시작하고 A2로 이동하고, 다음에 사용자가 홈 버튼을 누른다음 다시 앱을 작동했을 때 가장 마지막으로 수행한 A2를 마주하게 될 것이다. 하지만 A1의 이 플래그를 true로 설정하는 경우 사용자가 홈 화면을 누르고 작업을 백그라운드로 이동하면 그 위의 모든 액티비티(이 경우는 A2)가 제거된다. 따라서 사용자가 작업으로 돌아가면 A1만 보이게 되는 것이다.
2. intent flag 사용
소스코드에서 플래그를 사용하고 싶을 때는 Intent에 addFlags() 메소드를 사용한다.
launchMode의 singleTask와 FLAG_ACTIVITY_NEW_TASK가 동일한 역할을 하고, singleTop과 FLAG_ACTIVITY_SINGLE_TOP이 같은 역할을 한다.
- FLAG_ACTIVITY_CLEAR_TOP
호출하는 액티비티가 해당 스택에 존재하면, 해당 액티비티를 top으로 올리면서, 그 위에 존재하던 액티비티들은 모두 삭제한다.
A2를 clear top으로 설정, A1-A2-A3-A4 상태에서 A2 호출시 모두 pop되고 A1-A2
단, 해당 플래그는 액티비티를 모두 onDestroy() 시킨 후 새롭게 onCreate() 시키기 때문에 A1를 유지하려면 FLAG_ACTIVITY_SINGLE_TOP 플래그와 함께 사용하면 된다.
참고 자료
'🐸 Android' 카테고리의 다른 글
[Android] Fastlane으로 앱스토어 자동 배포하기 (0) | 2023.07.04 |
---|---|
[Android] 앱 삭제 후 데이터가 남아있는 문제 (0) | 2023.07.04 |
멀티 셀렉이 가능한 커스텀 갤러리 만들기 (2) | 2023.07.04 |
[Android] 웹과 앱의 통신 모델 만들기 (0) | 2023.04.21 |
블로그 이전 (1) | 2023.02.12 |