Android Service의 이해

2019. 9. 18. 21:03

Android Service의 이해

 

서비스(Service)란?

 

  • 어플리케이션을 구성하는 4대 컴포넌트 중에 하나.
  • 액티비티와 다르게 UI를 제공하지 않고 백그라운에서 실행되는 컴포넌트. 하지만 Main Thread(UI Thread)에서 동작한다. 
  • 서비스의 시작과 종료는 다른서비스, Activity, BroadCast Receiver를 포함한 다른 Application에서 가능.
  • 만약 서비스가 실행되고 있는 상태라면, 안드로이드 OS 에선 왠만하면(?) 프로세스를 죽이지 않고 관리한다.
     

 

서비스를 왜 사용하는가?

 

  • Activity가 OnPause되거나, 화면에서 없어지는 경우(OnStop)에도 지속적인 동작이 필요한 경우가 있다. 예를 들면, 화면 꺼졌을 때에도 음악을 플레이 하거나 메신저에서 파일 다운로드가 가능하도록 하는 것을 의미한다.
  • Worker Thread를 사용하면 되지 않는가?
public class LifeCycleApplication extends Application {
	private static final long SLEEP = 10000L;

	@Override
	public void onCreate() {
		super.onCreate();
		
		Thread thread = new Thread(new Runnable() {
			Log.d("THREAD_TEST", "start");

			SystemClock.sleep(SLEEP);
			Log.d("THREAD_TEST", "10 secs");

			SystemClock.sleep(SLEEP);
			Log.d("THREAD_TEST", "20 secs");

			SystemClock.sleep(SLEEP);
			Log.d("THREAD_TEST", "30 secs");
		}).start();
	}
}

 

  • 메모리가 부족할 경우에 LMK(Low Memory Killer)는 우선순위가 높지 않은 프로세스를 종료하는데, LMK가 Thread 실행 도중에 앱 프로세스를 종료할 수 있기 때문에 안정성을 보장할 수 없다.
  • 사용자가 직접 최근 앱 목록에서 앱을 제거할 수 있다. 이 때도 프로세스가 종료되면 Thread가 종료된다.

 

프로세스 우선순위

 

  • 프로세스가 LMK에 의해 강제 종료될 가능성은 아래의 가이드에서 확인할 수 있다.
       https://developer.android.com/guide/components/processes-and-threads
  1. Foreground Process: 안드로이드 컴포넌트가 포그라운드에서 실행되는 프로세스이다. 메모리가 부족할 때에도 가장 마지막까지 남을 수 있는 프로세스
  2. Visible Process: 포그라운드 컴포넌트를 가지고 있지는 않지만, 사용자가 보는 화면에 아직 영향이 있는 프로세스. OnPause 상태(Dialog 혹은 투명 액티비티)일 때 실행 중인 프로세스
  3. Service Process: startService()로 실행했지만, 위의 카테고리에는 들어가지 않는 서비스가 실행 중인 프로세스. 사용자가 지금 보고 있는 것과 직접적인 연관이 없다.
  4. Background Process: 액티비티가 종료되는 것은 아니지만, 사용자가 더 이상 보이지 않고 활성화된 컴포넌트가 없는 프로세스. 예를 들면 홈키를 누르면 onStop 상태가 불리고, 태스크가 백그라운드로 이동한다. 보통 백그라운드 프로세스가 여러 개가 존재한다.
  5. Empty Process: 사용자가 백 키로 액티비티를 모두 종료하고 활성화된 컴포넌트가 없다면 빈 프로세스가 된다. 이런 프로세스를 메모리에 한동안 유지하는 이유는 다음에 컴포넌트를 다시 띄울 때 빠르게 띄울 수 있도록 캐시(Warm Start와 같이)로 사용하기 위함이다. 우선순위가 낮아서 리소스가 부족하면 가장 먼저 강제 종료 대상이 된다.
public class LifeCycleApplication extends Application {

	@Override
	public void onCreate() {
		super.onCreate();
		
		startService(new Intent(this, SleepService.class));
		Log.d("THREAD_TEST", "Serivce has been started");
	}
}


public class SleepService extends Service {
	private static final long SLEEP = 10000L;

	@Override
	public void onCreate() {
		super.onCreate();
		
		Thread thread = new Thread(new Runnable() {
			Log.d("THREAD_TEST", "start");

			SystemClock.sleep(SLEEP);
			Log.d("THREAD_TEST", "10 secs");

			SystemClock.sleep(SLEEP);
			Log.d("THREAD_TEST", "20 secs");

			SystemClock.sleep(SLEEP);
			Log.d("THREAD_TEST", "30 secs");
		}).start();
	}
	
