본문 바로가기
Android/안드로이드 기본지식

[Android] Service 배우기

by 붕어사랑 티스토리 2022. 5. 25.
반응형

https://developer.android.com/guide/components/services?hl=ko 

 

서비스 개요  |  Android 개발자  |  Android Developers

서비스 개요 Service는 백그라운드에서 오래 실행되는 작업을 수행할 수 있는 애플리케이션 구성 요소이며 사용자 인터페이스를 제공하지 않습니다. 다른 애플리케이션 구성 요소가 서비스를 시

developer.android.com

서비스란?

서비스는 안드로이드 어플리케이션을 구성하는 4개의 컴포넌트(액티비티, 서비스, 브도르캐스트, 컨텐트 프로바이더) 중 하나로 백그라운드에서 수행작업을 할 때 사용되는 구성요소이다.

 

 

 

서비스의 타입

서비스는 총 3가지의 타입이 존재한다

 

 

1. Foreground 서비스

 

포어그라운드 서비스는 유저에게 보이는 작업을 수행하는 서비스이다. 대표적인 예시로 음악을 재생하는 것이 있다.

포어그라운드 서비스는 반드시 Notification을 표시해야 한다. 포어그라운드 서비스는 유저가 앱과 소통하지 않아도 계속 진행된다.

 

 

2. Background 서비스

 

백그라운드 서비스는 유저에게 보이지 않는 작업을 수행하는 서비스이다.

 

 

 

3. Bound 서비스

 

앱이 서비스를 시작할 때 bindService() 를 호출하여 서비스를 실행하면, 이를 bound 되었다고 한다.

바운드 서비스는 클라이언트-서버 인터페이스를 제공하며, 컴포넌트들이 서비스와 소통 가능토록 해준다.

 

소통은 리퀘스트를 날리거나, result를 받아오고, 또는 프로세스간 통신(IPC) 가 가능하다.

 

바운드 서비스는 바운드된 어플리캐이션이 살아있을 때 만 실행된다. 여러 컴포넌트들이 하나의 서비스에 동시 접근 가능하다.

 

만약 모든 컴포넌트들이 서비스를 unbind하면, 서비스는 destroyed 된다.

 

 

 

 

 

 

서비스의 기본 사용법

앞서 세가지 종류의 서비스를 배웠다. 이를 다시 두개의 그륩으로 묶어서 분류하면 독립적으로 실행되는 started 서비스(포어그라운드, 백그라운드)와 Bound 서비스로 나누는게 가능하다.

 

두 그륩의 차이점은 서비스안에 onStartCommand() 메소드와 onBind() 메소드중 어느걸 구현해서 사용하느냐에 따라 달렸다.

 

onStartCommand() : 서비스를 독립으로 사용할 때 호출, startService() 함수에 의해 호출된다.

onBind() : 서비스를 Bound로 사용할 때 호출, bindService() 함수에 의해 호출된다.

 

 

서비스와 커뮤니케이션 하는 방법은 서비스의 종류에 상관없이 동일하게 Intent를 이용하면 된다.

 

만약 다른 앱과 서비스와의 커뮤니케이션을 막고 싶으면 매니페스트 파일에서 private로 선언하면 된다.

 

 

 

 

 

 

서비스 구현 기초

서비스를 구현하는 방법은 Service클래스를 상속받으면 된다. 그리고 몇몇 콜백들을 구현해야 한다.

아래 네가지 콜백들은 서비스에서 가장 중요한 콜백이니 익히고 가자

 

 

  • onStartCommand()

이 함수는 startService() 함수가 호출되면 호출된다. 앞서 말한것 처럼 독립적인 서비스가 필요할 때 구현해야 한다.

startService()로 서비스를 호출했다면 stopSelf() 메소드나 stopService() 함수가 호출될 때 까지 서비스는 계속 살아있다.

 

 

  • onBind()

bindService() 함수를 호출할 때 호출된다. Bound 서비스를 사용하고 싶다면 반드시 구현해야 한다.

이 메소드는 클라이언트와 소통할 IBinder를 반드시 리턴해야한다. 만약 binding을 허락하고 싶지 않다면 null을 리턴하자

 

 

  • onCreate()

