[안드로이드 컴포넌트] Service 구현 예제, 안드로이드 백그라운드 서비스, Service Example of Android Components, Android Background

2018. 9. 10. 17:58 안드로이드/안드로이드 TIP
오늘은 안드로이드 4대 컴포넌트 중 하나인 Service에 대한 포스팅입니다. 안드로이드 4대 컴포넌트가 뭐냐구요? 안드로이드 어플리케이션을 구성하는 메인 구성 요소라 할 수있는데, 우리가 가장 많이 사용하는 Activity가 그 중 하나입니다!



1. Android Components


[그림 1] 안드로이드 4대 컴포넌트




2. 서비스(Service)?

안드로이드에서 Activity는 Background에서 작업을 수행할 수 없다. 이를 위해 우리는 Service를 사용한다. 예를 들면 노래를 켜고 웹 서핑을 하는 상황이 있다. 우리가 음악을 듣는 어플을 사용할때 보통 어플에 들어가 노래를 켜고 다른 화면으로 가거나 화면을 끄거나 다른 어플리케이션을 사용한다. 그럼에도 불구하고 노래를 들을 수 있는건 백그라운드에서 노래를 틀어주는 작업을 수행하고 있기 때문이다. 그럼 이제 머리속에 Service라는 컴포넌트에 대한 스케치가 됐을 것이다.


Service 사용에 있어서 기억해야할 내용은 다음과 같다.

 * 서비스 라이프사이클(Service Life Cycle)

 * 단순 Service에 대한 수행 처리 과정

 * 서비스바운딩에 의한 Service 수행 처리 과정

 * 동시 수행 처리 과정




3. 서비스 라이프사이클(Service Life Cycle)


[그림 2] 서비스 라이프사이클


위의 그림은 안드로이드 디벨로퍼 공식 홈페이지에서 제공하는 API Guide에서 Service Life Cycle 그림을 가져온 것이다. 왼쪽은 사이클은 startService()메서드를 통해 서비스를 수행할 때의 라이프 사이클이다. 서비스를 시작할때는 공통적으로 onCreate()가 호출되며 서비스를 종료할 때는 공통적으로 onDestroy()가 호출된다.




4. 서비스의 수행과 처리


서비스는 타 컴포넌트(클라이언트)와 연결되며 여러 클라이언트를 가질 수 있다. 또한 클라이언트는 특정 작업을 통해 서비스에게 데이터를 요청(서비스 객체, 멤버에 접근)할 수 있다.


[그림 3] 서비스 수행


[그림 3]에서 처럼 startService()를 통해 서비스를 실행할 수 있으며, 서비스 객체를 사용할 수 없다. 즉 서비스 클래스 내의 멤버에 접근할 수 없다. 그렇다면 어떻게 해야할까? 아래 그림은 서비스바운딩(바운드된 서비스 수행)을 통해 서비스 객체에 접근하는 과정이다.

[그림 4] 서비스, 서비스바운딩 수행


[그림 4]에서 처럼 '액티비티1'에서 startService() 호출 후 '액티비티2'에서 bindService()를 호출한 경우, '액티비티2'에서는 서비스바운딩을 통해 현재 실행 중인 서비스 객체에 접근할 수 있다. '액티비티1'에서도 startService() 호출 후 서비스 객체에 접근하기 위해 bindService()를 호출할 수 있다. 이렇게 하나의 서비스에 대해 여러 서비스바운딩이 이뤄진 경우 서비스가  종료되는 시점에 대해 어려움을 가질 수 있다. [그림 3]에서 처럼  startService() 만 호출만 경우에는 stopService()를  통해 서비스를 종료할 수 있지만, 어딘가에서 서비스바운딩이 일어났을때는 바운딩이 끊어져야 서비스 종료가 가능해진다. 즉, 서비스바운딩 중에 stopService()를 호출해도 바운딩이 끊어질때까지 기다리며 서비스바운딩 중인 액티비티에서 바운딩이 끊어져야 비로소 서비스에서 onDestroy()가 호출되며 서비스 수행이 종료된다. 또한 startService() 호출 없이 bindService()만 호출해서 서비스를 시작할 수 있으며 unbindService()를 호출하면 onDestroy()가 호출됨과 동시에 서비스 수행이 종료된다.


혹시 모를 분들을 위해 재밌는 비유를 하겠다.

나는 친구들과 OO음식점에 왔다. 여기서는 음식을 남기면 안되는 규정이 있다. 이때 친구들과 함께 앉은 테이블은 'service', 각자 그릇에 음식을 담는건 'bind', 각자 그릇에 담긴 음식을 다 먹어치우는건 'unbind'라 할 수 있다. 그릇에 담긴 음식을 다 먹기 전까진 나갈 수 없으니 각각 'unbind'가 일어나야 테이블에서 일어 날 수(서비스 종료) 있다. 이때 의문이 들 수 있다.  그럼 테이블 앉지 않고 그릇을 들고 서서 먹으면 되지 않나? 그래도 된다. 친구들과 각자의 그릇을 다 비우고 나가면 된다.!




