본문 바로가기

안드로이드

서비스

반응형

서비스는 UI를 제공하지 않고 백그라운드에서 실행되는 컴포넌트이다.

서비스 자체가 메인스레드가 아닌 별도 스레드에서 실행하는 것으로 착각하면 안된다. 서비스에서 UI를 블로킹하는 작업이 있다면 백그라운드 스레드를 생성해 작업을 진행해야 한다.

서비스는 단일 인스턴스로 실행된다. 그렇기 때문에 일부러 싱글톤으로 만들 필요는 없다.

서비스 시작 방법

startService()bindService() 메서드 두가지가 있다.

스타티드 & 바운드 서비스

서비스는 보통 스타티드 서비스 or 바운드 서비스로 존재하는데, 스터디드이면서 바운드일 수 있다. 이는 코드도 복잡하고 고려해야할 것이 두 배 이상이 되기 때문에 피하는 게 좋지만 어쩔 수 없이 사용해야 하는 경우가 있다. 예를 들어, 음악 재생화면이 있을 때 종료해도 음악을 들을 수 있으려면 스타티드를 사용해야한다. 근데 다시 화면에 진입할 때 재생 중인 음악 정보를 보여줘야 한다면 바운드 서비스이기도 해야 한다. 

스타티드 서비스

startService()를 호출하는 시점에 바로 서비스가 시작되는 않는다. 메인 Looper의 MessageQueuedp Message가 들어가서 메인 스레드를 쓸 수 있는 시점에 서비스가 시작된다.

onCreate()와 onStartCommand() 

Service가 처음 생성되는 경우에는 onCreate()를 거쳐서 onStartCommand() 메서드를 실행한다. 그 이후에 startService()를 호출하면 onCreate() 메서드는 거치지 않고 onStartCommand() 메서드가 실행된다. onCreate()는 Service에 필요한 리소스 등을 준비하는 작업을 하고 onStartCommand()는 메서드 이름 그대로 명령을 매번 처리하는 역할을 한다.

표준 패턴은 onStartCommand()에서 백그라운드 스레드를 생성하고 스레드에서 작업을 진행하는 것이다.

 서비스에서 작업을 진행 상황에 따라 액티비티에 메시지를 보내려면 일반적으로 브로드캐스트를 사용한다. 또 다른 방법으로는 Service를 시작하는 Intent에 ResultReceive를 전달하고, 서비스에 ResultReceiver에 값을 되돌려줄 수도 있다.

서비스 재시작 방식 제어법

onStartCommand() 메서드에 리턴하는 int 상수를 가지고서 재시작 방식을 제어한다.

START_NOT_STICKY

강제 종료되면 재시작하지 않는다.

START_STICKY

기본 리턴값이다. 정상적으로 종료되지 않았을 때 재 시작한다. 재시작시에는 다시 onStartCommand()를 호출하는데 이때 Intent파라미터가 null로 전달된다. START_STICKY는 전달된 Intent를 사용하지 않고 내부 상태 변수만 사용하는 서비스에 적합하다.

START_REDELIVER_INTENT

재시작하면서 Intent를 다시 전달하여 실행한다. 어떻게든 해당 파라미터를 가지고 실행해야하는 서비스가 이에 해당한다.

 Application의 onCreate()에서 startService()를 실행해도, Message Queue의 순서상 액티비티가 먼저 시작되고 서비스는 그 다음에 시작된다. 이 때 액티비티에서 크래시가 발생하면 프로세스는 죽지만 펜딩서비스를 실행하기 위해 다시 프로세스를 띄운다. 

 서비스가 예기치 않게 종료된 것으로 간주되면 재시작하면서 크래시가 반복해서 발생할 수 있다. 따라서 서비스는 다른 컴포넌트보다도 안전성이 높아야만 한다.

할 일은 다 끝났는데 서비스가 시작된 상태로 남아 불필요하게 메모리를 차지하고 있을 때, START_STICKY나 START_REDELIVER_INTENT인 경우에 의도치 않게 재시작하는 일이 생긴다.