서비스가 생성될 때 한번만 호출된다. 순서는 onStartCommand()onBind()보다 앞선다.

 

 

  • onDestroy()

서비스가 파괴될 때 호출된다. 이 메소드에서 리스너 및 리시버 제거, 리소스 정리를 해 주어야 한다.

 

 

 

 

 

매니페스트에 서비스 선언하기

서비스를 사용하기 위해선 아래와 같이 매니페스트에 서비스를 선언해 주어야 한다.

<manifest ... >
  ...
  <application ... >
      <service android:name=".ExampleService" />
      ...
  </application>
</manifest>

필요한 attribute 속성은 name이며 서비스 클래스 이름이 된다.

만약 다른앱이 서비스를 사용하는걸 막고 싶은 경우 exported를 false값으로 주자.

 

 

 

 

 

서비스 사용시 주의사항

서비스를 사용할 때는 반드시 새로운 스레드를 생성해 주어야 한다.

 

서비스는 기본적으로 어플리케이션의 main thread를 이용하므로, 별도 스레드 생성없이 작업할 코드를 수행하면 메인 스레드에 과부하가 걸린다.

 

서비스가 살아있는 기간

서비스는 start로 실행되었을 경우 stopSelf() 또는 stopService()가 호출되기전 까지 무한히 살아있다.

반면 bind로 실행되었을 경우, 바인드되는 컴포넌드가 더이상 없을 경우 자동으로 destroy 된다.

 

서비스 구현 예시

kotlin

class HelloService : Service() {

    private var serviceLooper: Looper? = null
    private var serviceHandler: ServiceHandler? = null

    // 메인 스레드로부터 메세지를 전달받을 핸들러 선언
    private inner class ServiceHandler(looper: Looper) : Handler(looper) {

        override fun handleMessage(msg: Message) {
            // 보통 서비스에서는 파일 다운로드 같은 작업을 수행함
            // 이 예제에서는 sleep. 5초를 주도록 함
            try {
                Thread.sleep(5000)
            } catch (e: InterruptedException) {
                // Restore interrupt status.
                Thread.currentThread().interrupt()
            }
            // 서비스를 사용하였다면 서비스를 종료해 주어야 함.
            // 아래 메소드는 작업 startId가 가장 최신일때만 서비스를 stop하게 함
            // 이렇게 하면 동시에 여러 작업할 때, 모든작업이 끝나야 stop이 된다
            // 이게뭔지는 이후 설명에서 나옴
            stopSelf(msg.arg1)
        }
    }

    override fun onCreate() {
        // 서비스는 메인스레드에서 동작하므로 서비스의 작업은 따로 스레드를 선언해 주어야 한다.
        // CPU priority를 background로 선언하여 ui의 버벅임을 방지해야 한다.
        HandlerThread("ServiceStartArguments", Process.THREAD_PRIORITY_BACKGROUND).apply {
            start()

            // 스레드의 루퍼를 얻어와 핸들러를 만들어준다.
            serviceLooper = looper
            serviceHandler = ServiceHandler(looper)
        }
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()

        // For each start request, send a message to start a job and deliver the
        // start ID so we know which request we're stopping when we finish the job
        serviceHandler?.obtainMessage()?.also { msg ->
            msg.arg1 = startId
            serviceHandler?.sendMessage(msg)
        }

        // If we get killed, after returning from here, restart
        return START_STICKY
    }

    override fun onBind(intent: Intent): IBinder? {
        // We don't provide binding, so return null
        return null
    }

    override fun onDestroy() {
        Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
    }
}

java

public class HelloService extends Service {
  private Looper serviceLooper;
  private ServiceHandler serviceHandler;

  // 메인 스레드로부터 메세지를 전달받을 핸들러 선언
  private final class ServiceHandler extends Handler {
      public ServiceHandler(Looper looper) {
          super(looper);
      }
      @Override
      public void handleMessage(Message msg) {
          // 보통 서비스에서는 파일 다운로드 같은 작업을 수행함
          // 이 예제에서는 sleep. 5초를 주도록 함
          try {
              Thread.sleep(5000);
          } catch (InterruptedException e) {
              // Restore interrupt status.
              Thread.currentThread().interrupt();
          }
          // 서비스를 사용하였다면 서비스를 종료해 주어야 함.
          // 아래 메소드는 작업 startId가 가장 최신일때만 서비스를 stop하게 함
          // 이렇게 하면 동시에 여러 작업할 때, 모든작업이 끝나야 stop이 된다
          // 이게뭔지는 이후 설명에서 나옴
          stopSelf(msg.arg1);
      }
  }

