원문 : https://proandroiddev.com/deep-dive-into-android-services-4830b8c9a09


안드로이드 Service로 빠져 봅시다

소개

안드로이드에서 종종 백그라운드 실행이 필요한 작업을 수행해야합니다. 이럴경우 메인쓰레드가 아닌 서브쓰레드를 생성하여 사용하게 됩니다.

하지만 예기치 않은 결과가 발생할 수 있습니다.

예를들어 서브쓰레드를 수행한 뒤 액티비티 화면 회전 등으로 인해 onDestroy()가 호출되고,  서브 쓰레드의 작업이 끝났을 때 해당 Activity는 존재하지 않는 상태라면 UI갱신을 할 수 없습니다.

이럴 때 필요한 안드로이드의 컴포넌트는 바로 Service 입니다.

서비스는 안드로이드 애플리케이션의 구성요소중 하나로 메인쓰레드에서 실행되는 UI가 없습니다.  서비스를 사용하기 위해서는 AndroidManifest.xml에 해당 서비스를 선언해야합니다.  서비스에서 백그라운드쓰레드는 개발자가 직접관리해야합니다.  백그라운드와 포어그라운드라는 용어가 많이 나오므로 다음의 두가지로 나누어 보겠습니다.

  • 안드로이드 컴포넌트 생명주기
  • 쓰레드

이 포스팅에서는 컴포넌트 생명주기에 대해서 이야기 할때 백그라운드포어그라운드라는 용어를 사용하고, 쓰레드를 참조할 때는 백그라운드 쓰레드포어그라운드 쓰레드라는 용어를 사용하겠습니다.

서비스 자신의 백그라운드 쓰레드를 다루는 서브클래스가 있는데 바로 IntentService라는 녀석입니다. 이 클래스에 대해서는 다루지 않겠습니다.

쓰레드, 서비스, 안드로이드 컴포넌트의 생명주기

한 걸음 뒤로 물러서서 서비스가 어떤 의미있는 일들을 하는지 더 큰 그림을 보도록 하겠습니다. 개발자가 짠 백그라운드 쓰레드에서 돌아가는  자바 쓰레드 또는 Executor 와 같은 것들은 안드로이드 컴포넌트 생명주기와 크게 묶이지 않습니다. 액티비티관점에서 봤을 때 별개의 시작점과 종료점을 가지고 있고,  이것은 사용자와의 상호작용에 근간합니다. 어쨌거나 이러한 시작점과 종료점은 쓰레드의 생명주기와는 반드시 연결될 필요가 없습니다.

앞의 그림은 액티비티와 서비스에 관한 유의사항을 다이어그램으로 표현했습니다. 이 모든 포인트에 대한 세부설명은 뒷부분에서 설명할 것입니다.

서비스의 onCreate() 메소드는 서비스가 시작되거나 바인딩되려고 생성될 때 호출이 됩니다.

  • 서비스는 생성된 뒤 쓰레드 또는 익시큐터를 생성합니다.
  • 쓰레드가 종료될 때 서비스에게 종료를 알리고 싶다면  stopSelf()를 서비스내부에서 호출하면됩니다. 

쓰레드 또는 익시큐터에 작성한 코드는 백그라운드 쓰레드에서 시작되었는지 종료되었는지 여부를 서비스에게 알려야 합니다.

  • 쓰레드가 시작되면 서비스의 시작상태를 설정해야한다
  • 그리고 스레드가 멈추면 stopSelf()를 호출해야만 한다.

서비스의 onDestroy() 메소드는 서비스가 종료할 시점을 개발자가 알려줬을때 안드로이드 시스템에 의해서만 호출 됩니다. 서비스 입장에서는 쓰레드나 익시큐터 작업이 현재 수행중인지 알지 못합니다. 그러므로 서비스가 언제 시작되고 언제 끝나야하는지 알게 하는것은 개발자의 책임입니다.

두가지 종류의 서비스가 있습니다. 시작되는 서비스와 바운딩 되는서비스 입니다.
그리고 서비스는 동시에 둘다 될수도있습니다. 이제 세가지 종류의 서비스의 행동에 대해서 알아보겠습니다

  • 시작된 서비스
  • 바운드 서비스
  • 바운드 그리고 시작된 서비스

