[안드로이드-그 한계를 넘어서] 6장. 서비스 및 백그라운드 작업

서비스 및 백그라운드 작업

백그라운드 작업을 수행할 때 Service 컴포넌트를 사용하는 법을 다룬다. 먼저 언제 Service를 사용하는 게 좋은지 살펴보고, Service를 최적으로 설계하고 설정하는 법을 배운다. 또 Service가 실행되는 생명주기에 대해서도 자세히 들여다 본다. 이어서 Service와 다른 컴포넌트의 토잇ㄴ에 대해 자세히 설명하고, Service에서 백그라운드 스레드로 작업을 처리하는 모범 기법에 대해 설명해보쟈.

서비스의 사용 시점과 방법

서비스는 사용자와 상호작용하지 않으면서 오랜 시간이 걸리는 작업을 수행하거나 다른 애플리케이션에서 사용할 기능을 제공하기 위한 목적을 나타내는 애플리케이션 컴포넌트

서비스의 두가지 기능

  1. 오랜 시간이 걸리는 작업을 수행함
  2. 다른 애플리케이션에서 사용할 기능 제공

    필자는 사용자 인터페이스와 관련 없는 모든 작업은 백그라운드 스레드로 옮기고 Service를 통해 이 스레드를 시작하고 제어할 것을 권장.

서비스 유형 첫 번째 유형은 사용자 입력과 상관없이 애플리케이션 작업을 수행하는 서비스. 예를 들어 음악 플레이어는 전방에 음악 앱이 없더라도 음악을 재생할 수 있다.

두 번째 유형은 사용자 행동으로부터 직접 실행되는 서비스다. 예를 들어 사진 공유 애플리케이션이 이에 해당한다. 사용자가 사진을 찍으면 애플리케이션은 Intent를 사용해 사진을 Service로 보낸다. 그럼 서비스가 실행되고 인텐트에 들어있는 데이터를 파싱해 업로드하는 백그라운드 스레드를 시작한다. 작업이 완료되면 시스템에 의해 Service가 자동으로 중단된다.

서비스 생명주기의 이해

안드로이드에서 Service는 Activity와의 생명주기가 다르다.

오직 서비스 생성(onCrate())와 소멸(onDestroy())

onCrate() 메서드에서는 새 Handler 객체를 초기화하고, 시스템 Service를 조회하며, 새 BroadcastReceiver를 등록하고, 그 외 Serviceㅇ서 필요한 초기화 작업을 수행한다. 이 메서드는 메인 스레드에서 실행되므로 오랜 시간이 걸리는 작업이나 블로킹 작업은 AsyncTask, Handler등을 이용해 백그라운드 스레드로 위임해야 함을 기억합시다.

Service의 정리 작업은 onDestroy()메서드에서 수행한다. 이때는 앞에서 시작한 HandlerThread를 모두 종료하고, 서비스에서 앞서 등록한 BroadcastReceiver를 모두 등록 해제해야 한다. 이때도 여기서 수행하는 작업은 모두 메인 스레드에서 실행되므로 오랜 시간이 걸리는 정리 작업은 새 스래드에서 수행해야 한다. 예를 들어 네크워크 서비스를 닫을 경우 AsyncTask를 활용

서비스 시작

서비스를 시작하기 위한 방법으로는 두가지 방법이 있음.

  1. Context.startService()
    Intent를 받는 방식
  2. Context.bindService()
    클라이언트가 해당 매서드를 사용해 서비를 바인딩하는 방식

=> 둘 중 어느 방식을 사용하든 Service는 “시작”상태로 전환된다.

startService()사용해서 Service를 실행하면 Service의 onStartCommand()콜백이 호출됨. 이 메서드에서 Service로 보낸 Intent를 받을 수 있고, 시스템에서 서비스를 닫을 때 Service가 어떻게 반응해야 하는지 안드로이드 시스템에 알려주는 상수를 반환한다. 이 부분은 Service의 동작 원리와 관련해 가장 복잡한 부분 중 하나로 잘못 이해하기도 쉽다.

상수는, START_STICKY => 문제가 생겨 재시작하게함/ START_NOT_STICKY => 문제가 생겨도 재시작은 하지않음, 서버 업로드처럼 한 번만 작업을 수행하기 위해서 적합/ START_REDELIVER_INTENT => 재시작하는데ㅡ 원본 Intent를 항상 다시 전달