  @Override
  public void onCreate() {
    // 서비스는 메인스레드에서 동작하므로 서비스의 작업은 따로 스레드를 선언해 주어야 한다.
    // CPU priority를 background로 선언하여 ui의 버벅임을 방지해야 한다.
    HandlerThread thread = new HandlerThread("ServiceStartArguments",
            Process.THREAD_PRIORITY_BACKGROUND);
    thread.start();

    // Get the HandlerThread's Looper and use it for our Handler
    serviceLooper = thread.getLooper();
    serviceHandler = new ServiceHandler(serviceLooper);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
      Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

      // For each start request, send a message to start a job and deliver the
      // start ID so we know which request we're stopping when we finish the job
      Message msg = serviceHandler.obtainMessage();
      msg.arg1 = startId;
      serviceHandler.sendMessage(msg);

      // If we get killed, after returning from here, restart
      return START_STICKY;
  }

  @Override
  public IBinder onBind(Intent intent) {
      // We don't provide binding, so return null
      return null;
  }

  @Override
  public void onDestroy() {
    Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
  }
}

 

위 예시에서는 모든 작업을 onStartCommand()에서 받도록 설계되어 있고, 작업이 들어오면 post를 통해 handler로 전달시켜준다.

또한 위 예제는 리퀘스트가 들어올 때 마다 하나씩 순서대로 처리해준다. 이는 IntentService 동작과 비슷하다.

만약 작업을 병렬로 동시에 여러개 수행하고 싶으면 thread pool을 이용하면 된다.

 

 

여기서 주목할점은 onStartCommand() 메소드는 반드시 integer를 리턴해야 하는 점이다.

이 값은 시스템이 서비스를 죽였을 때, 서비스를 어떻게 재시작 할 지를 나타낸다.

 

 

  • START_NOT_STIKY

만약 시스템이 onStartCommand() 값 리턴 이후 서비스를 kill했다면. 서비스를 재시작하지 않는다.

서비스를 재시작하려면 다시 pending intent를 전달해야 한다

 

  • START_STICKY

만약 시스템이 onStartCommand() 값 리턴 이후 서비스를 kill했다면. 서비스를 다시 생성하고 onStartCommand()를 다시 호출한다. 그러나 마지막 intent는 다시 전달해 주지 않는다. 이 값은 보통 미디어 플레이어에 사용된다.(작업을 실행하지 않고 계속 waiting할 때)

 

 

  • START_REDELIVER_INTENT

만약 시스템이 onStartCommand() 값 리턴 이후 서비스를 kill했다면. 서비스를 다시생성하고 onStartCommand()를 다시 호출하고 마지막 intent까지 전달 해 준다. 서비스가 죽어도 바로 다시 resumed 되야 할 작업을 해야 할 때(예를들면 파일 다운로드) 사용한다.

 

 

 

 

서비스 시작방법, 펜딩 인텐트 사용법

서비스를 시작하는 방법은 앱 컴포넌트에서 startService() 혹은 startForegroundService()에 Intent를 전달하면 된다.

그러면 onStartCommand()가 호출되고 Intent가 전달된다.

 

여기서 foreground service란, 서비스 그자체는 백그라운이지만, 시스템에서 이를 포어그라운드로 인식하게 만드는 서비스이다.

 

kotlin

Intent(this, HelloService::class.java).also { intent ->
    startService(intent)
}

java

Intent intent = new Intent(this, HelloService.class);
startService(intent);

 

 

 

펜딩인텐트

만약 서비스가 binding을 허락하지 않으면(앞서 배운것처럼 IBinder를 null로 리턴하면), 서비스랑 앱 컴포넌트만이 커뮤니케이션 할 수 있다. 그러나 만약 서비스가 result값을 전달하기를 원할경우, 클라이언트에서 Broadcast를 이용할 PendingIntent 사용하여 서비스에 전달해주면 된다. 서비스는 전달받은 펜딩 인텐트를 이용하여 broadcast를 이용해 결과값을 전달한다.

 

 

 

 

 