멀티스레드 이슈

- 멤버 변수는 최소한으로 사용
  ㄴ값을 잘못 저장하면 문제가 발생할 여지가 있다.
- 여러 작업 진행 중에는 stopSelfResult() 메서드 사용
 ㄴ stopSelf() 메서드를 호출하면 진행 중인 작업에서 문제가 발생한다. 이럴 때를 대비해서 stopSelfResult(int startId) 메서드가 존재한다.

롤리팝부터는 서비스를 시작할 때 암시적 인텐트가 문제발생한다.

IntentService 클래스

서비스에서 멀티 스레딩이 필요한 경우가 많지는 않다. 동시에 여러 요청을 처리할 필요가 없다면 IntentService를 활용하자.
IntentService에서 내부적으로 구현한 onStartCommand() 메서드의 기본 리턴 값은 START_NOT_STICKY이다. 이 값을 변경하려면 생성자에서 setIntentRedelivery(true)를 호출하자. 

IntetnService에서 Toast 띄우는 문제

Toast 내부 클래스인 TN에서 Handler로 기본 생성자를 사용하는 부분이 있기 때문에 Looper가 없다고 에러를 발생시킨다. 에러메시지를 따라 백그라운드에서 Looper.prepare()를 먼저실행하면 Toast는 정상적으로 보여진다. Toast.show()를 실행하면 바인더 통신을 통해 system_server프로세스에서 NotificationManagerService의 enqueToast() 메서드를 호출한다. 이 때 파라미터로 바인더 콜백이 전달되고, 콜백에서는 두가지 작업이 진행된다. 토스트를 보여주는 것과 일정시간 이후에 제거하는 것이다. 이 사이에 stopSelf()가 호출되면 어떨까? onDestroy()에서는 HandleThread에서 생성한 Looper.quit()를 호출한다. 따라서 토스트가 떴지만 토스트가 제거되지 않는 현상이 일어난다. 따라서, Looper.getMainLooper()가 전달된 Handler에서 Toast를 띄우는 것이 더 적절하다.

서비스 중복 실행 방지

onCreate()에서 백그라운드 스레드를 생성하여 작업을 처리하는 것이 좋다. 이유는 여러 곳에서 startService()를 호출하는 경우에도 매번 스레드를 시작하지 않고, 이미 시작되었으면 나머지는 스킵(skip)하기 위해서다. onCreate()에서 작업이 끝났을 때 확실히 stopSelf()를 호출하면 문제가 없다. 다만 onStartCommand()에서 stopSelf()를 실행하는 서비스의 일반적인 형태와 차이가 있다.

public class MyService extends Service {
	private boolean isRunning = false;
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
    	if (isRunning) {
        	return START_NOT_STICKY;
        }
        isRunning = true
        // 생략
        
        return START_STICKY;
    }
    
    @Override
    public void onDestory() {
    	isRunning = false;
    }
}

 

onStartCommand()는 메인 스레드에서 동작하기 때문에 단순히 boolean 값으로 체크할 수 있다. 스킵 시에는 START_NOT_STICKY를 리턴하여 스킵된 요청에 의하여 재시작하는것을 방지하는 것이다.

다른 방법은 아래와 같다.

private ThreadPoolExecutor exec = new ThreadPoolExecutor(1, 1,
	0, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
    new ThreadPoolExecutor.DiscardPolicy());

1개의 쓰레드만 사용하게끔 고정한다음 DiscardPolicy를 전달하여 요청이 추가로 들어올 때는 해당 요청을 버리게 한다.

바운드 서비스

BIND_AUTO_CREATE 옵션