Service의 onStartCommand()에서 어떤 값을 반환하든 상관없이 Service가 어떻게 시작하는지 뿐만 아니라 어떻게 종료되는지도 항상 고려하자.

Service에서 onStartCommand()호출을 받으면 처리해야 할 파라미터가 세 가지. 첫 번째 파라미터는 Intent => onStartCommand()호출에서 반환한 값에 따라 null이 될수 있음 두 번째 파라미터는 이 호출을 나타내는 플래그로 => 0, START_FLAG_RETRY, START_FLAG_REDELIVERY 중 하나. 세 번째 파라미터는 startId로, 생명주기 동안 onStartCommand()가 여러 번 호출되고 안전하게 Service를 중단해야 할 때 종종 유용하게 활용할 수 있다.

Context.startService()사용은 비동적으로 이뤄지며 작업이 완료됐음을 Activity에 알려줄 수 있는 방법도 필요하다. 이를 알리는 방법중 하나는 BroadcastReceiver를 활용하는 것.

pic6_1

서비스 바인딩 Service를 시작하는 두번째 방법은 Context.bindService()를 사용하는 것. Service를 바인딩하면 마지막 클라이언트가 연결을 해제할 때까지 서비스가 작업을 계속 수행한다. 같은 애플리케이션 프로세스에서 Service를 바인딩하면 다른 컴포넌트에서도 Service 객체 참조에 쉽게 접근해 서비스 메서드를 호출할 수 있게 된다. 이를 로컬 바인딩이라고 부른다.

KakaoTalk_Photo_2017-02-13-09-42-43

다음 코드는 어플 내에서만 접근할 때 Service의 로컬 Binder를 구현하는 법을 보여줌.

 public class MyLocalService extends Service {

   private LocalBinder mLocalBinder - new LocalBinder();

   public IBinder onBind(Intent intent){
     return mLocalBinder;
   }

   public void doLongRunningOperation(){
     //오랜 시간이 걸리는 작업을 위해 새 스레드를 시작
   }

   public class LocalBinder extends Binder {
     public MyLocalService getService(){
       return MyLocalService.this;
     }
   }
 }

onBind()메서드에서 Service가 null만을 반환하면 다른 컴포넌트에서 이를 바인딩할 수 있다. Activity에서 서비스를 바인딩할 경우에는 다음과 같이 주로 onResume()과 onPause()메서드에서 바인딩 및 바인딩 해제를 구현할 수 있다.

 public class MainActivity extends Activity
         implements ServiceConnection, MyLocalService.Callback {
     private MyLocalService mService;

     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main_activity);
     }

     @Override
     protected void onResume() {
         super.onResume();
         Intent bindIntent = new Intent(this, MyLocalService.class);
         bindService(bindIntent, this, BIND_AUTO_CREATE);
     }

     @Override
     protected void onPause() {
         super.onPause();
         if (mService != null) {
             unbindService(this);
         }
     }

         @Override
         public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
             mService =((MyLocalService.LocalBinder) iBinder).getService();

         }

         @Override
         public void onServiceDisconnected(ComponentName componentName) {
             mService = null;
         }
     }

이 Activity예제에서 bindService()호출은 비동기적이며, onBind()에서 반환하는 실제 IBinder 인터페이스는 onServiceConnected()콜백으로 전달된다. 두 번째 콜백인 onServiceDisconnected() 메서드에서 unbindService()를 호출할 때 트리거 된다. 이와 같은 설계 방식은 Activity가 활성화되어 있거나 사라질 때 각각 서비스를 효과적으로 바인딩하고 바인딩 해제할 수 있게 해준다. Service에 대한 실제 참조를 받는 콜백은 onServiceConnected()는 UI를 업데이트하고 Service를 필요로 하는 버튼이나 다른 컴포넌트를 활성화하는데도 활용가능. 실제 Service객체에 대한 참조를 가져온 후에는 이를 다른 자바 객체처럼 처리하면 된다. 하지만 onPause()에서 Activity와 Service사이의 모든 참조를 항상 해제해야 한다는 사실을 꼭 기억하자!

죽지 않는 서비스