서비스 중단하기

start로 시작된 서비스는 서비스 자체의 고유한 라이프사이클을 가지고 있다. 즉 시스템이 메모리가 부족해서 kill하지 않는이상 시스템이 직접 stop이나 destroy하지 않는다. start된 서비스는 반드시 stopSelf() 나 stopService()를 통해 중단해야 한다.

 

 

서비스가 stopSelf()나 stopService()로 중단요청을 받으면, 시스템은 가능한 서비스를 빠르게 소멸시키려 한다.

 

 

만약 서비스가 여러개의 작업을 동시 처리해야 할 때 stop은 어떻게 처리를 할 까?

예를들어 A라는 작업중간에 B라는 작업이 들어오고, A작업이 끝난 후 stop을 했다고 하자. 그럼 B작업은 처리가 되지 못할 것이다.

이를 해결하기 위해 stopSelf(int) 라는 메소드가 제공된다.

 

 

앞선 예제에서 아래 부분이 바로 이에 해당한다.

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
...............................................
        serviceHandler?.obtainMessage()?.also { msg ->
        msg.arg1 = startId
        serviceHandler?.sendMessage(msg)
    }
...............................................
    return START_STICKY
}

override fun handleMessage(msg: Message) {
...............................................
            stopSelf(msg.arg1)
...............................................
        }

 

서비스는 내부적으로 startId라는 걸 가지고 있고, 리퀘스트가 올 때 마다 새로운 값을 부여한다.

그리고 stopSelf(startId) 메소드를 호출 할 때, 메소드에 전달되는 startId와, 가장 최신의 startId를 비교하고 일치하면 서비스를 stop한다.

이러한 방식을 사용하면 리퀘스트가 여러개 들어 왔을 때, 모든 작업이 끝날때만 service를 stop 할 수 있다.

 

 

 

 

 

Bound Service 만들기

bound service는 서비스를 bindService()로 호출할 때 생성된다. bound service는 주로 액티비티나 앱의 다른 컴포넌트들과 interact 할 일이 있을 때, 혹은 다른 애플리케이션과 통신할 일이 있을 때(IPC 통신) 사용된다.

 

bound 서비스를 사용하려면 먼저 onBind() 콜백을 구현하고 IBinder를 리턴해 주어야 한다.

 

IBinder는 서비스와 클라이언트의 소통하는 인터페이스이다. bound service를 이용하려면 필히 구현을 해 주고 onBind()에서 리턴해주어야 한다.

 

다른 어플리케이션들도 bindService()를 호출하여 서비스를 이용할 수 있다. bound service는 서비스와 bind된 앱들을 위해서만 일하며 만약 서비스와 더이상 바인딩된 애플리케이션 컴포넌트가 없을 시, 시스템은 서비스를 destroy 한다. 그러므로 start로 시작한 서비스처럼 stop을 꼭 해줄 필요가 없다.

 

클라이언트가 서비스와 interaction을 끝내고 더이상 서비스가 필요 없다면, unbindService() 를 호출하면 바인딩이 해제된다.

앞서말한것 처럼 서비스가 더이상 바인딩된 컴포넌트가 없으면 시스템에서 destroy한다.

 

bound service는 구현방법이 다양하고 start로 시작한 서비스보다 구현이 좀 더 복잡하다. 자세한 구현방법은 아래 링크를 참조하자

 

https://developer.android.com/guide/components/bound-services

 

바인드된 서비스 개요  |  Android 개발자  |  Android Developers

바인드된 서비스 개요 바인드된 서비스란 클라이언트-서버 인터페이스 안의 서버를 말합니다. 이를 사용하면 구성요소(예: 활동)를 서비스에 바인딩하고, 요청을 보내고, 응답을 수신하며, 프로

developer.android.com

 

 

 

 

유저에게 notification 보내기

서비스가 running 상태일 때, 유저에게 notification을 보낼 수 있다. 사용되는 notification은 두가지로 하나는 Toast notification이며 다른 하나는 status bar notification이다. 이를 통해 유저에게 서비스가 동작하고 있음을 알리며, status bar notification을 사용하는것을 권장한다.

 

