티스토리 뷰
이번에는 가볍게 서비스에 관련해서 알아보려고 합니다.
추후 자세한 응용은 간단한 음악 스트리밍을 제작할 예정인데 아직 일정은 정해지지 않았습니다!
우선 기본 이론 편들 위주로 진행하려고 합니다.
1. 서비스란?
Service는 백그라운드에서 오래 실행되는 작업을 수행할 수 있는 애플리케이션 구성 요소이며 사용자 인터페이스를 제공하지 않습니다. 다른 애플리케이션 구성 요소가 서비스를 시작할 수 있으며, 이는 사용자가 다른 애플리케이션으로 전환하더라도 백그라운드에서 계속해서 실행됩니다. 이외에도, 구성 요소를 서비스에 바인딩하여 서비스와 상호작용할 수 있으며, 심지어는 프로세스 간 통신(IPC)도 수행할 수 있습니다. 예를 들어 한 서비스는 네트워크 트랜잭션을 처리하고, 음악을 재생하고 파일 I/O를 수행하거나 콘텐츠 제공자와 상호작용할 수 있으며 이 모든 것을 백그라운드에서 수행할 수 있습니다.
공식 문서기준으로 위와 같이 나와있습니다.
중요한 포인트는 백그라운드에서 작업을 수행한다는 점입니다.
예로 음악 앱을 생각하시면 제일 간단합니다.
음악을 재생시키고 화면을 나갔다고 음악이 종료된다고 하면 스마트폰 이용이 많이 불편할 것입니다.
서비스를 이용하면 음악 재생을 백그라운드가 진행하고 있으므로 화면을 이동한다고 해서 음악이 종료되지 않습니다.
서비스의 유형에는 [포그라운드, 백그라운드, 바인드] 이렇게 3가지 유형이 있습니다.
포그라운드
포그라운드 서비스는 사용자에게 잘 보이는 몇몇 작업을 수행합니다. 예를 들어 오디오 앱이라면 오디오 트랙을 재생할 때 포그라운드 서비스를 사용합니다. 포그라운드 서비스는 알림을 표시해야 합니다. 포그라운드 서비스는 사용자가 앱과 상호작용하지 않을 때도 계속 실행됩니다.
백그라운드
백그라운드 서비스는 사용자에게 직접 보이지 않는 작업을 수행합니다. 예컨대 어느 앱이 저장소를 압축하는 데 서비스를 사용했다면 이것은 대개 백그라운드 서비스입니다.
바인드
애플리케이션 구성 요소가 bindService()를 호출하여 해당 서비스에 바인딩되면 서비스가 바인딩됩니다. 바인딩된 서비스는 클라이언트-서버 인터페이스를 제공하여 구성 요소가 서비스와 상호작용하게 하며, 결과를 받을 수도 있고 심지어 이와 같은 작업을 여러 프로세스에 걸쳐 프로세스 간 통신(IPC)으로 수행할 수도 있습니다. 바인딩된 서비스는 또 다른 애플리케이션 구성 요소가 이에 바인딩되어 있는 경우에만 실행됩니다. 여러 개의 구성 요소가 서비스에 한꺼번에 바인딩될 수 있지만, 이 모든 것에서 바인딩이 해제되면 해당 서비스는 소멸됩니다.
기본 이론의 경우 제 설명보단 공식 문서가 더 좋은 거 같아서 공식 문서로 남겨둡니다
2. 서비스 생성
다음과 같이 생성하면 쉽게 만들 수는 있지만 일단 기본 이론이니 일반 Kotlin 파일 생성으로 진행하는 방법을 알아보겠습니다.
class MyService : Service() {
override fun onBind(p0: Intent?): IBinder? {
TODO("Not yet implemented")
}
}
우선 위과 같이 클래스를 생성 후 Service를 상속하게 만든 뒤 필수인 onBind를 override 해줍니다.
<application
...
<service
android:name=".service.MyService" />
...
</application>
그다음 위와 같이 Manifest에 해당 위치에 서비스를 선언해줍니다.
이렇게까지 하면 서비스를 생성할 준비가 완료되었습니다.
2022.04.19일 기준으로 공식 문서 한국어 버전 기준으로는 IntentService 클래스 확장 부분이 존재합니다만
현재 IntentService의 경우 API Level 30에서 Deprecated 되었으므로 진행하지 않겠습니다.
공식 문서 기준으로 추후에 다룰 WorkManager를 활용을 고려해보라고 되어있네요~
이번 예제에서 진행하는 방식은 startService()를 이용하는 방식과 bindService()를 이용하는 방식입니다.
들어가기 전에 앞서 이번 예제에서는 UI를 고려하지 않기 때문에 다음과 같이 만들어 봤습니다.
<androidx.constraintlayout.widget.ConstraintLayout 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=".service.ServiceActivity">
<Button
android:id="@+id/btnStartService"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="startService()"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnBindService"/>
<Button
android:id="@+id/btnBindService"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="bindService()"
app:layout_constraintTop_toBottomOf="@id/btnStartService"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
준비사항으로 이번 예제에서 음악 재생을 해보려고 합니다.
경로를 위와 같이 준비해 주세요~
저의 경우 해당 사이트에서 음악을 다운로드하여서 이용하였습니다.
Music Library - NCS
ncs.io
3. startService() 방식
전체 코드는 본문 마지막에 들어갑니다~
우선 위에서 만든 서비스에서 onBind() 함수가 필수로 ovrride 하게 되어있는데
이번에는 사용하지 않을 예정이므로
override fun onBind(intent: Intent): IBinder? = null
null을 반환합니다.
그다음 서비스에서 사용할 Thread를 만들기 위해 Handler 클래스를 만듭니다.
// 스레드로부터 메시지를 받는 핸들러
private inner class ServiceHandler(looper: Looper) : Handler(looper) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
try {
mediaPlayer = MediaPlayer.create(this@MyService, R.raw.music_02).also {
// 테스트 편의를 위해 음악의 마지막 부분으로 이동합니다.
it.seekTo(215000)
// MediaPlayer 실행
it.start()
// MediaPlayer 종료되었을때 리스너 등록
it.setOnCompletionListener {
// MediaPlayer 가 종료되면 서비스를 종료 시킵니다.
stopSelf()
}
}
} catch (e : Exception) {
mediaPlayer?.stop()
mediaPlayer?.release()
e.printStackTrace()
stopSelf()
}
}
}
해당 Handler에서 MediaPlayer를 동작시켜줍니다.
startService()로 구현 시에는 서비스 종료를 명시적으로 할 필요성이 있습니다.
종료시키는 방법은 위와 같이 Service 안에서 stopSelf()를 하는 방법이고
다른 방법은 다른 구성요소에서 stopService()를 해주어야 합니다.
이걸 안 해주면 서비스가 일반적으로 종료가 안되므로 반드시 구현해야 합니다.
// 서비스는 일반적으로 우리가 차단하고 싶지 않은 프로세스의 메인 스레드에서 실행되기 때문에 별도의 스레드를 만듭니다.
// 또한 CPU를 많이 사용하는 작업이 UI를 방해하지 않도록 백그라운드 우선 순위를 지정합니다.
override fun onCreate() {
super.onCreate()
HandlerThread("ServiceStartArguments", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
// HandlerThread 의 Looper 를 가져와서 ServiceHandler 에 사용
serviceLooper = looper
serviceHandler = ServiceHandler(looper)
}
}
이제 onCreate()에서 HandlerThread()를 생성 및 실행시켜주고
looper를 가져와서 위에서 만든 ServiceHandler를 생성합니다.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()
// 각 시작 요청에 대해 작업을 시작하라는 메시지를 보내고 시작 ID를 전달하여 작업을 완료할 때 중지할 요청을 알 수 있습니다.
serviceHandler?.obtainMessage()?.also { msg ->
msg.arg1 = startId
serviceHandler?.sendMessage(msg)
}
return START_STICKY
}
이제 onStartCommand()에서 serviceHandler?.sendMessage(msg)로 메시지를 전송함으로써
ServiceHandler()에서 Override 한 handlerMessage()가 동작하게 됩니다.
즉, 이제 MediaPlay가 작동하므로 음악이 들리게 됩니다
return 값으로 START_STICKY를 사용했는데 이 부분은 공식 문서에 자세히 나와있으므로 공식문서 내용을 추가하겠습니다.
START_NOT_STICKY
시스템이 서비스를 onStartCommand() 반환 후에 중단시키면 서비스를 재생성하면 안 됩니다. 다만 전달할 보류 인텐트가 있는 경우는 예외입니다. 이는 서비스가 불필요하게 실행되는 일을 피할 수 있는 가장 안전한 옵션이며, 애플리케이션이 완료되지 않은 모든 작업을 단순히 다시 시작할 수 있을 때 유용합니다.
START_STICKY
시스템이 onStartCommand() 반환 후에 서비스를 중단하면 서비스를 다시 생성하고 onStartCommand()를 호출하되, 마지막 인텐트는 전달하지 않습니다. 그 대신 시스템이 null 인텐트로 onStartCommand()를 호출합니다. 단, 서비스를 시작하기 위한 보류 인텐트가 있는 경우는 예외입니다. 이 경우에는 그러한 인텐트가 전달됩니다. 이것은 명령을 실행하지는 않지만 무한히 실행 중이며 작업을 기다리고 있는 미디어 플레이어(또는 그와 비슷한 서비스)에 적합합니다.
START_REDELIVER_INTENT
시스템이 onStartCommand() 반환 후에 서비스를 중단하는 경우, 서비스를 다시 생성하고 이 서비스에 전달된 마지막 인텐트로 onStartCommand()를 호출하세요. 모든 보류 인텐트가 차례로 전달됩니다. 이것은 즉시 재개되어야 하는 작업을 능동적으로 수행 중인 서비스(예: 파일 다운로드 등)에 적합합니다.
마지막으로 서비스가 종료되었을 때 처리입니다.
override fun onDestroy() {
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
if (mediaPlayer?.isPlaying == true) {
mediaPlayer?.stop()
}
mediaPlayer?.release()
super.onDestroy()
}
복잡하긴 한데 정리하자면
1. Service() 상속받은 클래스 생성
2. 필수 Override인 OnBinde()는 사용 안 함으로 null을 리턴
3. Handler를 상속받은 클래스 구현 (서비스에서 동작하고 싶은 기능을 안에 구현)
4. onCreate()에서 HandlerThread() 생성 및 실행 + HandlerThread()의 looper로 3번에서 Handler 클래스 생성
5. onStartCommand()에서 3번의 Handler클래스 객체에서 obtainMessage를 통해 메시지를 가져온 후 sendMessage로 메시지 전달
6. Handler 클래스의 handlerMessage() 안의 동작 수행
이렇게 이해하시면 될 것 같습니다!
class ServiceActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_service)
findViewById<Button>(R.id.btnStartService).setOnClickListener {
Intent(this, MyService::class.java).also {
startService(it)
}
}
}
}
사용 방법은 위와 같이 만들어둔 서비스를 이용해 Intent를 생성하고 그 Intent를 인자로 startService()를 호출해주면 됩니다.
4. bindService() 방식
새로운 Service 클래스를 생성해서 진행하겠습니다.
class MyBindService : Service() {
private val binder : IBinder = MyServiceBinder()
// 클라이언트 바인더에 사용되는 클래스입니다.
// 우리는 이 서비스가 항상 클라이언트와 동일한 프로세스에서 실행된다는 것을 알고 있기 때문에 IPC를 다룰 필요가 없습니다.
inner class MyServiceBinder : Binder() {
// 클라이언트가 공용 메서드를 호출할 수 있도록 이 MyBindService 인스턴스를 반환합니다.
val service : MyBindService
get() = this@MyBindService
}
override fun onBind(intent: Intent): IBinder = binder
}
우선 위와 같이 Binder()를 상속받은 클래스를 만들어야 합니다.
MyServiceBinder에서 만든 service를 이용해서 MyBindService에 접근하게 됩니다.
만든 binder를 onBind()의 return 값으로 주게 하면 준비는 완료되었습니다. (onDestroy는 startService() 방식과 동일)
startService() 방식에 비해 준비사항은 간단하네요.
추가로 이 예제에서는 음악 재생을 해야 하므로 MyBindService 안에 다음의 함수만 추가해줍니다.
fun play() {
try {
mediaPlayer = MediaPlayer.create(this, R.raw.music_02).also {
it.seekTo(215000)
it.start()
it.setOnCompletionListener {
mediaPlayer?.stop()
mediaPlayer = null
}
}
} catch (e : Exception) {
mediaPlayer?.stop()
mediaPlayer = null
e.printStackTrace()
}
}
이제 액티비티로 넘어옵니다.
class ServiceActivity : AppCompatActivity() {
...
override fun onStart() {
super.onStart()
Intent(this, MyBindService::class.java).also {
bindService(it, connection, BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
unbindService(connection)
}
private var service : MyBindService?= null
private val connection : ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName?, iBinder: IBinder?) {
val binder = iBinder as MyBindService.MyServiceBinder
service = binder.service
}
override fun onServiceDisconnected(p0: ComponentName?) {
service = null
}
}
}
bindService() 방식을 사용하기 위해서는 우선 ServiceConnection이 필요합니다.
ServiceConnection은 Service가 바인딩되었을 때 콜백을 받게 되는데
필수로 onServiceConnected와 onServiceDisconnected를 Override 하게 됩니다.
이름에서 예상할 수 있듯이 서비스가 반인드 되었을 때와 연결이 끊길 때 작업을 추가해 주면 됩니다.
onServiceConnected에서 IBinder의 정보를 가져올 수 있는데 이를 통해 서비스를 받아와 서비스에 접근이 가능해집니다.
bindService() 방식은 startService() 방식과 달리 stopSelf()나 stopService()를 사용하지 않고
위와 같이 onStart()에서 바인드 해주고 onStop()에서 바인드를 제거함으로써 서비스를 종료하게 됩니다.
이제 음악을 재생해야 하므로 버튼 리스너만 추가하면 모두 끝나게 됩니다.
// bindService() 방식
findViewById<Button>(R.id.btnBindService).setOnClickListener {
service?.play()
}
5. 전체 코드
[activity_service.xml]
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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=".service.ServiceActivity">
<Button
android:id="@+id/btnStartService"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="startService()"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/btnBindService"/>
<Button
android:id="@+id/btnBindService"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="bindService()"
app:layout_constraintTop_toBottomOf="@id/btnStartService"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
[ServiceActivity.kt]
class ServiceActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_service)
// startService() 방식
findViewById<Button>(R.id.btnStartService).setOnClickListener {
Intent(this, MyService::class.java).also {
startService(it)
}
}
// bindService() 방식
findViewById<Button>(R.id.btnBindService).setOnClickListener {
service?.play()
}
}
override fun onStart() {
super.onStart()
Intent(this, MyBindService::class.java).also {
bindService(it, connection, BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
unbindService(connection)
}
private var service : MyBindService?= null
// bindService()에 전달된 서비스 바인딩에 대한 콜백을 정의합니다.
private val connection : ServiceConnection = object : ServiceConnection {
// MyServiceBinder 에 바인딩하고 IBinder 를 캐스팅하고 MyServiceBinder 인스턴스를 가져옵니다.
override fun onServiceConnected(className: ComponentName?, iBinder: IBinder?) {
val binder = iBinder as MyBindService.MyServiceBinder
service = binder.service
}
override fun onServiceDisconnected(p0: ComponentName?) {
service = null
}
}
}
// 음악 자료 출처 : https://ncs.io/music
// 자료 출처 (공식문서) : https://developer.android.com/guide/components/services?hl=ko
[MyService.kt]
package com.example.standardstudy.service
import android.app.Service
import android.content.Intent
import android.media.MediaPlayer
import android.os.*
import android.util.Log
import android.widget.Toast
import com.example.standardstudy.R
import java.lang.Exception
class MyService : Service() {
private var mediaPlayer: MediaPlayer? = null
private var serviceLooper: Looper?= null
private var serviceHandler: ServiceHandler?= null
// 스레드로부터 메시지를 받는 핸들러
private inner class ServiceHandler(looper: Looper) : Handler(looper) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
try {
mediaPlayer = MediaPlayer.create(this@MyService, R.raw.music_02).also {
// 테스트 편의를 위해 음악의 마지막 부분으로 이동합니다.
it.seekTo(215000)
// MediaPlayer 실행
it.start()
// MediaPlayer 종료되었을때 리스너 등록
it.setOnCompletionListener {
// MediaPlayer 가 종료되면 서비스를 종료 시킵니다.
stopSelf()
}
}
} catch (e : Exception) {
mediaPlayer?.stop()
mediaPlayer?.release()
e.printStackTrace()
stopSelf()
}
}
}
// 서비스는 일반적으로 우리가 차단하고 싶지 않은 프로세스의 메인 스레드에서 실행되기 때문에 별도의 스레드를 만듭니다.
// 또한 CPU를 많이 사용하는 작업이 UI를 방해하지 않도록 백그라운드 우선 순위를 지정합니다.
override fun onCreate() {
super.onCreate()
HandlerThread("ServiceStartArguments", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
// HandlerThread 의 Looper 를 가져와서 ServiceHandler 에 사용
serviceLooper = looper
serviceHandler = ServiceHandler(looper)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()
// 각 시작 요청에 대해 작업을 시작하라는 메시지를 보내고 시작 ID를 전달하여 작업을 완료할 때 중지할 요청을 알 수 있습니다.
serviceHandler?.obtainMessage()?.also { msg ->
msg.arg1 = startId
serviceHandler?.sendMessage(msg)
}
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? = null
override fun onDestroy() {
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
if (mediaPlayer?.isPlaying == true) {
mediaPlayer?.stop()
}
mediaPlayer?.release()
super.onDestroy()
}
}
[MyBindService.kt]
package com.example.standardstudy.service
import android.app.Service
import android.content.Intent
import android.media.MediaPlayer
import android.os.Binder
import android.os.IBinder
import android.util.Log
import com.example.standardstudy.R
import java.lang.Exception
class MyBindService : Service() {
private val binder : IBinder = MyServiceBinder()
private var mediaPlayer : MediaPlayer?= null
// 클라이언트 바인더에 사용되는 클래스입니다.
// 우리는 이 서비스가 항상 클라이언트와 동일한 프로세스에서 실행된다는 것을 알고 있기 때문에 IPC를 다룰 필요가 없습니다.
inner class MyServiceBinder : Binder() {
// 클라이언트가 공용 메서드를 호출할 수 있도록 이 MyBindService 인스턴스를 반환합니다.
val service : MyBindService
get() = this@MyBindService
}
override fun onBind(intent: Intent): IBinder = binder
// 음악 재생
fun play() {
try {
mediaPlayer = MediaPlayer.create(this, R.raw.music_02).also {
it.seekTo(215000)
it.start()
it.setOnCompletionListener {
mediaPlayer?.stop()
mediaPlayer = null
}
}
} catch (e : Exception) {
mediaPlayer?.stop()
mediaPlayer = null
e.printStackTrace()
}
}
override fun onDestroy() {
super.onDestroy()
if (mediaPlayer?.isPlaying == true) {
mediaPlayer?.stop()
}
mediaPlayer?.release()
Log.e("MyBindService", "bind Service end")
}
}
참고자료
공식문서 1 (Service) : https://developer.android.com/guide/components/services?hl=ko
공식문서 2 (바인드 된 Service) : https://developer.android.com/guide/components/bound-services?hl=ko
추가 도움이 되는 자료
블로그 : https://recipes4dev.tistory.com/166
'안드로이드 > 코드' 카테고리의 다른 글
도로명 주소 개발센터 API 사용해 보기 2:DI + Hilt 사용해 보기 (0) | 2022.05.07 |
---|---|
도로명 주소 개발센터 API 사용해보기1 : Api활용 신청, Retrofit (0) | 2022.04.28 |
안드로이드 웹뷰 사용해보기 3: 각종 속성들 알아보기 (0) | 2022.04.16 |
안드로이드 웹뷰 사용해보기 2 : 웹뷰 적용하기 (0) | 2022.04.13 |
안드로이드 웹뷰 사용해보기 1 : 웹 사이트 제작 (0) | 2022.04.11 |
- Total
- Today
- Yesterday
- Kotlin
- Compose 네이버 지도 api
- Compose Naver Map
- Compose BottomSheet
- Compose 네이버 지도
- Duplicate class found error
- compose
- Worker
- WebView
- Compose BottomSheetDialog
- Gradient
- Android Compose
- Pokedex
- Row
- 안드로이드
- 안드로이드 구글 지도
- Compose QRCode Scanner
- Android
- Fast api
- column
- LazyColumn
- Compose MotionLayout
- 웹뷰
- Compose BottomSheetScaffold
- Duplicate class fond 에러
- 포켓몬 도감
- WorkManager
- Compose ModalBottomSheetLayout
- Compose ConstraintLayout
- Retrofit
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |