[Android Fundamentals] Activity - 8. 프로세스와 앱 생명주기
모든 안드로이드 앱은 개별 리눅스 프로세스에서 실행된다. 앱이 실행되는 프로세스는 시스템에 의해 회수되기 전까지 계속 실행된다.
즉 프로세스의 생명주기를 앱이 아닌 시스템이 제어한다는 것. 프로세스 생명주기는 앱의 실행 여부, 사용자에게의 중요도, 메모리 상황 등 시스템의 많은 부분을 종합적으로 고려하여 결정된다.
따라서 우리 개발자들은 ``Activity``, ``Service``, ``BroadcastReceiver`` 등의 컴포넌트가 프로세스 생명주기에 어떻게 영향을 미치는 지 알아야 한다. 컴포넌트를 정확히 이해하지 못하면 중요한 로직이 실행되지 않을 수도 있고, 로직이 실행되는 도중에 프로세스가 종료될 수도 있다.
예를 들어 ``BroadcastReceiver``에서 ``Intent``를 받은 후 스레드를 생성했다고 해 보자. ``BroadcastReceiver``는 ``onReceive()``가 종료된 후에는 실행이 종료된 것으로 간주되므로 프로세스가 언제든지 종료될 수 있다. 리시버 프로세스가 종료되면 스레드까지 함께 종료되므로 스레드의 작업이 실행되지 않을 수 있다.
이 경우에는 스레드를 만드는 대신 ``JobService``나 ``WorkManager``를 활용해야 한다.
메모리가 부족할 때에는 우선순위가 낮은 프로세스부터 종료된다. 우선순위가 높은 순서대로 살펴보면 다음과 같다.
포그라운드 프로세스
포그라운드 프로세스는 사용자의 현재 작업에 필요한 프로세스를 의미한다. 컴포넌트마다 포그라운드로 간주되는 조건이 모두 다르다.
- ``Activity``: 사용자와 상호작용하고 있는 activity (resumed 상태의 activity)
- ``BroadcastReceiver``: ``onReceive()`` 함수가 실행 중인 리시버
- ``Service``: 콜백을 실행 중인 서비스 (``onCreate()``, ``onStart()``, ``onDestroy()``)
전체 프로세스 중 포그라운드로 간주되는 프로세스는 매우 적고, 포그라운드조차 실행되지 못할 정도로 메모리가 부족한 상황이 아니라면 종료되지 않는다.
솔직히 그 정도로 메모리가 부족하다면 기기나 내 코드 중 하나가 잘못됐을 가능성이 높다.
사용자에게 보이는(visible) 프로세스
사용자가 인식할 수 있는 작업을 수행하는 프로세스이다.
- ``Activity``: 사용자에게 보이지만, 직접 상호작용하지는 않는 activity이다. 즉 paused 상태에 있는 activity라고 볼 수 있다. 다른 activity에 의해 부분적으로 가려진 activity 등이 해당된다.
- ``Service``: ``Service.startForeground()``로 실행한 포그라운드 서비스
- 사용자가 인식할 수 있는 서비스를 실행 중인 프로세스도 해당된다. 라이브 배경화면 등.
Visible 프로세스는 포그라운드 프로세스보단 많지만 전체 프로세스 중에서는 몇개 안 된다. 따라서 포그라운드 프로세스를 위해 메모리를 확보하는 경우가 아니라면 종료되지 않는다.
서비스 프로세스
서비스 프로세스는 ``startService()``로 실행된 ``Service``를 실행하는 프로세스이다. 사용자에게 직접 보이는 건 아니지만, 어쨌든 사용자에게 필요한 작업을 수행하는 프로세스이므로 나름 중요하게 다뤄진다.
단, 너무 오래 실행되는 ``Service``는 ``캐시된 프로세스``로 한 단계 강등될 수 있다. 30분 이상 실행된다던가...
오랫동안 실행되어야 하는 프로세스는 ``CoroutineWorker.setForeground()``로 실행할 수 있다. 프로세스가 정확한 시각에 실행되어야 한다면 ``AlarmManager``를 사용할 수 있다.
캐시된 프로세스
캐시된 프로세스는 당장 필요하지 않고, 따라서 시스템이 자유롭게 종료할 수 있는 프로세스를 말한다. 일반적인 상황에서는 캐시된 프로세스만이 종료된다.
시스템이 정상 상태라면 ``캐시된 프로세스``가 항상 여러 개 존재하고, 작업 상황을 고려하여 몇몇 캐시된 프로세스가 종료된다. 시스템이 극한 상황까지 가지 않는 이상 캐시된 프로세스가 모두 종료되지는 않는다.
이 상태에 있는 프로세스는 모든 작업을 중단하는 것이 좋다. 언제든지 종료될 수 있기 때문. 뭔가 작업을 하고 싶다면 위에서 설명한 방법들을 활용하자.
Stopped 상태의 ``Activity``도 캐시된 프로세스로 간주된다. 생명주기를 잘 지키는 앱이라면, stopped 상태의 activity가 종료되더라도 UX에 부정적인 영향이 없어야 한다. Activity가 종료될 때의 상태는 ``onCreate``에서 ``savedInstanceState``를 통해 복구할 수 있다.
단, 시스템이 activity를 종료할 때에는 ``onDestroy()``가 실행되지 않을 수도 있음에 주의.
캐시된 프로세스는 리스트로 관리되고, 리스트의 순서는 플랫폼에 따라 다르게 구현된다. 제조사마다, 안드 버전마다 다를 수 있다.
일반적으로는 홈 UI나 직전에 본 activity 등 사용자에게 중요한 의미를 갖는 프로세스의 우선순위가 높게 매겨진다. 물론 다른 알고리즘이 적용될 수도 있다. 캐시 상태가 너무 오래 지속되면 아예 종료한다던가.
정리
프로세스의 중요도는 프로세스에서 실행되는 컴포넌트 중 중요도가 가장 높은 컴포넌트의 중요도와 같다. 또, 프로세스 간 의존성에도 영향을 받는다.
예를 들어 프로세스 A가 ``Context.BIND_AUTO_CREATE`` 플래그를 설정한 ``Service``를 실행하거나 프로세스 B의 ``ContentProvider``를 참조하고 있다면, 프로세스 B의 중요도는 A보다 낮지 않다.
결론: 생명주기가 중요하다! Jetpack이나 컴포넌트를 사용할 때 생명주기를 잘 관리하자.
참고자료