Android O의 변경

Oreo에서 백그라운드 서비스의 많은 부분이 변경되었습니다. 주요 변화 사항중 하나가 액티비티가 사라졌을 때 백그라운드에 있는 서비스가 지속적인 알림(persistent notification)을 가질수 없다는 것입니다. 반드시 이러한 알림이 필요하다면 startForegroundService() 메소드를 호출해야합니다. 그리고 5초이내에 startForeground()를 호출하여 서비스를 포어그라운드로 변경하지 않는 다면 ANR이 발생되게 됩니다.

시작된 서비스

시작된 서비스는 startService(Intent)메소드를 통해 시작될 수 있습니다. 이 인텐트는 직접만든 서비스 클래스를 참조하거나 애플리케이션의 패키지 이름을 포함하는 명시적 인텐트여야만 합니다. 다음 아래의 명시적 인텐트를 생성하는 코드를 확인해보세요.

서비스를 시작된 상태로 옮기기 위해 반드시 명시적 인텐트와 함께 startService()를 호출해야한다. 만약 그렇지 않으면 서비스는 시작되지 않는다. 서비스가 시작된 상태가 아니라면 포어그라운드로 옮길수도 없고 stopSelf()도 동작 하지 않는다.

만약 서비스를 시작된 상태로 놓지 않으면 지속적인 알림 또한 사용할 수 없다. 개발자가 서비스를 언제 시작상태에 두어야하는 지 알 때 고려해야할 중요한 사항이다.

서비스는 여러번 실행될수 있다. 시작될 때마다 onStartCommand()가 호출 된다. 명시적 인텐트에 포함된 엑스트라가 파라미터가 되어 이 메소드로 넘겨지게 된다. 비록 서비스가 여러번 시작 될 수 있지만, onCreate()는 단 한번만 호출된다. 바인딩된 서비스 일때에도 마찬가지다.  서비스를 죽이기 위해서는 서비스내부에서 stopSelf()를 호출해야된다. 서비스가 멈추고 아무것도 바인된것이 없을 때 onDestroy()가 호출된다. 명심해야할것은 시작된 서비스를 위해 시스템 자원이 할당 된다는 것이다.

인텐트

시작된 서비스는 인텐트와 함께 실행되는데, 서비스를 실행시키는 컴포넌트가 서비스와 연결되지 않는 상태에서 후에 서비스와 통신을 위해 연결을 하고 싶다면 다른 인텐트를 생성하여 다시 시작할수 있습니다. 이것은 시작된 서비스와 바운드 서비스의 주요 차이점중 하나입니다. 바운드 서비스는 client-server 패턴을 다르는데, 이때 클라이언트의 역할은  UI를 담당하는 액티비티나 또는 서비스가 되어 stub 또는 binder를 유지하고 서버역할을 담당하는 연결된 서비스의 메소드를 직접적으로 호출하게 된다.

조심해야할것은 안드로이드 O에서의 시작된 서비스이다. 더이상 백그라운드에서 지속적인 알림을 갖는 것을 허용하지 않기 때문에 서비스를 시작하기 위해 startForegroundService(Intent)를 실행해야한다.

포어그라운드 와 지속적인 알림 

시작된 서비스는 포어그라운드에서 동작할 수 있습니다. 다시 말해 서비스가 메인스레드에서 돌아가던지 백그라운드 스레드에서 돌아가던지 앞서 말한 포어그라운드라는 용어가 적용되지 않습니다.  이게 무슨 뜻이냐면 안드로이드 시스템은 낮은 시스템 리소스 환경에서도 서비스에게 우선순위를 높게 주고, 종료(Destroy)되지 않도록 노력 한다는 것입니다. 포어그라운드에서 동작하는 시작된 서비스는 아주 중요한 작업을 수행할 때 사용자를 실망시키지 말아야할 때 사용하면 됩니다.

다은과 같은 유즈케이스에 좋습니다.

  1. 애플리케이션이 녹화 또는 비디오/음악을 백그라운드에서 재생해야한다
  2. 지도/네비게이션 앱과 같이 백그라운드에서 위치를 측정해야할 때

