Primary/Android

[Android Fundamentals] Activity - 1. Overview

해스끼 2024. 7. 5. 14:10

Activity는 사용자와 직접 상호작용하는 컴포넌트이다. Activity의 가장 큰(그리고 거의 유일한) 용도는 사용자에게 UI를 보여주는 것이다. 보통 activity는 전체 화면으로 보이는 경우가 많지만, 플로팅 윈도우나 멀티 윈도우 등 화면의 일부만 차지하는 경우도 있다. 

 

Activity는 자신의 lifecycle이 바뀔 때마다  ``onCreate``, ``onPause`` 등의 콜백을 실행한다. Lifecycle이 바뀔 때 실행해야 하는 작업이 있다면 콜백을 오버라이드하면 된다.

Fragment

Activity는 하나의 화면으로 이루어질 수도 있지만, 여러 개의 화면 조각(fragment)로 구성할 수도 있다. Fragment를 적절히 활용하면 UI 코드를 모듈화하고, 더 복잡한 UI를 편리하게 개발할 수 있다.

 

Fragment는 나중에 따로 알아보겠다.

Activity 생명주기

시스템은 activity stack을 통해 activity를 관리한다. 새로 실행된 activity는 보통 스택의 맨 위에 배치되고, running activity로 취급된다. 직전에 보인 activity는 새로 실행된 activity의 밑에 남아 있으며, 새로 실행된 activity가 끝나기 전까지는 foreground로 보이지 않는다.

 

Activity의 상태는 4개로 나눌 수 있다.

  • Active(or running): Activity가 스택의 최상단에 위치하여 foreground에 보이는 상태이다. 사용자와 상호작용하는 activity는 대부분 active 상태이다.
  • Visible: Activity가 focus를 잃었지만(즉 사용자와 상호작용할 수 없지만) 여전히 사용자에게 보이고 있는 상태이다. 새 activity가 화면의 일부만 차지하거나, 멀티 윈도우 모드에서 다른 actvity가 사용자와 상호작용하는 경우 직전 activity는 visible 상태가 된다. 하지만 상호작용을 하지 않을 뿐 activity는 여전히 완벽하게 살아 있다. 즉 모든 state와 멤버 변수 등이 메모리에 남아 있다.
  • Stopped(or hidden): 다른 activity에 의해 완전히 가려진 상태이다. 여전히 모든 state와 멤버 변수를 기억하고 있지만, 사용자에게 전혀 보이지 않는 상태이므로 메모리가 부족한 경우 시스템에 의해 종료되는 경우도 종종 있다.
  • Destroyed: 시스템에 의해 메모리에서 완전히 제거된 상태이다. Destroy된 activity가 사용자에게 다시 보이려면 완전히 재시작되어야 하며, 이전 state도 (어딘가에서) 복구해와야 한다.

앞서 말했듯이 생명주기가 바뀔 때마다 콜백이 실행되는데, 콜백의 구조는 다음과 같다. 이 그림을 잘 기억하자.

Activity의 주요 lifecycle path는 다음과 같다.

  • Entire lifetime: ``onCreate(Bundle)``에 의해 생성된 후 ``onDestroy()``에 의해 파괴되는 path이다. onCreate에서는 activity의 global state를 세팅하고, onDestroy에서 모든 값이 할당 해제된다. 예를 들어 백그라운드에서 데이터를 다운로드하는 스레드를 실행한다면, onCreate에서 스레드를 만든 후 onDestroy에서 스레드를 stop하면 된다.
  • Visible lifetime: Activity가 조금이라도 사용자에게 보이는 시기로, ``onStart()``부터 ``onStop()``까지이다. 이 시기 동안 사용자는 activity의 전체 또는 일부를 볼 수 있다. onStart부터 onStop 동안에는 UI를 보여주기 위한 리소스를 계속 유지해야 한다. 예를 들어 UI에 영향을 주는 ``BroadcastReceiver``를 동적으로 할당한다면, onStart에서 register한 후 onStop에서 해제해야 한다.
  • Foreground lifetime: Activity가 사용자와 상호작용할 수 있는 시기로, ``onResume()``부터 ``onPause()``까지이다. Activity는 resumed와 paused 상태를 매우 빈번하게 오갈 수 있다. 따라서 onResume과 onPause 콜백은 최대한 가벼워야 한다.

위 그림에 표시된 onCreate, onStart 등의 콜백을 통해 생명주기별로 필요한 작업을 수행할 수 있다. onCreate에서 activity를 초기화하고, onPause에서 상호작용 코드를 중지하는 등...

 

콜백을 표로 정리하면 다음과 같다.