어플이 활성화돼 있지 않을 떄도 계속해서 전방에서 Service를 유지해야 한다면 Service.startForeground()메서드를 호출하면 된다.

     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (intent != null) { // We might receive a null intent since we start with START_NOT_STICKY
             String action = intent.getAction();


             if (ACTION_SHARE_PHOTO.equals(action)) {
                 // Build the notification to be shown
                 Notification.Builder builder = new Notification.Builder(this);
                 builder.setSmallIcon(R.drawable.ic_launcher);
                 builder.setContentTitle(getString(R.string.notification_title));
                 builder.setContentText(getString(R.string.notification_text));
                 Notification notification = builder.build();
                 // Start the service in the foreground
                 startForeground(NOTIFICATION_ID, notification);

                 // Upload the photo
                 String photoText = intent.getStringExtra(EXTRA_PHOTO_TEXT);
                 Bitmap photoBitmap = intent.getParcelableExtra(EXTRA_PHOTO_BITMAP);
                 uploadPhotoWithText(photoBitmap, photoText);
             }
         }
         return START_NOT_STICKY;
     }

먼저 배경 작업에 사용할 알림을 생성한다. 다음 고유 ID와 방금 전 생성한 알림을 사용해 startForeground()를 호출. 그럼 Service가 중단되거나 true파라미터를 사용해 stopForeGround()를 호출할 때까지 상태 바에 알림이 표시된다.

이 방식은 어플이 백그라운드에서 실행되더라도 Service를 계속해서 유지하려고 할 때 권하는 방식이지만 꼭 필요하지 않다면 사용하지 말아야 한다.

서비스 중단

서비스를 실행하고 나면 서비스는 가능한 한 오랫동안 계속해서 실행된다. 서비스의 실행 방식에 따라 시스템에서는 리소스가 부족할 경우 서비스를 닫은 후 재시작할 수 있다.

Service가 갑자기 재시작하면 사용자가 애플리케이션을 실행하지 않았더라도 예상하지 못한 결과가 일어날 수 있다. 따라서 사용자의 작업이 완료되면 Service를 적절히 중단하는 게 중요하다.

Context.bindService()를 사용해 Service를 시작했다면 마지막 클라이언트가 연결을 해제할 때 서비스가 자동으로 중단된다. 다만 예외적으로 startForeground를 호출한경우 unbindService를 해도 계속 유지가 되므로 이때는 stopForeGround를 적절히 호출해야한다

 public class MyMusicPlayer extends Service implements MediaPlayer.OnCompletionListener {
     public static final String ACTION_ADD_TO_QUEUE = "com.aptl.services.ADD_TO_QUEUE";
     private ConcurrentLinkedQueue<Uri> mTrackQueue;
     private MediaPlayer mMediaPlayer;

     public IBinder onBind(Intent intent) {
         return null;
     }

     @Override
     public void onCreate() {
         super.onCreate();
         mTrackQueue = new ConcurrentLinkedQueue<Uri>();
     }

     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         String action = intent.getAction();
         if (ACTION_ADD_TO_QUEUE.equals(action)) {
             Uri trackUri = intent.getData();
             addTrackToQueue(trackUri);
         }
         return START_NOT_STICKY;
     }

     @Override
     public void onDestroy() {
         super.onDestroy();
         if(mMediaPlayer != null) {
             mMediaPlayer.release();
             mMediaPlayer = null;
         }
     }


//   Add track to end of queue if already playing,
//   otherwise create a new MediaPlayer and start playing.

     private void addTrackToQueue(Uri trackUri) {
         if(mMediaPlayer == null) {
             try {
                 mMediaPlayer = MediaPlayer.create(this, trackUri);
                 mMediaPlayer.setOnCompletionListener(this);
                 mMediaPlayer.prepare();
                 mMediaPlayer.start();
             } catch (IOException e) {
                 stopSelf();
             }
         } else {
             mTrackQueue.offer(trackUri);
         }
     }

     // Track completed, start playing next or stop service...
     @Override
     public void onCompletion(MediaPlayer mediaPlayer) {
         mediaPlayer.reset();
         Uri nextTrackUri = mTrackQueue.poll();
         if(nextTrackUri != null) {
             try {
                 mMediaPlayer.setDataSource(this, nextTrackUri);
                 mMediaPlayer.prepare();
                 mMediaPlayer.start();
             } catch (IOException e) {
                 stopSelf();
             }
         } else {
             stopSelf();
         }
     }
 }

