개인적으로 Service 는 사용하는 일은  관리가 어렵고 생각 못한 문제가 많이 발생해서 서 늘 고생하고 있는 부분이였습니다만, 구글 쪽에서도 관련되어 여러가지 고민이 많았는지, 2.0 이후 부터는 Service API 에 많은 변화가 생겼네요. 구글 Android Developer 블로그에 Android 2.0 에서사 변경된 Service API 에 관한 글이 있어서 번역해 보았습니다. 원래 번역글을 블로그에 올려두고 있었습니다만, 안드로이드 펍 분들에게도 혹시 도움이 되지 않을까 해서 올려봅니다.

원문 : http://android-developers.blogspot.com/2010/02/service-api-changes-starting-with.htm


Service API changes starting with Android 2.0
  • Service.setForeground() 가 더 이상 작동하지 않음. (Deprecated)
  • 상황에 따라 더 이상 필요하지 않은 Service 가 죽지 않는 경우가 많았는데, 이런한 에외사항을 보다 잘 처리할 수 있도록 새로운 API 를 제공함.
  • 사용자들이 현재 시스템에서 작동 중인 Service 를 확인하고 관리할 수 있는 UI 를 제공.
Background on services

  서비스 API 는 어플리케이션이 백그라운드에서 특정 작업을 수행할 수 있게 해주는.안드로이드의 핵심 매커니즘의 하나이다. 안드로이드 플랫폼 특성상, 화면상에 표시되지 않은 개별 어플리케이션은 시스템이 종료시키고, 필요한 경우 다시 시작할 수 있다고 여겨진다. 따라서 특정 어플리이션이 백그라운드에서도 특정 일을 수행할 필요가 있을 경우, Service 를 시작해야 하며, 시스템은 Service 를 구동중인 어플리케이션은 강제로 종료시키지 않는다. (최대한...)

   Service 는 매우 유용한 수단이지만, 언제나 권한에는 책임이 뒤 따른다. (power comes some responsibility). Service 를 가동하고 있는 어플리케이션은 시스템 자원을 소모기 때문에 전반적인 시스템 성능에 문제를 일으킬 수 있다. 따라서 개발자들은 정말로 필요한 경우에만 Service 를 사용해야 하며, 필요없는 Service 가 백그라운드에 남아있지 않도록 주의를 기울여야 한다. (서비스 릭 버그를 내지 말아야 한다...)

Redesigning Service.setForeground()

 Androi 1.6 안정화 기간동에, 너문 많은 어플리케이션이 Service.setForeground() API 를 사용하기 때문에 발생하는 문제들이 발견되었다.  이 API가 호출 되면,  시스템은 특정 Service가 구동중인 프로세스가 포그라운드 프로세스로 간주한다. (우선 순위 부여)  따라서, 시스템이 메모리 부족등의 예외 상황을 처리하게 매우 어렵게 만든다. 

  이러한 문제점이 발견된 시점이 너무 늦었기에, 1.6 버전 SDK는 이 API에 대하여 큰 변화 없이 공개되었지만, 2.0 에서는 중요한 변경사항이 이루어졌다. Service.setForeground() 는 이제 작동하지 않는다. 따라서 기존의 API 를 대체해서 2.0 버전에서는 두가지  새로운 API 가 추가되었다. 이제 Service 가 포그라운드로 진입하고자 할  경우, 사용자가 해당 사실을 확인하고 필요한 경우, 포그라운드에서 작동중인 서비스를 정지 시킬 수 있도록, Notification 을 보내야 한다. (주> 음악 재생 서비스 와 같이, 상태바에 특정 메세지를 표시하도록...)
 
public final void startForeground(int id, Notification notification);

public final void stopForeground(boolean removeNotification);
 
 시스템은 Service 가 포그라운드에서 작동 중이라면, 어떠한 형태든 Notification 이 남아 있음을 확신 할 수 있고 따라서, Service 와 연관된 Notification 상태를 쉽게 관리 할 수 있다.  당연히, 많은 개발자들이 2.0 뿐만 아니라 이전 플랫폼에서도 작동하는 Service 코드를 작성하고 싶어한다. 아래와 같이 가능한 경우에 한해서 새롭게 추가된 API 를 호출하는 방법으로 예전 버전과 2.0 이후 버전에서 모두 작동하는 코드를 작성 할 수 있다.

private static final Class[] mStartForegroundSignature = new Class[] {
int.class, Notification.class};
private static final Class[] mStopForegroundSignature = new Class[] {
boolean.class};