콜백 설명 Killable? 다음 콜백
onCreate Activity가 처음 만들어질 때 호출된다. View를 만드는 등의 모든 초기화 작업은 여기서 수행해야 한다. 이전에 종료된 activity의 state가 남아 있다면 ``Bundle`` 매개변수로 참조할 수 있다.
onCreate 이후에는 항상 onStart가 실행된다.
No onStart
onRestart Activity가 stop된 이후 start되기 전에 실행된다. onRestart 이후에는 항상 onStart가 실행된다. No onStart
onStart Activity가 사용자에게 보이기 시작할 때 호출된다. Foreground에서 사용자와 상호작용할 수 있다면 onResume이 이어서 호출되고, activity가 더 이상 보이지 않게 된다면 onStop이 호출된다. No onResume
onStop
onResume Activity가 사용자와 상호작용하기 시작할 때 호출된다. 이 시점에서 activity는 스택의 맨 위에 존재하고, 사용자의 입력을 받을 수 있다. 
onResume 이후에는 항상 onPause가 실행된다.
No onPause
onPause Activity가 사용자와 더 이상 상호작용할 수 없을 때 실행된다. 그러나 여전히 사용자에게 보이고는 있으므로, UI를 보여주는 데 필요한 자원을 계속 갖고 있어야 한다.
이전 activity의 onPause()가 return해야만 다른 activity가 사용자와 상호작용할 수 있다. 따라서 onPause 콜백은 매우 가벼워야 한다.
No onResume
onStop
onStop Activity가 사용자에게 보이지 않게 될 때 호출된다. Activity가 사용자에게 보이지 않게 되는 경우는 1) 새로운 activity 또는 기존에 존재하던 activity가 스택의 맨 위로 올라오거나, 2) 이 activity를 종료하는 경우에 해당된다.
Activity가 다시 사용자에게 보이게 된다면 onRestart와 onStart가 실행되고, 완전히 종료된다면 onDestroy가 실행된다.
Yes
(onStop이 return한 이후)
onRestart
onDestroy
onDestroy Activity가 완전히 제거되기 전에 마지막으로 실행되는 콜백이다. Activity가 finish() 함수에 의해 종료되거나 시스템에 의해 강제로 종료되는 경우가 있다. finish()에 의해 종료되는 경우는 ``fun isFinishing(): Boolean``을 통해 알 수 있다. Yes -

참고로 위 표에서 killable이 Yes인 함수가 실행된 이후에는 activity를 실행하고 있는 프로세스가 언제든지 종료될 수 있다.  시스템에 의해 강제로 종료되는 경우에는 어떠한 콜백도 실행되지 않는 경우도 있다(without any line of its code being executed). 따라서 activity가 종료된 후에도 유지해야 하는(persistent) 데이터가 있다면 ``onStop``에서 저장하는 것이 좋다.

 

물론 메모리가 극도로 부족한 상황에서는 모든 프로세스가 종료 대상이 될 수 있다.

 

또, activity를 실행하고 있는 프로세스가 종료될 때에는 ``onSaveInstanceState(Bundle)`` 함수가 실행되는데, 이 함수에서 동적 state를 bundle에 저장할 수 있다. 저장한 bundle은 activity가 다시 만들어질 때(re-created) ``onCreate(Bundle)``의 매개변수로 주어지므로, 이전에 저장한 state를 bundle로부터 얻을 수 있다.

 

Android 9.0 이후에는 ``onStop`` 이후에 ``onSaveInstanceBundle``이 항상 실행된다. 따라서 ``onStop``에서는 UI 리소스를 해제하고, persistent 데이터는 ``onSaveInstanceBundle``에서 저장해도 된다.

Configuration Changes

안드로이드에서 configuration은 언어나 배율(scale) 등 사용자 정보와 화면 크기 및 방향 등 기기 정보를 모두 포함한다. 이러한 configuration이 바뀐다면 사용자에게 보이는 내용도 업데이트되어야 한다. 사용자와 직접 상호작용하는 activity 역시 configuration change에 대응하는 메커니즘을 갖고 있다.

 

Configuration change가 발생하면 activity는 기본적으로 destroy된다. 물론 ``onStop``이나 ``onDestroy`` 같은 생명주기 콜백은 전부 실행한다. 만약 이 activity가 사용자에게 보이고 있었다면, destroy된 후 즉시 새 activity가 만들어진다. 새 activity를 만들 때 ``onSaveInstanceState``에서 저장한 bundle을 참조할 수 있다.

 