시작된 서비스를 포어그라운드로 옮길 때 반드시 지속적인 알림을 나타내야 하고, 명백하게 사용자에게 서비스가 동작하고 있음을 알려야합니다. 이게 중요한 이유는 포어그라운드로 시작된 서비스는 더 이상 UI컴포넌트가 화면에 나타나지 않기 때문입니다. 그리고 사용자의 핸드폰에서 어떤작업을 수행하고 있는지 유져는 알길이 없기 때문에 이부분은 매우 중요한 사항이며, 화면에 나타내지 않는 다면 잠재적으로 배터리를 갉아먹어서 사용자에게 좋은 경험을 주지 못합니다.

여기 포어그라운드에서의 시작된 서비스의 예제가 있습니다.

안드로이드 O 이전이라면 다음과 같이 지속적인 알림을 표현합니다.

안드로이드 O에서는 알림 채널과 함께 지속적인 알림을 화면에 나타냅니다.

또한 MediaStyle 알림에 대한 더 자세한 정보는 아래의 글에서 살펴보실 수 있습니다. (오디오 백그라운드 플레이백은 시작된 서비스, 바운드 서비스 모두가 필요합니다.)

https://medium.com/androiddevelopers/migrating-mediastyle-notifications-to-support-android-o-29c7edeca9b7

Stopping Started Services

알림에게 주어진 PendingIntent에서 서비스를 멈추고 싶을 수 있다. 그럴땐 startService(Intent)를 호출할 때 Intent에게 적당한 인자를 넘겨서 onStartCommand()에서 처리한다.

시작된 서비스를 포어그라운드로부터 내리고 싶다면 stopForeground(true)를 호출한다. 이렇게 하면 지속적인 알림도 내려갈 것이다. 하지만 서비스를 중지 하지는 않는다. 서비스는 stopSelf() 등을 호출하면 된다.

서비스를 중지하기 위해 다음을 수행할 수 있다.

  1. 앞서 살펴본 코드와 같이, 인텐트의 엑스트라로 startService()를 호출할만한 인자를 넘기면 onStartCommand()에서 이를 수행할 것이고, 서비스내에서 결국 stopSelf()를 호출하여 서비스를 종료하게 된다. 만약 연결된 클라이언트(액티비티 등)이 없다면 onDestroy()가 호출될 것이고 서비스는 종료되게 된다.
  2. 서비스를 실행하기 위한 명시적 인텐트를 만들고 이를 stopService(Intent)에게 넘겨 stopSelf()가 호출되게 되도록 만들어 서비스를 종료시킬 수도 있다.

액티비티로 부터 시작된 서비스를 종료하는 샘플 코드이다.

시작된 서비스내에서 포어그라운드로부터 내리고 종료하는 것을 가정한다.

바운드 서비스

시작된 서비스와는 달리 바운드 서비스는 안드로이드 컴포넌트 사이에서 서비스와 연결을 설정하게 된다. 이 연결은 IBinder 라고 부르는 서비스의 메소드를 호출하게 해주는 녀석으로 부터 이루어진다. 서비스를 바인딩하는 가장 간단한 예제는 로컬 프로세스(하나의 앱) 내에서 클라이언트를 갖는 것이다. 이 경우에는 자바 객체(Binder의 서브클래스)는 클라이언트에게 노출되어지게 되고 서비스의 public 메소드에 접근할 수 있게 된다.

좀더 복잡한 시나리오는 바운드 서비스가 서로다른 클라이언트 프로세스의로부터 연결되는 것이다. Message 핸들러 또는 AIDL 코드가 있어야한다. 어쨌거나 로컬 프로세스내에서의 바인딩은 매우 간단하다.

바운드 서비스와 시작된 서비스의 차이점 목록을 확인하자

  • 클라이언트 컴포넌트는 시작된 서비스에게 연결을 갖지 않는다. 이것은 그저 인텐트를 수행할 때 startService() 또는 stopService()를 수행하여 시작된 서비스의 onStartCommand()를 수행하면 된다.
  • 클라이언트 컴포넌트( 액티비티, 프레그먼트 또는 다른 서비스)가 바운드 서비스에 연결하여 서비스의 메소드 호출을 원할때에는 IBinder 객체가 필요하다.