이 예제에서는 Service에서 불필요한 리소스를 더는 사용하지 않게끔 Service.stopSelf()를 사용해 서비스를 중단하는 법을 잘 보여준다.

백그라운드 실행

Service는 애플이 전방에 있지 않을 때도 실행될 수 있다. 하지만 그렇다고해서 서비스가 메인 스레드에서 아무 작업도 수행하지 않는다는 말은 아니다. 컴포넌트의 생명주기 콜백은 어플의 메인 스레드에서 실행되므로 Service에서 오랜 시간이 걸리는 작업은 새 스레드로 옮겨야함.

메인스레드에서 작업을 실행하는 두 가지 추가 방법

IntentService Handler와 Service를 함께 사용하는 방식이 효과적임을 감안해 구글에서는 Service내에 Handler의 백그라운드 스레드 처리 로직을 감싼 IntentService라는 유틸클래스를 구현함. 이 클래스를 사용하려면 클래스를 확장하고 onHandleIntent()메서드만 구현해 다음과 같이 Service에서 해야 할 행동을 추가하면 된다.

    public class MyIntentService extends IntentService {
        private static final String NAME = "MyIntentService";
        public static final String ACTION_UPLOAD_PHOTO = "com.aptl.services.UPLOAD_PHOTO";
        public static final String EXTRA_PHOTO = "bitmapPhoto";
        public static final String ACTION_SEND_MESSAGE = "com.aptl.services.SEND_MESSAGE";
        public static final String EXTRA_MESSAGE = "messageText";
        public static final String EXTRA_RECIPIENT = "messageRecipient";

        public static final String BROADCAST_UPLOAD_COMPLETED = "com.aptl.services.UPLOAD_COMPLETED";

        public MyIntentService() {
            super(NAME);
            // We don't want intents redelivered in case we're shut down unexpectedly
            setIntentRedelivery(false);
        }

        /**
         * 이 메서드는 한 번에 한 인텐트씩 자체 스레드에서 실행.
         */
        @Override
        protected void onHandleIntent(Intent intent) {
            String action = intent.getAction();

            if(ACTION_SEND_MESSAGE.equals(action)) {
                String messageText = intent.getStringExtra(EXTRA_MESSAGE);
                String messageRecipient = intent.getStringExtra(EXTRA_RECIPIENT);
                sendMessage(messageRecipient, messageText);
            } else if(ACTION_UPLOAD_PHOTO.equals(action)) {
                Bitmap photo = intent.getParcelableExtra(EXTRA_PHOTO);
                uploadPhoto(photo);
            }
        }

        private void sendMessage(String messageRecipient, String messageText) {
            // TODO Make network call...
            //작업이 완료됐음을 브로드캐스트로 알림
        }

    private void uploadPhoto(Bitmap photo) {
        // TODO Make network call...
        ////작업이 완료됐음을 브로드캐스트로 알림
        sendBroadcast(new Intent(BROADCAST_UPLOAD_COMPLETED));
    }
  }

이 예제에서는 IntentService클래스를 확장해 두 가지 액션을 처리한다. 이 중 하나는 사진 업로드이고, 다른 하나는 메시지 전송이다. 각 액션은 매니페스트에서 Service를 선언할 때 인턴트 필터에도 추가해야 한다. 액션을 트리거하려면 특정 액션과 extras를 담고 있는 Intent를 사용해 Context.startService()를 호출하면 된다. Context.startService를 여러 번 호출하면 내부 핸들러에 의해 큐에 호출이 쌓이게 되고, 이 클래스는 한 번에 한 Intent만 처리한다. IntentService를 기반으로 한 Service는 처리할 추가 작업이 큐에 더 이상 남아있지 않을 때까지 실행 상태를 유지.

병렬 실행