	...
}

 

 

서비스 구현 방법은?

 

단일 인스턴스 실행

 

  • 서비스는 앱에서 1개의 인스터스만 생성된다. 싱글톤 객체를 만들지 않아도 된다.
  • onCreate() 이후에 다시 서비스가 호출된다면, onStartCommand()부터 실행된다.

 

서비스 시작 방법

 

  • Context에는 서비스를 시작하는 방법으로 startService()와 bindService() 메서드 2가지가 있다.
  • Unbound Service = Started Service. 바인딩되었다가 해제된 것이 아님.
  • 예를 들어, 음악 재생 화면이 있을 때 화면을 종료해도 음악을 들을 수 있으면 스타티드 서비스를 이용한다. 그런데 다시 화면에 진입할 때 재생 중인 음악 정보를 화면에 보여줘야 한다면 바운드 서비스이기도 해야한다. 

 

서비스 라이프 사이클

 

 

 

 

스타티드 서비스

 

  • 표준 패턴은 onStartCommand()에서 백그라운드 스레드에서 작업을 진행하는 것이다.
  • Main Thread를 점유하고 있다는 점을 유의
  • 브로드캐스트로 컴포넌트 간 통신: 액티비티에 메시지를 보내기 위해서는 일반적으로 Boardcast를 사용.
  • ResultReceiver로 단방향 메세지 전달
// Activity
public void onEvent() {
	...
	Intent = new Intent(this, ExampleService.class);
	intent.putExtra(Constant.EXTRA_RECEIVER, resultReceiver);
	startService(intent);
}

private Handler handler = new Handler();

private ResultReceiver resultReceiver = new ResultReceiver(handler) {
	@Override
	protected void onReceiveResultResult(int resultCode, Bundle resultData) {
		if(resultCode == Constant.ANY_COMMAND_DONE) {
			progressBar.setVisibility(View.GONE);
			textView.setText(R.string.done);
		}
	}
}

// Service
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
	new Thread(new Runnable() {
		@Override
		public void run() {
			Log.e("SERVICE_TEST", "Service started");
			...
			final ResultReceiver resultReceiver = intent.getParcelableExtra(Constant.EXTRA_RECEIVER);
			resultReceiver.send(Constant.ANY_COMMAND_DONE, null);
			stopSelf();
		}
	}).start();

	return START_NOT_STICKY;
}


 

스타티드 서비스 재시작 방식

 

  • 강제 종료 후 스타티드 서비스는 가능한 한 빨리 시스템에서 서비스를 재시작한다.
  • onStartCommand() 메소드의 리턴 상수
  1. START_NOT_STICKY: 종료 후 재시작 안함.
  2. START_STICKY: Default, 정상적으로 종료되지 않았을 때 재시작.
  3. START_REDELIVER_INTENT: 재시작을 하면서 onStartCommand()에 Intent를 다시 전달하여 실행.
  • 서비스에서 Crash가 발생했을 경우, 서비스가 예기치 않게 종료된 것으로 간주하여 재시작하면서 반복해서 Crash가 발생할 수 있음.
  • 불필요한 재시작 방지: stopService(), selfService()
    • stopService(): 앱을 사용하는 내내 실행되는 서비스(실제 사용되는 케이스 많지 않음)
    • selfService(): 서비스를 명시적으로 종료. onDestroy까지 실행

 

 

인텐트 서비스

 

  • 동시에 여러 요청을 처리할 필요가 없을 때 주로 사용
  • 내부적으로 1개의 백그라운드 스레드를 가지고 전달된 Intent를 순차적으로 처리(내부적으로 HandlerThread 사용)
  • onHandleIntent(Intent) 메소드 구현

 

public class ExampleIntentService extends IntentService {
	public ExampleIntentService() {
    	super("ExampleIntentService");
    }
    
    @Override
    protected void onHandleIntent(Intent intent) {
    	...
    }
}

 

  • IntentService 내부적으로 구현한 onStartCommand() 메소드의 기본 리턴 값은 START_NOT_STICKY이다.
  • IntentService는 onCreate()에서 HandlerThread를 생성하고 시작하면서 HandlerThread의 Looper와 연결된 Handler를 생성

 