Configuration change에 의해 레이아웃 파일이나 drawable 등 참조해야 하는 리소스가 달라질 수 있다. 따라서 완전히 destroy하고 다시 만드는 방법만이 안전하다. 콜백을 잘 정의했다면 configuration change 이후에도 사용자 경험이 끊어지지 않을 것이다.

 

완전히 destroy하고 다시 만드는 과정을 우회하고 싶다면, manifest의 ``android:configChanges`` 속성을 사용할 수 있다. 이 속성에 정의된 configuration change가 발생하면 activity가 완전히 destroy되는 대신 ``onConfigurationChanged(Configuration)`` 콜백이 실행된다. 

Activity 실행 & 결과 반환받기

``Context.startActivity(Intent)``를 통해 activity를 실행할 수 있다. 실행한 activity는 스택의 맨 위에 만들어지므로 사용자와  바로 상호작용할 수 있다. ``Intent`` 매개변수를 통해 activity에게 데이터를 전달할 수 있다.

 

Activity의 실행 결과가 궁금할 수도 있다. 예를 들어 연락처 선택 activity를 실행한 후, 선택한 연락처를 돌려받을 수 있다. ``Context.startActivityWithResult(Intent, int)`` 함수를 실행하면 된다. int 매개변수는 호출 id 역할을 한다. Activity의 결과는 ``onActivityResult(int, int, Intent)``로 반환된다. 첫 번째 매개변수를 통해 호출 id를 확인할 수 있다.

protected void onActivityResult(int requestCode, int resultCode, Intent data)

resultCode는 ``setResult(int)``로 지정할 수 있다. ``RESULT_OK`` 등의 일반적인 값을 사용할 수도 있고, 직접 선언한 상수 값을 반환할 수도 있다. 반환하고 싶은 데이터가 있다면 ``data``에 넣어서 반환할 수 있다.

 

Activity 실행에 실패했다면(크래시 등), ``RESULT_CANCELED`` resultCode가 반환된다.

 public class MyActivity extends Activity {
     ...

     static final int PICK_CONTACT_REQUEST = 0;

     public boolean onKeyDown(int keyCode, KeyEvent event) {
         if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
             // 연락처 activity 실행
             startActivityForResult(
                 new Intent(Intent.ACTION_PICK,
                 new Uri("content://contacts")),
                 PICK_CONTACT_REQUEST);
            return true;
         }
         return false;
     }

     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         if (requestCode == PICK_CONTACT_REQUEST) {
             if (resultCode == RESULT_OK) {
                 // data를 통해 연락처를 확인
                 startActivity(new Intent(Intent.ACTION_VIEW, data));
             }
         }
     }
 }

프로세스 생명주기

메모리가 부족할 때에는 몇몇 프로세스가 종료될 수 있다. Activity 생명주기에서 말했듯이 사용자가 상호작용하고 있지 않은 activity/프로세스부터 먼저 종료된다.

 

프로세스의 상태는 프로세스가 실행 중인 activity의 상태에 따라 다음과 같이 나눌 수 있다. 중요도 순서대로 적은 것이며, 가장 중요하지 않은 프로세스부터 먼저 종료된다.

  1. Foreground activity: Activity 스택의 맨 위에 위치하며, 사용자와 현재 상호작용하고 있는 activity이다. 당연히 제일 중요하다. Foreground activity까지 종료되어야 할 정도라면 기기 상황은 뭐...
  2. Visible activity: 사용자에게 보이긴 하지만 직접 상호작용하지는 않는 activity이다. 사용자에게 보이고 있기 때문에 매우 중요하다.
  3. Background activity: 사용자에게 보이지 않고 stop된 activity이므로 foreground이나 visible보단 비교적 부담없이 종료할 수 있다. 사용자가 종료된 activity로 돌아온다면 ``onCreate(Bundle)``가 다시 호출된다.  onCreate에서 ``onSaveInstanceState(Bundle)``에서 저장한 bundle을 통해 이전 상태를 복구할 수 있다.
  4. Empty process: activity나 service 등의 컴포넌트를 실행하고 있지 않은 프로세스이다. 사실 이런 프로세스는 수시로 청소된다. 따라서 activity 밖에서 작업을 수행하려면 적어도 Service나 BroadcastReceiver 등의 컴포넌트 context에서 수행하는 것이 좋다. (Context가 이런 의미였나!)

데이터 업로드 등의 백그라운드 작업은 activity보단 service에서 실행하는 것이 좋다. 요즘에는 WorkManager 같은 다양한 선택지가 생겼으니 더더욱 activity에서 실행할 이유가 없다.

참고자료

 

Activity  |  Android Developers

 

developer.android.com