방근 전 살펴본 특수 Service클래스는 백그라운드 작업을 시작하고 시작 시점이 크게 중요하지 않은 대부분의 상황에서 매우 유용하게 활용할 수 있다. IntentService로 다섯 개의 Intent를 보내면 이들 인텐트는 한 번에 하나씩 순서대로 실행된다. 대개는 이런 방식이 적합하지만 때로는 이 방식으로 인해 문제가 발생. 개별 백그라운드 작업을 가능한 한 빨리 실행하고 싶다면 병렬 실행 방식이 필요. IntentService는 단일 스레드만을 갖고 있는 Handler를 중심으로 개발됐으므로 이런 상황을 처리하려면 다른 스레드 메커니즘을 사용해야 한다. 다음 예제는 미디어를 새 형식으로 변환하는 Service의 스텁코드(예를 들면 WAV 를 MP3로)

여기서는 실제 변환 부분은 생략하고 병렬 실행을 설정하는 ExecutorSerivce API 의 사용법에 집중하자. 애플리케이션이 전방에 있지 않을 때도 Service가 살아 있게 하려면 Service에서 Service.startForeground를 호출하면 된다. Service.startForeground / Service.stopForeGround()호출은 스택에 쌓이지 않으므로 남아 있는 작업 개수를 추적하는 내부 카운터를 보관하고 카운터 수치가 다시 0이 될 때 Service.stopForeGround를 호출해야 한다.

    public class MediaTranscoder extends Service {
        private static final int NOTIFICATION_ID = 1001;
        public static final String ACTION_TRANSCODE_MEDIA = "com.aptl.services.TRANSCODE_MEDIA";
        public static final String EXTRA_OUTPUT_TYPE = "outputType";
        private ExecutorService mExecutorService;
        private int mRunningJobs = 0;
        private final Object mLock = new Object();
        private boolean mIsForeground = false;

        public IBinder onBind(Intent intent) {
            return null;
        }

        @Override
        public void onCreate() {
            super.onCreate();
            mExecutorService = Executors.newCachedThreadPool();
        }

        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            String action = intent.getAction();
            if(ACTION_TRANSCODE_MEDIA.equals(action)) {
                String outputType = intent.getStringExtra(EXTRA_OUTPUT_TYPE);

                // Start new job and increase the running job counter
                synchronized (mLock) {
                    TranscodeRunnable transcodeRunnable = new TranscodeRunnable(intent.getData(), outputType);
                    mExecutorService.execute(transcodeRunnable);
                    mRunningJobs++;
                    startForegroundIfNeeded();
                }
            }
            return START_NOT_STICKY;
        }

        @Override
        public void onDestroy() {
            super.onDestroy();
            mExecutorService.shutdownNow();
            synchronized (mLock) {
                mRunningJobs = 0;
                stopForegroundIfAllDone();
            }
        }

        public void startForegroundIfNeeded() {
            if(!mIsForeground) {
                Notification notification = buildNotification();
                startForeground(NOTIFICATION_ID, notification);
            }
        }

        private Notification buildNotification() {
            Notification notification = null;
            // TODO Build the notification here...
            return notification;
        }

        private void stopForegroundIfAllDone() {
            if(mRunningJobs == 0) {
                stopForeground(true);
                mIsForeground = false;
            }
        }

        private class TranscodeRunnable implements Runnable {
            private Uri mInData;
            private String mOutputType;

            private TranscodeRunnable(Uri inData, String outputType) {
                mInData = inData;
                mOutputType = outputType;
            }

            @Override
            public void run() {
                // TODO Perform transcoding here...

                // Decrease counter when we're done...
                synchronized (mLock) {
                    mRunningJobs--;
                    stopForegroundIfAllDone();
                }
            }
        }
    }

스레드를 관리하는 코드는 대부분 ExecutorService API에서 수행하므로 병렬 실행을 구현할 때는 이 방식이 좋음

서비스와의 통신

언제 Service를 사용하고 서비스가 어떻게 실행되는지 알았다면 다른 컴포넌트에서 서비스와 통신하는 법을 배워야 한다. Service와 통신하는 방법은 두 가지. Context.bindService() / Context.startService()

Context.startService()는 Intent를 Service.onStartCommand()메서드로 전달한다. 이 메서드에서는 백그라운드 작업을 실행하거나 브로드캐스트 및 다른 수단을 통해 호출 컴포넌트로 나중에 결과를 전달할 수 있다. Context.bindService()는 Service 객체로 직접 동기적 메서드 호출을 보내는 데 사용할 수 있는 Binder 객체를 가져오게 해준다.