바운드 서비스

 

  • 특정 기능을 제공하는 메소드를 클라이언트에게 노출
  • 클라이언트는 서비스에 연결하여 메소드를 호출함으로서 통신을 수행하며 자신이 직접 구현하지 않은 기능을 사용
  • 일련의 기능 집합을 제공하는 라이브러리와 유사

 

Binder

 

로컬에서 호출되는 바운드 서비스는 Binder 클래스를 확장하는 방식으로 구현한다. getService 메소드를 재정의하여 자신이 속한 서비스 객체를 리턴하며 서비스는 Binder 객체를 멤버로 생성해 놓고 onBind 메소드에서 이 객체를 리턴하여 클라이언트에게 자신을 노출한다.

 

public class ExampleService extends Service {
	public class ExampleBinder extends Binder {
    	ExampleService getService() {
        	return ExampleService.this;
        }
    }
    
    ExampleBinder exampleBinder = new ExampleBinder();
    
    public IBinder onBinder(Intent intent) {
    	return exampleBinder;
    }
    
    ...
}

 

서비스 연결/해제

 

클라이언트에서 서비스에 연결하거나 해제할 때 다음 메소를 호출하여 바인딩한다.

 

public abstract boolean bindService(Intent service, ServiceConnection conn, int falgs);
void unbindService(ServiceConnection conn);

 

  1. 첫 번째 인수 S첫 번째 인수 service는 사용하고자 하는 서비스를 지정하는데 같은 패키지에 있으면 클래스명으로 지정하고 외부에 있다면 서비스의 액션명을 사용한다.
  2. 두 번째 인수 conn은 서비스가 연결, 해제될 때의 동작을 정의하는 연결 객체이다. 서비스는 사용하는 클라이언트는 ServiceConnection 인터페이스를 구현하는데 클라이언트와 서비스가 연결되거나 해제될 때 호출되는 콜백 메서드를 정의한다.
  3. 마지막 인수 flag는 서비스 바인딩 방식을 지정하는데 통상 BIND_AUTO_CREATE로 지정하여 서비스를 자동으로 기동시킨다.
public class ExampleActivity extends Activity {

	...
    
	public void onResume() {
		super.onResume();
		Intent intent = new Intent(this, ExampleService.class);
		bindService(intent, serviceConnection, BIND_AUTO_CREATE);
	}

	public void onPause() {
		super.onPause();
		unbindService(serviceConnection);
	}

	ServiceConnection serviceConnection = new ServiceConnection() {
		public void onServiceConnected(ComponentName className, IBinder binder) {
			exampleService = ((ExampleService.ExampleBinder)binder).getService();
		}

		public void onServiceDisconnected(ComponentName className) {
			exampleService = null;
		}
	}
}

 

 

 

안드로이드 버전별 백그라운드 정책 히스토리

 

구글에서는 안드로이드 메이저 업데이트 때마다 배터리 효율성을 개선하기 위해 여러가지 기능을 포함해 왔다.

 

  • Kitket 4.4(API 19) 이전: AlarmManager와 Broadcast receiver를 사용.
    • AlarmManager의 지정한 타이밍에 맞춰 시스템에서 알람이 오고, 이 알람에 맞춰서 백그라운드 작업을 수행
    • Kitket부터는 알람이 한없이 미뤄지거나 한번에 몰아서 처리되는 등 정확한 실행을 보장받지 못함
  • Lollipop 5.0(API 21): JobScheduler의 등장. 작업을 미루거나 스케쥴링 할 수 있도록 함.
    • Lollipop 이전 버전에서도 동작해야하는 앱을 관리하기 위해서 버전에 따라 AlarmManager와 JobScheduler를 각각 사용하도록 구현해야함.
  • Marshmello 6.0: Doze modeApp standby mode 등장. 디바이스 또는 앱이 장시간 사용중이 아닐 때, 네트워크의 엑세스를 제한하고 백그라운드 작업을 유예하기 시작
  • Nougat 7.0(API 24): 개선된 Doze mode. 화면이 꺼지고 움직이지 않을 때 Doze mode의 하위 제약조건이 적용되기 시작
    • 특정 인텐트에 대한 동작이 제한
  • Oreo 8.0(API 26): 백그라운드 제약, 백그라운드 서비스와 위치 갱신을 제약하기 시작
    • 암시적 브로드캐스트 리시버의 등록을 차단하고 등 점점 사용 방법에 제한이 추가
    • Background Service 제약 - App이 Background 일 때 Foreground service가 아니면 Background Service를 쓸 수 없게 됨
    • 매니페스트에 등록한 암시적 브로드캐스드를 받을 수 없음
    • 이후 Firebase JobDispatcher를 제공. 안드로이드 G(API 9) 이상을 지원, 내부적으로 현재 시스템의 안드로이드 버전에 따라 AlarmManager와 JobScheduler를 알아서 선택
  • Pie 9.0: App Standby Buckets(앱 대기 버킷), 배터리 세이버 개선

 

 