5. 구현 과정


[그림 5] 소스 구성


1) 메인 액티비티 생성.

2) 메인 레이아웃 구성.

3) 서비스 상속 클래스 생성.

4) AndroidManifest.xml에 서비스 목록 추가.

[그림 6] 안드로이드 매니패스트 설정





6. 구현 화면 및 소스 코드


구현 화면


[그림 7] 안드로이드 캡쳐 화면


[그림 8] 'startService() - stopService()' 수행



[그림 9] 'bindService() - unbindService()' 수행



[그림 10] 'startService() - bindService() - unbindService() - stopService()' 수행


소스 코드

AndroidManifest.xml

<service android:name="MyService"></service> 


activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="peace.service.MainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center">

<Button
android:id="@+id/bt_srart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service Start"/>

<Button
android:id="@+id/bt_bind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service bind"/>

<Button
android:id="@+id/bt_unbind"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service unbind"/>

<Button
android:id="@+id/bt_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service stop"/>

<Button
android:id="@+id/bt_call_variable"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="서비스 멤버 변수 호출하기"/>

<TextView
android:id="@+id/tv_text"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="서비스 멤버 변수 호출 : "/>
</LinearLayout>

</RelativeLayout>


MainActivity.class
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

private Button bt_start, bt_stop, bt_bind, bt_unbind, bt_call_variable;
private TextView tv_text;

private MyService mService;
private boolean isBind;

ServiceConnection sconn = new ServiceConnection() {
@Override //서비스가 실행될 때 호출
public void onServiceConnected(ComponentName name, IBinder service) {
MyService.MyBinder myBinder = (MyService.MyBinder) service;
mService = myBinder.getService();

isBind = true;
Log.e("LOG", "onServiceConnected()");
}

@Override //서비스가 종료될 때 호출
public void onServiceDisconnected(ComponentName name) {
mService = null;
isBind = false;
Log.e("LOG", "onServiceDisconnected()");
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//버튼에 대한 참조
bt_start = (Button)findViewById(R.id.bt_srart);
bt_stop = (Button)findViewById(R.id.bt_stop);
bt_bind = (Button)findViewById(R.id.bt_bind);
bt_unbind = (Button)findViewById(R.id.bt_unbind);
bt_call_variable = (Button)findViewById(R.id.bt_call_variable);
tv_text = (TextView)findViewById(R.id.tv_text);

//각 버튼에 대한 리스너 연결 - OnClickListener를 확장했으므로 onClick 오버라이딩 후 this사용
bt_start.setOnClickListener(this);
bt_stop.setOnClickListener(this);
bt_bind.setOnClickListener(this);
bt_unbind.setOnClickListener(this);
bt_call_variable.setOnClickListener(this);

}

@Override
public void onClick(View v) {
switch (v.getId()){

case R.id.bt_srart :
startService(new Intent(MainActivity.this, MyService.class)); // 서비스 시작
break;

case R.id.bt_stop :
stopService(new Intent(MainActivity.this, MyService.class)); // 서비스 종료
break;

case R.id.bt_bind :
if (!isBind) //해당 액티비이에서 바운딩 중이 아닐때만 호출 - 바운딩 시작
bindService(new Intent(MainActivity.this, MyService.class), sconn, BIND_AUTO_CREATE);
break;

case R.id.bt_unbind :
if (isBind) //해당 액티비티에서 바운딩중일때만 호출 - 바운딩 종료
unbindService(sconn);
break;

case R.id.bt_call_variable :
if (mService == null)
Toast.makeText(this, "mService가 null이므로 불러 올 수 없습니다.", Toast.LENGTH_SHORT).show();
else if (mService != null)
tv_text.setText("불러온 값 : "+mService.var);
break;

}
}
}


MyService.class
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;

public class MyService extends Service {

private IBinder mIBinder = new MyBinder();

public int var = 777; //서비스바인딩의 예시로 출력할 값

class MyBinder extends Binder{
MyService getService(){
return MyService.this;
}
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.e("LOG", "onBind()");
return mIBinder;
}

@Override
public void onCreate() {
Log.e("LOG", "onCreate()");
super.onCreate();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e("LOG", "onStartCommand()");
return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
Log.e("LOG", "onDestroy()");
super.onDestroy();
}

@Override
public boolean onUnbind(Intent intent) {
Log.e("LOG", "onUnbind()");
return super.onUnbind(intent);
}

}

# 참고 사항
'onServiceDisconnected()' 메서드가 호출되는 경우는 서비스로의 연결이 예기치 못하게 끊어졌을 때,
즉 서비스가 출돌했거나 중단되었을때 등 입니다.

클라이언트가 바인딩을 해제한다고 이것이 호출 되지 않습니다.

출처 : http://mailmail.tistory.com/9