안녕하세요!
오늘은 안드로이드의 4대 구성 요소 중 하나인 Service에 대해서 알아보려 합니다!
안드로이드에서 Service란!
" 백그라운드에서 오래 실행되는 작업을 수행할 수 있는 애플리케이션 구성 요소이며 사용자 인터페이스를 제공하지 않습니다. 다른 애플리케이션 구성 요소가 서비스를 시작할 수 있으며, 이는 사용자가 다른 애플리케이션으로 전환하더라도 백그라운드에서 계속해서 실행됩니다. 이외에도, 구성 요소를 서비스에 바인딩하여 서비스와 상호작용할 수 있으며, 심지어는 프로세스 간 통신(IPC)도 수행할 수 있습니다. 예를 들어 한 서비스는 네트워크 트랜잭션을 처리하고, 음악을 재생하고 파일 I/O를 수행하거나 콘텐츠 제공자와 상호작용할 수 있으며 이 모든 것을 백그라운드에서 수행할 수 있습니다. "
라고 안드로이드 공식문서에 설명되어있습니다.
간단하게 설명드리면 안드로이드 앱 이용 중 음원재생과 같은 작업들은 홈버튼을 누르거나 전원버튼을 눌러서 화면을 끄더라도 계속 음원 재생이 되어야합니다. 이럴 경우에는 Service 구성 요소를 사용하는 것입니다! 이 Service 안에서도 Foreground와 Background, Bind의 유형으로 나뉘어져 음원재생과 같은 기능은 Foreground로 구현, 압축 등과 같은 작업에 대해서는 Background로 구현하는 등 어떤 기능을 구현할 것이냐에 따라서 유형이 나뉘지만, 이번 게시글에서는 간단하게 Service 사용법에 대해서 소개하고자 합니다.
먼저 Service는 개발자가 직접 new 연산자를 통해서 인스턴스를 생성하고 실행하는 것이 아닙니다.
startService 혹은 bindService 메서드의 호출을 통해 Service를 시작할 수 있습니다. Service를 시작했는데 아직 Service 인스턴스가 없는 상태라면 Service 인스턴스를 생성하고 실행이 이루어집니다. 그 후 stopService나 unbindService 호출을 통해 Service의 입장에서 모든 참조가 없어졌을 경우 Service는 onDestroy Callback 메서드를 호출하고 파괴됩니다.
그렇다면 startService와 bindService의 차이는 무엇일까요?
가장 큰 차이점이라면 startService는 Service를 실행하고 Service와 직접적인 접근을 할 수 없습니다. 하지만 bindService는 Service안의 onBind Callback 메서드를 통해서 값을 전달 받을 수 있고 Service 인스턴스를 직접 리턴받아 Service의 public 메서드를 통해 접근할 수도 있습니다.
두번째로는 startService로 실행된 Service는 앱의 뒤로가기 버튼으로 앱을 종료해도 background에서 무한정 실행됩니다. startService로 실행된 Service를 종료하기 위해선 Service의 onStartCommand callback 메서드에서 작업수행 후 stopSelf 메서드를 호출하는 로직으로 구성하는 방법과 Activity에서 stopService로 종료하는 방법이 있습니다. 하지만 bindService로 실행된 Service 실행은 목적 자체가 Service와 다른 구성 요소 간의 소통이므로 앱의 Activity에서 onDestory가 호출된 후 자동으로 unbind 처리됩니다. 그러나 홈버튼으로 나갔을 경우에는 자동으로 unbind 처리되지 않습니다.
세번째는 같은 구성요소에서 startService 메서드를 여러번 호출하게되면 각각 다른 startId를 갖으며 하나의 Service 인스턴스 안에서 onStartCommand 메서드가 여럿 실행될 수 있지만 bindService는 여러번 bindService를 호출해도 onBind 메서드는 한번만 호출된다는 것입니다. 그러나 bindService를 호출할 때 아규먼트로 전달한 serviceConnection 인스턴스의 onServiceConnected Callback 메서드는 매번 호출해줍니다.
또 startService를 이용할 때 중요한 부분이 있습니다!
바로 Oreo버전 이상부터는 startService로 사용하지 않고 startForegroundService를 사용해야 background에서 무한정 실행될 수 있습니다. 이는 notification 알림을 남김으로써 사용자에게 background로 Service가 작동되고 있음을 알리고, 앱 관리 및 접근하기 편리함을 제공하려는 움직임으로 생각됩니다.
그렇기에 Oreo버전 이상에서 startService로 Service를 실행하게 된다면 50초 ~ 1분 쯤 지난 후 Service가 종료됩니다.
startService와 startForegroundService의 차이는 startForegroundService를 호출한 후 대략 5초 이내에 startForegorund 메서드가 호출되지 않으면 아래와 같은 Exception이 발생되고 binding된 구성요소가 있다면 onUnbind callback 메서드 호출 후 Service는 onDestroy callback 메서드 호출과 함께 파괴됩니다.
그래서 오늘 예제는 minSDK 버전이 26이므로 startForegroundService를 사용해서 소개해드리겠습니다.
오늘 만들 예제를 소개해드릴게요!
이번 예제에서는 startService와 bindService에 대해서 알아보고 실행되고 있는 Service와 값을 주고 받는 방법에 대해서 다룰 예정이고 어떤 Callback 메서드가
호출 되는지 로그를 통해서 Service에 대해 테스트할 수 있는 예제입니다. 아래는 출력되는 로그들을 캡처한 사진입니다.
예제 개발 순서는 아래와 같습니다.
1. 원하는 음원 파일 준비 및 프로젝트에 넣기
2. BGMService.java
3. activity_main.xml
4. MainActivity.java
1. 원하는 음원 파일 준비 및 프로젝트에 넣기
먼저 준비한 음원을 넣기 위한 Resource Directory를 생성해줘야 합니다.
아래의 사진처럼 res에서 마우스 오른쪽 버튼 > New > Android Resource Directory를 선택합니다.
그리고 아래의 사진처럼 raw를 선택해주신 후 OK로 완료해주세요.
그 다음 아래의 사진처럼 생성된 raw 디렉토리 안에 준비한 음원파일을 넣어주세요. 올려드린 예제의 소스를 사용할 경우 음원이름을 sample_music으로 해야합니다.
2. BGMService.java
package com.smparkworld.serviceexample;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import java.text.SimpleDateFormat;
public class BGMService extends Service {
private MediaPlayer mMP;
private BGMServiceBinder mBinder;
@Override
public void onCreate() {
super.onCreate();
Log.v("BGMService", "Called onCreate method in Service!");
mMP = MediaPlayer.create(getApplicationContext(), R.raw.sample_music);
mMP.setLooping(true);
mMP.start();
mBinder = new BGMServiceBinder();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v("BGMService", "Called onStartCommand method in Service! || startId is " + startId);
Intent notiIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notiIntent, 0);
String channelID = "BGMService";
NotificationChannel channel = new NotificationChannel(channelID, "BGMService", NotificationManager.IMPORTANCE_DEFAULT);
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
Notification noti = new NotificationCompat.Builder(this, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentText("BGMService")
.setContentText("BGM 실행 중")
.setContentIntent(pendingIntent)
.build();
startForeground(1, noti);
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
Log.v("BGMService", "Called onDestroy method in Service!");
mMP.stop();
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.v("BGMService", "Called onBind method in Service!");
return mBinder;
}
@Override
public boolean onUnbind(Intent intent) {
Log.v("BGMService", "Called onUnbind method in Service!");
return false;
}
public String getValue() {
return new SimpleDateFormat("y-M-d H:m:s").format(System.currentTimeMillis()) + " || Object is " + this;
}
public class BGMServiceBinder extends Binder {
public BGMService getService() {
return BGMService.this;
}
}
}
54번째 줄에 startForeground 메서드의 아규먼트를 보면 두번째로 notification이 전달되는 것을 볼 수 있습니다. 만약에 SDK 26보다 낮은 버전이라면 Service를 실행할 때 startService 메서드로 호출하면 되고 startForeground를 호출할 필요도 없기 때문에 notification을 생성할 필요도 없습니다 : )
추가적으로 48 ~ 50번째 줄처럼 아이콘, 제목, 내용이 지정되지 않으면 앱 삭제와 강제종료할 수 있는 앱 설정부분으로 넘어가는 알림이 뜨게 됩니다!
76번째 줄에 onUnbind 메서드의 리턴값을 true로 주게되면 다시 bindService를 호출했을 때 onBind 메서드가 아닌 onRebind 메서드가 호출됩니다. 하지만 처음 onBind 메서드를 호출한 Service 인스턴스가 아직 살아있는 상태여야 onRebind 메서드가 호출됩니다. Service 입장에서 모든 참조가 끊어져 파괴된 상태에서 다시 bindService 메서드를 호출하면 onRebind 메서드를 호출하는 것이 아니라 onBind 메서드를 호출합니다. ( onRebind 메서드 소스 넣는 걸 깜빡했네요.. )
3. activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="Service 예제"
android:textSize="25dp"
android:textColor="@android:color/black" />
<LinearLayout
android:layout_width="200dp"
android:layout_height="wrap_content"
android:orientation="vertical" >
<Button
android:id="@+id/btnStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="시작(start)" />
<Button
android:id="@+id/btnStop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="정지(stop)" />
<Button
android:id="@+id/btnBind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="시작(bind)" />
<Button
android:id="@+id/btnUnbind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="정지(unbind)" />
<Button
android:id="@+id/btnGetValue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="가져오기" />
</LinearLayout>
<TextView
android:id="@+id/tvText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:gravity="center"
android:text="값"
android:textSize="15dp" />
</LinearLayout>
</LinearLayout>
4. MainActivity.java
package com.smparkworld.serviceexample;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private BGMService mBGMService;
private ServiceConnection mServiceConn;
private TextView tvText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tvText = findViewById(R.id.tvText);
mServiceConn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.v("BGMService", "Called onServiceConnected method in Activity!");
mBGMService = ((BGMService.BGMServiceBinder)service).getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.v("BGMService", "Called onServiceDisconnected method in Activity!");
Toast.makeText(getApplicationContext(), "서비스와 연결하지 못했습니다.", Toast.LENGTH_SHORT).show();
}
};
findViewById(R.id.btnStart).setOnClickListener(this);
findViewById(R.id.btnStop).setOnClickListener(this);
findViewById(R.id.btnBind).setOnClickListener(this);
findViewById(R.id.btnUnbind).setOnClickListener(this);
findViewById(R.id.btnGetValue).setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch(v.getId()) {
case R.id.btnStart:
Log.v("BGMService", "Clicked btnStart view in Activity!");
//startService(new Intent(this, BGMService.class));
startForegroundService(new Intent(this, BGMService.class));
break;
case R.id.btnStop:
Log.v("BGMService", "Clicked btnStop view in Activity!");
stopService(new Intent(this, BGMService.class));
break;
case R.id.btnBind:
Log.v("BGMService", "Clicked btnBind view in Activity!");
bindService(new Intent(this, BGMService.class), mServiceConn, BIND_AUTO_CREATE);
break;
case R.id.btnUnbind:
Log.v("BGMService", "Clicked btnUnbind view in Activity!");
if (mBGMService != null) {
mBGMService = null;
unbindService(mServiceConn);
}
break;
case R.id.btnGetValue:
Log.v("BGMService", "Clicked btnGetValue view in Activity!");
if (mBGMService != null)
tvText.setText(mBGMService.getValue());
else
Toast.makeText(getApplicationContext(), "서비스가 아직 실행되지 않았습니다.", Toast.LENGTH_SHORT).show();
break;
}
}
}
여기서 주의깊게 봐야할 점은 29 ~ 42번째 줄의 ServiceConnection 인스턴스를 생성하는 것과 68번째 줄에서 bindService를 호출할 때, 75번째 줄에서
unbindService를 호출할 때 아규먼트로 ServiceConnection을 전달하는 부분입니다 : )
위에서도 언급했듯이 bindService의 핵심은 아무래도 Service와의 소통인 것 같습니다. 그렇기에 Service 인스턴스를 전달받아 접근할 수 있는 ServiceConnection 인스턴스와 bindService, unbindService에서 아규먼트로 전달되는 등의 부분을 중요하게 보아야할 것 같습니다.
31번째 줄의 onServiceConnected 메서드에서 두번째 파라미터 값인 IBinder 인스턴스는 BGMService.java 안에 있는 onBind 메서드의 리턴 값입니다. 그렇기에 34번째 줄처럼 캐스팅 후 Service에 대해 직접적인 접근을 할 수 있습니다.
68번째 줄의 bindService의 3번째 아규먼트인 BIND_AUTO_CREATE를 설정하지 않으면 Service가 실행되지 않은 상태에서는 Service를 실행하지 않고 startService를 통해 Service 인스턴스가 존재하는 상황에서만 bindService가 작동하며 onBind 메서드가 호출됩니다. 하지만 BIND_AUTO_CREATE를 설정하게되면 Service 인스턴스가 존재하지 않더라도 bindService로도 Service를 실행할 수 있습니다. 그리고 시스템에 의해 강제종료 되더라도 메모리가 충분해지면 다시 Service가 실행되며 Service의 onBind callback 메서드의 파라미터인 Intent도 재전달됩니다.
이상으로 Service에 대해서 포스팅을 마치겠습니다!
최근 Service 부분에 대해 공부가 부족하다고 느껴서 공부했었던 내용을 위주로 정리해서 기록한 내용이기에 부족하거나 궁금하신 사항이 있으시면 댓글로 남겨주시면 감사드리겠습니다 : )