어떠한 경우간에 하나의 프로세스내에서 서비스가 연결된 클라이언트 또는 서비스를 시작한 컴포넌트에게 메시지를 보낼 필요가 있다면 LocalBroadcastManager와 같은 어떤 무언가가 필요하다.  바운드 서비스들은 보통은 다른 컴포넌트에 직접 연결 하지는 않는 편이다.

bindService() 그리고 onCreate()

클라이언트 컴포넌트를 서비스에 연결하기 위해 명시적인텐트와 함께  bindService()를 반드시 호출해야한다. 샘플 코드를 보자.

bindService()를 호출할 때 인자로 넣는 BIND_AUTO_CREATE는 매우 일반적인 플레그이다. (다른 플레그도 물론 존재한다) 자동으로 생성(auto create)다는 것은 연결된 서비스가  없다면 bindService()를 호출하면서 onCreate()를 호출하겠다는것이다. 기본적으로는 첫번째 클라이언트에 연결될때 자동적으로 바운드 서비스가 생성이 된다.

일단 bindService()가 호출되고 나면 서비스는 클라이언트에게 무언가 반응을 보일 방법이 필요하다 그리고 IBinder 객체에게 이제 서비스의 메소드를 호출할 수 있음을 알린다. 이러한 일들은 ServiceConnection에 의해 일어난다. 바운드서비스는 클라이언트들에게 연결과정이 끝났음을 알리기 위해 ServiceConnection  콜백을 사용한다. 바운드 서비스의 연결이 끊어지더라도 이를 통해 클라이언트가 알 수 있게 된다.

ServiceConnection 구현 예제를 보자.

서비스 바인더

클라이언트가 bindService(Intent)를 호출했을때 바운드 서비스 측면에서 어떤 일들이 일어나는지 함께 보자.

바운드 서비스에서 개발자는 onBindg()메소드를 구현해야하만 한다. 이것은 클라이언트 컴포넌트게 연결되는 첫번째 순간에 딱 한번만 호출될 것이다.

샘플 코드를 보자.

바운드 서비스는 IBinder타입의 mBinder객체를 생성한다.

그래서 IBinder가 무엇이란 말인가?

Binder는 안드로이드 기반의 클래스로 원격 객체의 생성을 허용해준다. 가벼운 RPC 메커니즘의 구현으로 높은 퍼포먼스와 교차 프로세스간, 즉 클라이언트와 바운드 서비스간의 메소드 call을 구현한다.

예제를 확인하자.

앞선 예제를 살펴보면 우리는 간단하게 클라이언트에게 getService()메소드를 노출시켰다. IBinder의 참조와 함께 클라이언트는 이제 바운드 서비스 객체에 직접적으로 접근하여 public 메소드를 호출 할 수 있게 되었다. 알아둬야 할 것은 메소드는 클라이언트의 쓰레드로 실행된다는 것이다. 액티비티 또는 프레그먼트의 경우 이 메소드는 메인스레드에서 동작하니 바운드서비스에서 블락킹 메소드를 호출하지 않도록 조심해야한다. 그렇지 않으면 ANR를 맛보게 된다.

연결해제와 onDestroy

바운드서비스로부터 연결을 해제하기 위해 간단히 unBindService(ServiceConnection)을 호출 할 수 있다. 시스템은 onUnbind()메소드를 바운드 서비스내에서 호출하게 될것이다. 만약 연결된 클라이언트가 없다면 서비스가 시작상태일지라도 바운드 서비스내에서 onDestroy()가 호출된다. 서비스가 시작된 상태가 아니라도 onDestroy()가 즉시 호출되고 바운드 서비스는 죽게 된다.

클라이언트 컴포넌트에서 unbindService()를 호출하는 예제를 살펴보자

앞의 코드를 살펴보면 액티비티의 onStop()메소드는 unbindService()를 호출하기 위해 재정의 되었다. 사용자의 경험이 중요시 되는 앱이라면 클라이언트 컴포넌트는 bind(연결) 그리고 unbind(연결해제)를 onStart() 또는 onStop()에서 할 수 있다. 또는 다른 안드로이드 액티비티, 프레그먼트, 서비스의 생명주기 메소드내에서 호출해도 상관없다.

onUnbind() 대한 예제이다.

전형적으로 false를 반환한다. 만약 그렇지 않다면 다음 클러이언트가 연결할때 onBind()대신 onRebind()가 호출된다.