- bindService()를 실행하면 항상 바인딩될까? 그렇진 않다. 서비스가 생성되어야만 바인딩이 가능하다. bindService() 메서드에 서비스가 생성된 게 없다면 새로 생성하는 옵션이 있다. 그 옵션이 BIND_AUTO_CREATE 옵션이다. 이 옵션을 bindService의 flags 파라미터에 전달하면 된다. 이 옵션이 없다면 bindService()를  실행해도 서비스는 자동으로 생성되지 않고, 연결 콜백이 불리지 않게 된다.

- 서비스에 바인딩된 클라이언트가 여러 개 남아있을 때 stopService()를 실행하면 어떤일이 발생할까? BIND_AUTO_CREATE 옵션이 있다며 stopService()를 실행해도 서비스가 종료되지 않는다.

- 바인됭 클라이언트가 남아 있는 상태에서 서비스 프로세스는 메모리 문제로 종료될 수 있다. 이때 BIND_AUTO_CREATE 옵션이 있다면 프로세스가 살아나 재연결된다.

바운드 서비스의 용도

- API로 데이터 접근
- 콜백을 이용한 상호 작용

리모트 바인딩

리모트 바인딩 서비스는 다른 프로세스에서 접근하는 것을 전제로 만들어진다. 따라서 로컬에서만 사용하는 서비스라면 리모트 바인딩을 굳이 만들 필요가 없다.

aidl 인터페이스와 생성 클래스

interface IRemoteService {
	boolean validCalendar(long calendarId, String calendarType);
}

이렇게 하면 안드로이드 스튜디오는 build/generated/source/aidl 디렉터리에, 이클립스는 gen 디렉터리에 IRemoteService.java가 생성된다.

Service에 Stub 구현

서비스에 추상클래스인 Stub 구현체를 만든다.

private final IRemoteService.Stub binder = new IRemoteService.Stub() {
	
    @Override
    public boolean validCalender(long calendarId, String calendarType) {
    	...
    }
};

aidl에 있는 메서드인 validCalendar() 메서드가 Stub 익명 클래스에서 구현된다.

클라이언트에서 서비스 바인딩

bindService()는 바인딩 결과를 비동기로 받기 때문에, 콜백으로 사용할 ServiceConnection 인스턴스를 bindService() 메서드에 파라미터로 전달한다.

private IRemoteService mRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
	@Override
    public void onServiceConnected(ComponentName className, IBinder service) {
    	mIRemoteService = IRemoteService.Stub.asInterface(service);
    }
    
    @Override
    public void onServiceDeisconnected(ComponentName className) {
    	mIRemoteService = null;
    }
};

@Override
public void onStart() {
	super.start();
    bindService(new Intent(IRemoteService.class.getName()), mConnection, Context.BIND_AUTO_CREATE);
}

aidl에서 지원하는 데이터 타입

프로세스 간에 데이터를 주고 받을 때는 마샬링(marshaling)/ 언마샬링(unmarshaling)이 필요하기 때문에 aidl 인터페이스에 쓸 수 있는 데이터 타입이 제한된다.

aidl에서 기본적으로 지원하는 타입은 다음과 같다.

1. primitive Type
2. String
3. List: 구현체인 ArrayList는 불가, List<String>, List<List> 같은 타입은 가능하나, List<?>, List<List<String>>은 불가능하다.
4. Map: 역시 구현체인 HashMap은 불가능하고 제네릭은 지원하지 않는다.

이런 불편함 때문에 로컬 바인딩을 논한다.

로컬바인딩

로컬바인딩 서비스는 로컬 프로세스에서만 접근 가능한 서비스이다. 리모트 바인딩보다 간단하게 만들 수 있다. 파라미터로 enum을 쓸 수 있다는 장점이 있다.

로컬바인딩 서비스

Service에서 Stub을 구현할 필요가 없다.

public class LocalService extends Service {
	private final IBinder mBinder = new LocalBinder();
    
    public class LocalBinder extends Binder {
    	public LocalService getService() {
        	return LocalService.this;
        }
    }
    
    @Override
    public IBinder onBind(Intent intent) {
    	return mBinder;
    }
    
