https://velog.io/@woga1999/Android-Service
전편에서 이야기했던 거처럼 이 UnBounded Service
를 Started Service
라고 부르겠다
스타티드 서비스는 Context의 startService() 메서드로 시작된다.
이 메서드 호출 시점에 서비스가 바로 시작되지는 않고, 메인 Looper의 MessageQueue에 Message가 들어가서 메인 스레드를 쓸 수 있는 시점에 서비스가 시작된다.
startService()
를 통해 서비스가 처음 생성되는 경우는
onCreate() : Service에 필요한 리소스 등을 준비 작업 ->
onStartCommand() : 명령을 매번 처리하는 역할 순으로 실행한다.
그 이후에 startService()를 호출하면 onCreate()를 거치지않고 바로 onStartCommand()
가 실행된다.
서비스에서 작업하고 있다는 걸 알리고자 '작업 중'임을 알리는 애니메이션을 화면에 실행한다. UI 동작이라 메인스레드에서 정상 동작한다.
그런데 Service의 onStartCommand()
에서 백그라운드 스레드를 사용하지 않고 작업을 진행해서 메인 스레드를 점유했기 때문에 '작업 중'인 애니메이션은 동작하지 않는다.
서비스 -> 액티비티 메세지는 일반적으로 브로드캐스트를 사용한다
Service를 시작하는 Intent에 ResultReceiver
를 전달하고, 서비스에서 ResultReceiver
에 값을 되돌려줄 수도 있다.
가용 메모리가 낮거나 포커스를 갖고 있는 액티비티의 시스템 리소스를 복구해야 할 때 안드로이드 시스템은 강제로 서비스를 종료시킬 수 있다.
여기서 스타티드 서비스는 강제 종료 후 가능한 한 빨리 시스템에서 서비스를 재시작한다.
onStartCommand()
에서 리턴하는 int 상수를 가지고 재시작 방식을 제어한다.
강제 종료되면 재시작하지 않는다
정상적으로 종료되지 않았을 때 재시작한다. (메서드의 기본 리턴 값이다)
주의할 점이, 재시작시 onStartCommand()
를 호출하는데 이때 Intent 파라미터가 null로 전달된다.
그래서 이 인텐트 값을 사용하면 NPE 발생 가능성이 있다. 따라서 전달된 Intent를 사용하지 않고 내부 상태 변수만 사용하는 서비스에 적합하다.
재시작하면서 onStartCommand()
에 Intent를 다시 전달하여 실행한다.
만약 시작 액티비티에서 오류가 발생했다고 가정해보자.
보통 앱 아이콘을 누르면 -> Application의 onCreate()가 startService()
를 실행해도, 메세지큐 순서상 액티비티가 먼저 시작되고 서비스가 시작된다.
이 때 액티비티의 onCreate에서 크래시가 발생하면, 프로세스는 죽지만 com.android.server.am.ActiveServices
에서 펜딩 서비스를 실행하기 위해서 다시 프로세스를 띄운다.
프로세스가 뜰 때 액티비티는 띄우는 대상이 아니므로 Application을 생성한 이후 바로 서비스만 시작된다.
서비스를 정상적으로 종료하지 않으면 작업이 다 끝났는데 재시작하는 경우가 있다. 불필요하게 재시작하지 않는 것도 중요하다.
서비스를 종료하는 방법 중에 stopService()
메서드가 있는데 얘보다는 Service의 stopSelf()
의 사용 빈도가 높다.
서비스에서 할 일이 끝났으면 백그라운드 스레드든 아니든 stopSelf()
를 실행해서 서비스를 명시적으로 종료한다.
만약 할 일이 다 끝났는데 서비스가 started 상태로 남아 불필요하게 메모리를 차지하고 있으면 메모리 이슈로 서비스가 강제 종료 되면서 onStartCommand()
리턴 상수가 재시작하는 경우면 의도치 않게 재시작하는 일이 생긴다.
여러 곳에서 startService()
를 동시에 호출할 수 있다
그래서 스레드가 여러 개가 동시에 실행될 수 있다.
stopSelfResult()
메서드를 사용하자startService()
를 실행한다면 모든 작업이 끝났을 때 시점을 알 수 있을까? 그를 위해 이 함수가 존재한다.IntentService
는 내부적으로 1개의 백그라운드 스레드를 가지고 전달된 Intent를 순차적으로 처리한다.
(내부는 HandlerThread 사용)
그래서 onHandlerIntent(Intent intent)
만 구현하면 된다
public class NewReaderService extends IntentService {
public NewsReaderService() {
super("NewsReader");
}
@Overrice
protected void onHandleIntent(Intent intent) {
...
}
IntentService에는 기본 생성자가 없어서 생성자도 같이 추가해야 한다.
이 클래스의 구조는 단순하다.
onCreate에서 HandlerThread를 생성하고 시작 ->
HandlerThread의 Looper와 연결된 Handler를 만듦 ->
onStartCommand에서 Handler에 메시지를 보냄 ->
Handler의 handleMeesage()에서 IntentService의 onHandleIntent() 실행 ->
handleMessage() 메서드에서는 onHandleIntent()가 끝나면 stopSelf(int startId)
호출로 종료
백그라운드 스레드에서 토스트를 띄우려고 하면 Toast 내부 클래스인 TN
에서 Handler로 기본 생성자를 사용하는 부분이 있기 때문에 Looper가 없다고 에러를 발생시킨다.
그러면 Looper.prepare()
를 발생시키면 에러를 방지할 수 있지만 인텐트 서비스에서는 어떨까?
이미 내부적으로 사용하는 HandlerThread
에서 이미 Looper.prepare가 실행되어서 토스트가 잘 뜨는 것처럼 보인다.
Looper.quit()
를 호출한다.Looper가 종료되면서 Toast를 보여주는 콜백과 Toast를 제거하는 콜백이 실행되지 않을 수도 있고 둘 중 하나만 실행될 수도 있다.
만약 제거하는 콜백이 실행되지 않으면 계속 남아있는 현상이 발생한다.
그러므로 Toast는 가급적 메인 스레드에서만 띄우는 게 맞다. 인텐트서비스에서 된다고 무작정 쓰고 넘어가지 말자.
간혹 서비스를 onStartCommand()
가 아닌 onCreate()
에서 백그라운드 서비스를 생성하여 작업을 처리하곤 한다.
이뉴는 여러곳에서 startService()를 호출하는 경우에도 매번 스레드를 시작하지 않고, 이미 시작되었으면 나머지는 스킵하기 위해서다
다시한번 말하지만
onStartCommand()
는 메인스레드에서 동작한다.
그래서 단순히 isRunning
과 같은 boolean 값만으로도 체크할 수 있다.
물론 스킵 시 스킵된 요청이 시스템에 의해서 재시작되는 것을 방지하기 위해 START_NOT_STICKY
를 리턴하자