WorkManager

 

안드로이드에는 로깅, 데이터 업로드, 데이터 싱크 등 다양한 백그라운 작업들이 있다. 그리고 이를 처리하기 위해서 JobSchedulers, Services, Loaders, AlarmManager 등의 많은 처리 방법들이 존재한다.

 

Understanding Requirement

 

  • Exact Timing: 지금 바로 실행되어야하는가?
  • Deferrable: 어떠한 조건이 맞는 원하는 시점에 실행되어야하는가?
  • Best-Effort: 처리하려고 노력하지만, 실행이 취소될 수 있고, 그에 따라 결과물이 없어도 되는가?
  • Graranteed Execution: 반드리 처리하고 원하는 결과를 얻어야 하는가?

 

 

우리는 위 4가지 상황에 맞게 백그라운드 작업 처리하는 방법을 선택할 수 있다.

 

과거에는 AlarmManager, Broadcast Receiver, JobScheduler, Firebase JobDispatcher 등 다양한 방법으로 백그라운드 작업을 처리할 수 있었다. 그리고 2018년 구글 I/O에서 안드로이드의 백그라운 작업을 서포트하기 위해 WorkManager를 공개했다.

*Firebase JobDispatcher는 API 9이상을 지원하고, 내부적으로 현재 시스템의 안드로이드 버전에 따라 AlarmManager와 JobScheduler를 알아서 선택하여 처리해 준다.

 

WorkManager는 Android Jetpack의 아키텍처의 구성요소이다.

 

 

WorkManager는 

  • 실행이 보장된다. 또한 제약조건을 가지고 실행할 수 있다. 예를 들어, 네트워크 연결시에만 처리되는 작업을 추가하면 네트워크가 연결되면 반드시 실행된다.
  • Device의 상태를 존중한다. 가령, Doze mode에 진입하면 일을 처리하기 위해 기기를 깨우지 않는다.
  • 구글서비스의 유무에 상관없이 동작한다.
  • 실행/대기/완료 등의 상태 조회가 가능하다.
  • 작업 A의 결과에 따라 B 또는 C를 선택하여 처리하고 D를 이어서 처리하는 등 작업 연결처리가 가능하다.
  • 기회주의적이다. 즉, 어떤 제한조건이 충족 되었을 때 즉시 실행된다.

 

 

API의 버전에 맞게 AlarmManager 또는 JobScheduler를 사용하며, 만약 개발자가 앱에 Firebase JobDispatcher의 의존성을 추가해 두었다면, 이를 적극 이용한다. 개발자가 WorkManager를 사용함으로서 상황에 따른 고민이나 별도의 구현없이 앱이 종료나 기기의 재부팅된 경우에도 항상 장치에 맞는 가장 적합한 방법을 사용하여 백그라운드 작업을 처리할 수 있다.

 

 

 

그러다고, WorkManager가 모든 것을 처리할 수 있는 것은 아니다.

 

이 작업이 앱의 종료 여부와 상관없이 수행되어야 하는 작업. 즉, 앱의 프로세스 수명과 별도로 살아남기 위한 작업에서 사용할 것을 추천한다. 예를 들어, 이미지를 서버에 업로드 해야하거나, 데이터를 분석하고 이를 데이터베이스에 저장해야하는 작업에는 WorkManager를 사용하는 것이 좋다. 

 

그러나, 사용자가 현재 보고 있는 UI를 빠르게 변경해야하는 작업이나 물건 구입 과정에서의 결제 진행 등 즉시 처리를 해야하는 작업은 WorkManager를 사용하지 않는 게 좋다. WorkManager의 작업은 반드시 실행되지만, 그 처리가 상황에 따라 지연되거나 도중에 중단될 경우 다시 실행될 수 있기 때문이다. 이 점을 유념하여 백그라운드 작업을 처리해야한다.

 

 

 

참고문헌

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

https://developer.android.com/topic/libraries/architecture/workmanager

https://medium.com/mindorks/lets-work-manager-do-background-processing-58356e1ab844