Intent를 활용한 비동기적 메시징 앞서 본 IntentService예제는 컴포넌트와 Service사이의 간편한 단방향 통신 방식을 잘 보여준다. 하지만 보통은 작업 결과를 알아야 하는 경우가 많으므로 Service에서도 작업을 마친후 이를 다시 알려줄 수 있는 방법이 필요하다. 이떄는 여러 가지 방법을 활용할 수 있지만, IntentService의 비동기적 성격을 그대로 유지하고 싶다면 브로드캐스트를 전송하는 방식이 가장 좋을 것이다. 브로드캐스트는 IntentService에서 작업을 실행하는 것처럼 간단하게 전송할 수 있다. 이때는 응답을 리스닝하는 BroadcastReceiver만 구현하면 된다.

pic6_3

sendBroadcast(new Intent(BROADCAST_UPLOAD_COMPLETED));

이 방식을 사용하면 안드로이드에서 기본으로 제공하는 것을 모두 사용하므로 컴포넌트 간 메시지 통신을 처리하는 복잡한 시스템을 별도로 개발하지 않아도 된다는 장점. 여기서는 비동기적 메시지를 나타내는 액션만 선어하고, 이를 적절히 각 컴포넌트에 등록하면 된다. 이 방식은 Service가 다른 애플리케이션에 있거나 별도 프로세스에서 실행될 때도 제대로 동작한다.

단점은 전달할 수 있는 응답이 Intent로 제한된다는 점. 또 이 방식은 시스템에 무리를 주므로 진행 상태처럼 IntentService와 Activity사이에서 여러 개의 빠른 업데이트를 전달하는 데에는 적합하지 않다. 이런 경우에는 서비스 바인딩을 사용해야 하는 것을 고려.

좀더 자세한건 나중에 더욱더.

로컬 서비스 바인딩 앞서 살펴본 예제를 조금더 적극적으로 본다면 로컬 바인더 패턴을 활용해 같은 애플리케이션 내에서 Service를 바인딩하는 법을 봤다. 이 기법은 Service에서 제공하는 인터페이스가 Intent메시징만으로 전달하기에는 지나치게 복잡하고 일반 자바 메서드를 통해 구현해야 하는 경우에 효과적

로컬 Service바인딩을 사용하는 또 다른 이유는 Service에서 Activity로 복잡한 콜백을 전달하기 위해서. 오랜 시간이 걸리는 작업은 여전히 Service에서 백그라운드 스레드로 옮겨야 하므로 Service에 대한 대부분의 호출은 비동기적으로 설계한다. 실제 서비스 호출은 백그라운드 작업을 트리거하고 바로 종료. 그럼 서비스내에서 작업이 완료된 후 Service는 콜백 인터페이스를 사용해 Activity에 결과를 알려준다.

이전 예제에서 좀더 추가하여, 콜백인터페이스를 추가하고 오랜 시간이 걸리는 가상의 백그라운드 작업을 수행하는 AsyncTask를 구현했다. Service에서는 onBind()메서드에서 LocalBinder객체를 반환하는데, 이를 통해 클라이언트는 Service 객체 참조를 가져올 수 있고, doLongRunningOperation()을 호출.

이 메서드는 새 AsyncTask를 생성하고 클라이언트에서 전달한 파라미터를 사용해 이를 실행한다. 작업을 실행하는 동안 클라이언트에 진행 상황을 알리고 최종 결과를 알리기 위해 콜백인스턴스를 호출.

     public class MyLocalService extends Service {
         private static final int NOTIFICATION_ID = 1001;
         private LocalBinder mLocalBinder = new LocalBinder();
         private Callback mCallback;

         public IBinder onBind(Intent intent) {
             return mLocalBinder;
         }

         public void performLongRunningOperation(MyComplexDataObject dataObject) {
             new MyAsyncTask().execute(dataObject);
         }

         public void setCallback(Callback callback) {
             mCallback = callback;
         }

         public class LocalBinder extends Binder {
             public MyLocalService getService() {
                 return MyLocalService.this;
             }
         }

         public interface Callback {
             void onOperationProgress(int progress);
             void onOperationCompleted(MyComplexResult complexResult);
         }

         private final class MyAsyncTask extends AsyncTask<MyComplexDataObject, Integer, MyComplexResult> {

             @Override
             protected void onPreExecute() {
                 super.onPreExecute();
                 startForeground(NOTIFICATION_ID, buildNotification());
             }

             @Override
             protected void onProgressUpdate(Integer... values) {
                 if(mCallback != null && values.length > 0) {
                     for (Integer value : values) {
                         mCallback.onOperationProgress(value);
                     }
                 }
             }

             @Override
             protected MyComplexResult doInBackground(MyComplexDataObject... myComplexDataObjects) {
                 MyComplexResult complexResult = new MyComplexResult();
                 // Actual operation left out for brevity...
                 return complexResult;
             }

             @Override
             protected void onPostExecute(MyComplexResult myComplexResult) {
                 if(mCallback != null ) {
                     mCallback.onOperationCompleted(myComplexResult);
                 }
                 stopForeground(true);
             }

             @Override
             protected void onCancelled(MyComplexResult complexResult) {
                 super.onCancelled(complexResult);
                 stopForeground(true);
             }
         }

         private Notification buildNotification() {
             Notification notification = null;
             // Create a notification for the service..
             return notification;
         }
     }