바운드 그리고 시작된 서비스

애플리케이션에서 서비스를 요하는 많은 용례가 존재한다. 앞의 내용들에서 많은 세부내용을 언급했지만 그것들은 보통 생성과 소멸에 대한 내용만 포함했다.

바운드 서비스와 시작된 서비스 모두 메소드를 가질 수 있고, 이를 연결된 클라이언트 컴포넌트로부터 호출될 수 있다. 클라이언트가 서비스에 바인딩하기 위해 서비스를 시작하지 않아도 되므로 반드시 알아야한다. 이는 서비스에 바인딩 된 클라이언트가 onCreatre()를 호출함을 의미한다. 서비스를 시작상태로 옮기지 않으면 클라이언트가 서비스에서 바인딩을 해제하면서 서비스가 종료되고 onDestro() 메소드가 호출된다.

UI 컴포넌트가 서비스에 바인드되어 작성되는 상황일 때 어,느 시점에서 UI가 서비스에서 바인딩 해제되고, 오래 실행되는 작업을 수행하는 중이면 onDestroy()가 호출되어 종료됩니다. 앱 요구 사항에 따라 바인드 서비스가 UI 구성 요소의 생명주기 종료 이후에도 계속 실행되어야하는 경우 시작하고 포어그라운드로 이동하여 지속적으로 알림을 표시해야합니다. 이렇게하면 바운드 서비스와 시작된 서비스가 필요한 동안 또는 사용자가 서비스를 중지하기 위해 PendingIntent를 실행하여 서비스를 종료하기로 결정할 때까지 계속 실행됩니다.

시작상태로 옮기기

서비스에 바인딩 된 클라이언트는 서비스를 시작 상태로 이동하지 않으므로 바운드 및 시작 서비스의 경우를 대비하여 서비스 자체를 시작 상태로 옮기는 것이 안전합니다.

앞의 예제에서 

  1. commandStart()는 서비스에 클라이언트가 연결될 때 호출된다.
  2. 또는 인텐트를 통해 startService() 또는 startServiceInForeground()(안드로이드 O 이상에서)에 의해 호출 된다.

예제에서 보여주는 것은 실제로 익시큐터를 만들기 전에 서비스가 시작된 상태에 놓이는 것입니다. 

클라이언트 컴포넌트가 서비스에 연결된 후 commandStart()가 호출됬다고 가정했을 때 , 서비스는 아직 시작 되지 않았다.

  1. 만약 서비스가 클라이언트에 연결된다면, 시작되지 않은것이다 그리고 mServiceStarted는 false이다.
  2. 이 경우 moveToStarted () 상태에 대한 호출은 단순히 Extra (Command.START)를 사용하여 명시적 Intent를 작성하고 startService () 또는 startForegroundService ()를 호출합니다.
  3. 이것은 마침내 onStartCommand()를 호출하게 되고 caommandStart()를 다시 가르키게 됩니다.
  4. 어쨌거나 이때 commandStart() mServiceIsStarted는 true로 변경되고 이것은 사실상 commandStart()를수행하여 Executor를 생성하게 합니다.

소멸 그리고 연결해제(Destruction and unbinding)

클라이언트 컴포넌트가 서비스로 부터 연결이 해제가 될때 만약 시작상태가 아니라면 서비스에서 onDestroy()가 호출될 것입니다.

하지만 시작상태라면 서비스가 죽진 않습니다. 시작된 서비스가 멈추어야만 종료되게 됩니다. ( stopService(Intent) 또는 인텐트에 종료메시지가 포함된 startService(Intent)를 호출했을 때 )

여기 연결된 서비스와 바운드 서비스 사이의 이러한 상태와 전환에 대해 요약한 다이어그램을 살펴보세요.

소스코드 예제

이 예제에서 살펴본 Awake App에 대한 소스코드를 살펴볼수 있습니다. 간단한 유틸리티 앱으로 Android O 와 N에 대해 충전하는 동안 화면이 켜져있을수 있게 하는 기능이 포함되어있습니다.

Buy me a coffeeBuy me a coffee
카테고리: Android

0개의 댓글

답글 남기기

이메일은 공개되지 않습니다.