    public boolean validCalender(long calendarId, CalenderType calendarType) {
    ...
    }
 }

클라이언트에서 로컬 바인딩 접근

클라이언트에서 로컬바인딩을 사용하는 방법은 리모트 바인딩과 거의 동일하다.

리모트 바인딩과 달리 onServiceConncected() 메서드에서 얻어내는 것은 결국 LocalService 인스턴스이다. 직접적으로 인스턴스 접근이 가능하다.

public void onServiceConnected(CommponentName className, IBinder service) {
	LocalBinder binder = (LocalBinder) service;
    mService = binder.getService();
}

인터페이스를 사용한 로컬 바인딩

Service 메서드를 직접 호출할 수 없으니 바인딩을 통해서 Service 레퍼런스를 가져오고 그 다음 Service의 메서드를 직접 호출하는것에 불과하다. 먼저 앞에서 IRemoteService.aidl 파일은 그대로 IRemoteService 인터페이스를 만들고, Stub 구현과 유사하게 LocalBinder에서 IRemoteService를 구현한다.

public class LocalService extends Service {
	private final IBinder mBinder = new LocalBinder();
    
    private class LocalBinder extends Binder implements IRemoteService {
    	public LocalService getService() {
        	return LocalService.this;
        }
        
        @Override
        public boolean validCalender(long calendarId, CalenderType calendarType) {
    		...
    	}
    }
    
    @Override
    public IBinder onBind(Intent intent) {
    	return mBinder;
    }
 }

클라이언트에서는 결국 인터페이스로 캐스팅해서 사용한다.

바인딩의 특성

- 모든 클라이언트가 unbindService() 메서드를 호출해서 서비스와의 관계가 모두 정리되어야 서비스가 종료된다.

- Activity는 onStart/onStop메서드에서 bindService/unbindService 실행 권장

- 데이터를 조회하는 바인딩은 콘텐트 프로바이더로 대체 가능
  ㄴ DB 커서가 아닌 MatrixCursor를 사용 하여 Cursor에 직접 값 채우기가 가능하다

- 바운드 서비스에서 백그라운드 작업시 결과를 돌려주는 방법
   ㄴ 1. 스레드 작업이 끝났을 때 sendBroadcast()|
   ㄴ 2. 바인더 콜백을 메서드에 파라미터로 전달하여 결과를 받는다.
   ㄴ 3. bindService() Intent 파라미터에 ResultReceiver를 전달하면 Service의 onBind()에서 ResultReceiver를 가져올 수 있다. 작업 완료후 ResultReceiver의 send() 메세지로 결과를 다시 전달한다.
   ㄴ 4. Messenger를 사용하여 양방향 통신을 할 수 있다.

Messenger 클래스

Messenger는 바인더 콜백을 내부적으로 래핑해서 바운드 서비스와 클라이언트 간에 Handler로 메시지를 보내고 처리하는 방식을 제공한다.

- Parcelable 인터페이스를 구현해서 프로세스 간에 전달할 수 있는 객체
- Messenger(Handler targer)은 클라이언트와 바운드 서비스 양쪽에 있고, Messenger(IBinder target)은 Binder Proxy를 생성하는 것으로 클라이언트에 있다.
- aidl을 내부적으로 사용하는데 IMessenger 인터페이스로 되어 있다. Handler의 내부 클래스인 MessengerImpl에서 IMessenger.Stub을 구현한다. Stub에서는 Handler의 sendMessage() 메서드를 호출하기만 한다.
- Message 소스를 보면 replyTo라는 공개 변수가 있다. Handler에 Message를 보낼 때 replyTo에 값을 되돌려 줄 Messenger를 지정할 수 있다.
- Messenger의 send() 메서드는 결과적으로 바인더 통신을 통해 Stub의 메서드를 호출한다. RemoteException을 던질 수 있다.

참고 문서 : 안드로이드 프로그래밍 Next Step

반응형