다음 코드는 새로 수정한 Activity코드를 볼 수 있다. 가장 두드러진 차이점은 MyLocalService.Callback인터페이스를 구현한 점. onServiceConnected()에서 service에 대한 참조를 가져오면 오랜 시간이 걸리는 작업이 실행되는 동안 알림을 받을수 있게 service객체에 콜백을 설정. 이때 사용자가 Activity를 벗어나고 onPause()가 호출되면 콜백을 제거하는게 매우 중요하다. 이렇게 하지 않으면 누수가 발생.

    public class MainActivity extends Activity
            implements ServiceConnection, MyLocalService.Callback {
        private MyLocalService mService;

         // Called when the activity is first created.
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main_activity);
        }

        @Override
        protected void onResume() {
            super.onResume();
            Intent bindIntent = new Intent(this, MyLocalService.class);
            bindService(bindIntent, this, BIND_AUTO_CREATE);
        }

        @Override
        protected void onPause() {
            super.onPause();
            if (mService != null) {
                mService.setCallback(null); // Important to avoid memory leaks
                unbindService(this);
            }
        }

        public void onTriggerLongRunningOperation(View view) {
            if(mService != null) {
                mService.performLongRunningOperation(new MyComplexDataObject());
            }
        }

        @Override
        public void onOperationProgress(int progress) {
            // TODO Update user interface with progress..
        }

        @Override
        public void onOperationCompleted(MyComplexResult complexResult) {
            // TODO Show result to user...
        }

        @Override
        public void onServiceConnected(ComponentName componentName,
                                       IBinder iBinder) {
            mService = ((MyLocalService.LocalBinder) iBinder).getService();
            mService.setCallback(this);

            // Once we have a reference to the service, we can update the UI and
            // enable buttons that should otherwise be disabled.
            findViewById(R.id.trigger_operation_button).setEnabled(true);
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            // Disable the button as we are loosing the reference to the service.
            findViewById(R.id.trigger_operation_button).setEnabled(false);
            mService = null;
        }
    }

onServiceConnected() 및 onServiceDisconnected()메서드에서는 service에 의존하는 사용자 인터페이스 영역을 업데이트할 수 있다. 이 경우 오랜 시간이 걸리는 작업을 실행하는 버튼을 활성화또는 비활성화 할수 있다.

작업이 완료되기 전에 사용자가 Activity를 벗어나면 앞서 startForeground를 호출했으므로 service는 계속 실행, 이전에 시작한 작업을 완료되기 전에 Activity실행을 재게하면 Activity는 service에 바인딩이 성공하자마자 진행 상태에 대한 콜백을 받게된다. 이와 같은 방식을 활용하면 오랜 시간이 걸리는 작업을 사용자 인터페이스로부터 손쉽게 분리할 수 있음은 물론 Activity가 실행을 재개하면 언제든 현재 상태를 가져올수 있다.

service에서 내부 상태를 관리한다면 Activity가 실행을 재개하고 service에 바인딩할 때 상태가 바뀌었을 수도 있으므로 클라이언트에서 현재 상태를 조회하고 상태변경사항(콜백등을 활용)을 구독하는게 좋다.

정리

추가자료