private NotificationManager mNM;
private Method mStartForeground;
private Method mStopForeground;
private Object[] mStartForegroundArgs = new Object[2];
private Object[] mStopForegroundArgs = new Object[1];

@Override
public void onCreate() {
mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
try {
mStartForeground = getClass().getMethod("startForeground",
mStartForegroundSignature);
mStopForeground = getClass().getMethod("stopForeground",
mStopForegroundSignature);
} catch (NoSuchMethodException e) {
// Running on an older platform.
mStartForeground = mStopForeground = null;
}
}

/**
* This is a wrapper around the new startForeground method, using the older
* APIs if it is not available.
*/
void startForegroundCompat(int id, Notification notification) {
// If we have the new startForeground API, then use it.
if (mStartForeground != null) {
mStartForegroundArgs[0] = Integer.valueOf(id);
mStartForegroundArgs[1] = notification;
try {
mStartForeground.invoke(this, mStartForegroundArgs);
} catch (InvocationTargetException e) {
// Should not happen.
Log.w("MyApp", "Unable to invoke startForeground", e);
} catch (IllegalAccessException e) {
// Should not happen.
Log.w("MyApp", "Unable to invoke startForeground", e);
}
return;
}

// Fall back on the old API.
setForeground(true);
mNM.notify(id, notification);
}

/**
* This is a wrapper around the new stopForeground method, using the older
* APIs if it is not available.
*/
void stopForegroundCompat(int id) {
// If we have the new stopForeground API, then use it.
if (mStopForeground != null) {
mStopForegroundArgs[0] = Boolean.TRUE;
try {
mStopForeground.invoke(this, mStopForegroundArgs);
} catch (InvocationTargetException e) {
// Should not happen.
Log.w("MyApp", "Unable to invoke stopForeground", e);
} catch (IllegalAccessException e) {
// Should not happen.
Log.w("MyApp", "Unable to invoke stopForeground", e);
}
return;
}

// Fall back on the old API. Note to cancel BEFORE changing the
// foreground state, since we could be killed at that point.
mNM.cancel(id);
setForeground(false);
}