See the Toast Notifications or Status Bar Notifications developer guides for more information.

 

 

 

 

서비스의 라이프 사이클

서비스의 라이프사이클은 액티비티보다 훨씬 간단하다. 허나 유저의 눈에 보이지 않기에 더 신경 써 주어야 한다.

 

서비스의 생명주기는 어떻게 시작되냐에 따라 두가지 path로 나뉜다

 

 

  • started 서비스

startService()에 의해 시작된 서비스이다. 이 서비스는 무한으로 돌기 때분에 사용후 반드시 stop을 해 주어야 한다. stop을 하는 방법은 서비스 스스로 stopSelf()를 콜 하거나 다른 컴포넌트에서 stopService()를 호출해야 한다. 서비스가 스탑되면 시스템에서 서비스를 destroy한다.

 

  • bound 서비스

bindService()로 시작된 서비스이다. 이 서비스는 IBinder라는 인터페이스를 제공하며 클라이언트와 소통한다. 다수의 클라이언트가 같은 서비스에 바인들 될 수 있고, 서비스가 바인드된 클라이언트가 더이상 없을 시 시스템에서 destroy한다. 그러므로 굳이 알아서 서비스를 스탑 할 필요가 없다.

 

 

 

한가지 주목할점은 위 두가지 서비스의 path는 완전히 다른것이 아니다. stated 서비스도 binding을 할 수 있다.

 

가령 예를들어 startService()를 통해 음악을 재생하는 플레이어 서비스를 만들었다고 하자. 음악이 재생되는동안 이 플레이어를 컨트롤 하고 싶다면, 가령 볼륨조절이나 다음곡 재생등, 앱에서 bindService()를 호출하여 플레이어 서비스를 조작할 수 있다.

 

이러한 경우 stopService나 stopSelf가 바로 먹히지 않는다. 모든 클라이언트들이 unbind 된 후에 stopService나 stopSelf가 먹히고 스탑된다.

 

 

 

 

아래 예제는 구현해야 될 라이프사이클 콜백들이다.

 

 

kotlin

 

class ExampleService : Service() {
    private var startMode: Int = 0             // 서비스가 kill 될 때, 어떻게 동작할지를 나타냄
    private var binder: IBinder? = null        // bind 된 클라이언트와 소통하기 위한 인터페이스
    private var allowRebind: Boolean = false   // onRebind() 메소드가 사용될지 말지를 결정함

    override fun onCreate() {
        // 서비스가 생성 될 때
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // startService()에 의해 서비스가 시작될 때
        return startMode
    }

    override fun onBind(intent: Intent): IBinder? {
        // bindService()에 의해 서비스가 시작될 때
        return binder
    }

    override fun onUnbind(intent: Intent): Boolean {
        // 모든 클라이언트가 unbindService()를 호출했을 때
        // 즉, 더이상 바인딩된 클라이언트가 없을때
        return allowRebind
    }

    override fun onRebind(intent: Intent) {
        // onUnbind()가 호출된적이 있는 상태에서, 다시 bindService()를 통해 바인딩 할 때
    }

    override fun onDestroy() {
        // 서비스가 파괴될 때
    }
}

java

public class ExampleService extends Service {
    int startMode;       // 서비스가 kill 될 때, 어떻게 동작할지를 나타냄
    IBinder binder;      // bind 된 클라이언트와 소통하기 위한 인터페이스
    boolean allowRebind; // onRebind() 메소드가 사용될지 말지를 결정함

    @Override
    public void onCreate() {
        // 서비스가 생성 될 때
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // startService()에 의해 서비스가 시작될 때
        return startMode;
    }
    @Override
    public IBinder onBind(Intent intent) {
        // bindService()에 의해 서비스가 시작될 때
        return binder;
    }
    @Override
    public boolean onUnbind(Intent intent) {
        // 모든 클라이언트가 unbindService()를 호출했을 때
        // 즉, 더이상 바인딩된 클라이언트가 없을때
        return allowRebind;
    }
    @Override
    public void onRebind(Intent intent) {
        // onUnbind()가 호출된적이 있는 상태에서, 다시 bindService()를 통해 바인딩 할 때
    }
    @Override
    public void onDestroy() {
        // 서비스가 파괴될 때
    }
}

 

 

 

반응형

댓글