Service lifecycle changes

 Service 들이 필요하지 않은 경우에도 불구하고, 자신을 포그라운드 프로세스로 설정하는 문제를 제외하고도,  백그라운드에서 너무 많은 수의 Service 들이 돌아감에 따라, 각각의 Service 들이 한정된 메모리 자원을 차지하게 위해 서로 경쟁하게 되면 또 다른 문제가 발생할 수 있다. 안드로이드 플랫폼과 Service 간의 상호 작용 과정에서, 특정 어플리케이션이 코드상으로는 별다른 문제 없이 작성된 경우에도, Service 가 정상 종료되지 않고 백그라운드에 남아있을 수 있다. 예를 들어 다음과 같은 상황이다.
  1. Application 이 StartService 를 호출 한다.

  2. Service 의 onCreate, onStart() 함수가 호출 되고, 특정한 작업을 수행하기 위해 백그라운드 스레드가 생성된다.

  3. 시스템이 메모리 부족으로 인해 현재 작동중인 서비스를 강제로 종료 한다.

  4. 잠시 후에, 메모리 상황이 호전되어 Service 가 재시작한다. 이 때, Service 의 onCreate 함수는 호출 되지만, onStart 는 호출 되지 않는다. (startService 는 호출 되지 않았음으로..)

 
 결국, Service 는 생성된 채로 남아있게 된다. Service 는 어떠한 일을 수행 하지도 않으며 (무슨 일을 수행해야할 지 알 수 없음으로...), 어떤 시점에 종료되어야 하는지도 알 수 없다.

 이러한 문제를 해결하기 위해 안드로이드 2.0 에서 Service 의 onStart() 는 더이상 사용되지 않는다. (이전 버전과의 호환성 문제때문에 작동 자체는 하지만...) 대신 Service.onStartCommand() 콜백이 사용된다. 이 콜백 함수는 시스템이 Service 를 좀 더 잘 관리 할 수 있도록 해주는데, 기존 API 와는 달리  onStartCommand() 는 결과값을 반환한다. 안드로이드 플랫폼은 이 결과값을 기반으로 만일 작동중인 서비스가 강제로 종료될 경우, 어떠한 일을 수행해야 하는지 판단한다.
  • START_STICKY  는 기본적으로 이전과 동일하게 작동한다. 이전과의 차이점은 다음과 같다.  기존에 프로세스가 강제 종료된 후 Service 가 재 시작 될 때, onStart() 콜백이 호출 되지 않았지만, START_STICKY 형태로 호출된 Service 는 null Intent 가 담긴 onStartCommand() 콜백 함수가 호출된다. 이 모드를 사용하는 Service 는 null Intent 로 onStartCommand() 콜백 함수가 호출되는 경우를 주의깊게 처리해야 한다. 
  • START_NOT_STICKY 모드로 시작된 Service는  안드로이드 플랫폼에 의해 프로세스가 강제로 종료되는 경우, 다시 시작되지 않고 종료된 상태로 남게된다. 이러한 방식은 특정 Intent 로 주어진 명령을 수행하는 동안에만 Service 가 실행되면 되는 경우에 적당하다 예를 들어, 매15분마다 네트워크 상태를 체크하기 위해 실행되는 Service 를 생각해 보면, 만일 이 Service 가 작업도중 강제로 종료될 경우, 이 Service 는 재시작하기 보다는 정지된 상태로 남겨두고, 15분 후에 새롭게 시작되도록 하는 것이 최선이다. 
  • START_REDELIVER_INTENT  는 START_NOT_STICKY 와 유사하다. 단, 프로세스가 강제로 종료되는 경우 (stopSelf() 가 호출 되기 전에 종료되는 경우), Intent 가 다시 전달 되어 Service 가 재시작 한다.  (단, 여러차례 시도한 후에도, 작업이 종료되지 않으면, Service 는 재시작되지 않는다.) 이 모드는 전달받은 명령을 반드시 수행해야 하는 Service 에 유용하다. 
 기존에 존재하는 어플리케이션과의 호환성을 위해서, 이전 버전을 기준으로 제작된 어플리케이션은  START_STICKY_COMPATIBILITY 모드로 작동한다.  (null intent 를 보내지 않고 기존과 동일하게 작동). 단 API 버전 5  이상을 기준으로 작성된 어플리케이션들은 기본적으로 START_STICKY 모드로 작동 하며, 개발자는 반드시 onStart() 나 onStartCommand() 가 null intent 를 가지고 호출되는 경우를 염두해 두어야 한다. 

 개발자들은 아래에 제시된 코드를 활용하여, 손쉽게 이전 버전과 새로운 버전 양쪽 모두에서 작동하는 Service 를 구현할 수 있다. 

    // This is the old onStart method that will be called on the pre-2.0
    // platform.  On 2.0 or later we override onStartCommand() so this
    // method will not be called.
    @Override
    public void onStart(Intent intent, int startId) {
        handleStart(intent, startId);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        handleStart(intent, startId);
        return START_NOT_STICKY;
    }

    void handleStart(Intent intent, int startId) {
        // do work


New "running services" user interface

 * 이 후 이어지는 내용은 새롭게 추가된 Running Service 를 확인 할 수 있는 UI 어플리케이션에 관한 내용이라 대충 대충 건너 띄도록 하겠습니다.

 안드로이드 2.0 부터는 새롭게 "Running Services" Activity 가 어플리케이션 세팅에 추가 되었다. 사용자는 목록에 표시된 Service 를 터치해서 종료할 수 있다.

 화면 하단부에는 현재 시스템의 메모리 상황에 대한 정보가 표시된다. 

  • Avail: 38MB+114MB in 25 은 현재 38MB 의 공간이 바로 사용가능하며, 114MB 의 메모리 공간은 언제든지 종료시킬 수 있는 25개의 백그라운드 프로세스에 의해 점유되고 있음을 알려준다. 
  • Other: 32MB in 3 은 현재 3개의 임의로 종료되서는 않되는 포그라운드 프로세스에 의해 32MB 의 공간이 사용되고 있음을 알려준다. 
 대부분의 사용자들에게 이 새로운 UI 는 기존의 Task Killer 어플리케이션에 비해 사용하기 편리하며, 강제로 Service 를 종료시키는 것이 아니기 때문에 문제를 일으킬 소지가 훨씬 적다. 

 개발자들에게는 자신이 작성한 어플리케이션이 잘 작동하는지 (특히 Service 관리라는 측면에서) 확인 할 수 있게 해주는 유용한 툴이 될 수있다. 또한 어플리케이션이 구동한 Service 가 사용자에 의해 어느시점에서든지 종료 할 수 있다는 점을 염두해 두어야 한다. 안드로이드의 Service 는 매우 강력한 도구이지만, 어플리케이션 개발자들이 전체 폰의 성능에 큰 악영향을 끼칠 수 있는 문제를 가장 많이 일으키는 도구이기도 한다.