코딩하기 좋은날
Android Service ( Start / Intent / bound) 란? - 2 본문
지난 글에 이어 각 서비스들의 예제 프로그램을 만들어 보겠 습니다.
먼저 IntentService 는 onHandleIntent 메서드만 오버라이딩 해주면 됩니다.
IntentService
아래는 그냥 Log를 찍어보는 예제입니다. IntentService의 서브클래스로 생성 해주면 되고 worker thread의 이름을 매개변수로 넣어주어야 합니다.
MyIntentService.kt
package com.example.intentserviceexample
import android.app.IntentService
import android.content.Intent
import android.util.Log
class MyIntentService: IntentService("MyIntentService") {
override fun onHandleIntent(p0: Intent?) {
Log.d("log", "log!!")
}
}
MainActivity 에서 startService 메서드에 MyIntentService 클래스의 Intent를 전달해주면 Service가 실행 됩니다. 이후 큐에 담긴 모든 요청을 처리한 후 중단 됩니다.
MainActivity.kt
package com.example.intentserviceexample
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Intent(this, MyIntentService::class.java).also {intent->
startService(intent)
}
}
}
StartService
StartService는 IntentService 와는 달리 조금 더 귀찮습니다. Service() 클래스의 서브 클래스로 구성됩니다.
최소한 onStartCommand() 콜백 메서드를 구현해야 하며 이 메서드는 서비스가 시작될 때 호출된다.
또한 바운드 서비스가 아니므로 onBind() 메서드에서 null 값을 반환 해야 합니다.
예제 프로그램에서는버튼을 클릭하면 Service가 시작 되고 새로운 스레드를 생성하여 5초간 sleep을 한 후 종료되는 동작을 하겠습니다. 또한 서비스를 멈추기 위해서 stopself( startid ) 함수를 스스로 호출 해줘야 합니다. 이를 호출 하지 않으면 서비스가 종료되지 않습니다.
MyService.kt
package com.example.startedservice
import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.os.SystemClock.sleep
import android.util.Log
import androidx.constraintlayout.widget.Constraints.TAG
import java.lang.Exception
class MyService : Service() {
override fun onCreate() {
Log.d("TAG", "Service Create")
super.onCreate()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d("TAG", "Service Start")
val r = Runnable() {
val time = System.currentTimeMillis() + 5*1000
while(System.currentTimeMillis() < time) {
synchronized(this) {
try {
Thread.sleep(time - System.currentTimeMillis())
} catch (e: Exception) {
}
}
}
stopSelf(startId)
}
val t = Thread(r)
t.start()
return START_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
Log.d("TAG", "Service Destroy")
super.onDestroy()
}
}
MainActivity.kt
package com.example.startedservice
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun btnClick(view: View) {
val intent = Intent(this, MyService::class.java)
startService(intent)
}
}
버튼을 3번 클릭하면 아래와 같이 Log가 뜨는 것을 볼 수 있습니다. 5초가 지나면 각 생성된 서비스가 종료되는 것을 볼 수 있습니다.
BoundService (Local / Remote)
boundService는 안드로이드 서비스와 하나 이상의 클라이언트 컴포넌트 간의 통신을 구현하는 메커니즘을 제공합니다.
기본적으로 하나의 바운드 서비스에 여러 클라이언트 컴포넌트가 바인딩 될 수 있습니다. 그리고 바인딩이 되면 다양한 메커니즘을 사용해서 서비스와 상호작용을 할 수 있습니다.
바운드 서비스는 onBind() 메서드를 구현해야 하며 클라이언트 에서는 bindService() 메서드를 호출하여 서비스에 바인딩 할 수 있습니다.
바운드 서비스에 최초 바인딩이 요청되면 서비스의 onBind() 메서드가 호출되고 IBinder 객체(서비스와 상호작용 하기위해 클라이언트가 필요로 하는 정보를 가짐) 가 생성 됩니다.
이후의 바인딩 요청에서는 onBind()가 호출되지 않고 생성된 IBinder 객체만 전달 합니다. 이후 각 클라이언트가 바인딩을 해제하고 마지막 클라이언트까지 바인딩을 해제하면 시스템이 서비스를 소멸시킵니다.
참고
StartService를 통해 서비스를 실행시키고 클라이언트가 boundservice()를 통해 이에 Binding 할수도 있습니다. 이 때는 클라이언트가 바인딩을 모두 해제해도 서비스가 소멸되지 않으므로 stopself() 또는 stopservice()를 호출 해 주어야 합니다. 또한 이러한 서비스(서비스를 계속 실행 하면서 바인딩도 제공 해야 하는 경우)는 onStartCommand() 와 onBind() 둘 다를 구현 해주어야 합니다. |
바운드 서비스를 구현하는 메커니즘은 두 가지 경우가 있습니다.
- Local: 바운드 서비스와 클라이언트가 동일 애플리케이션에 대해 로컬(동일 프로세스에서 실행)이면서 private(다른 애플리케이션에서 사용 불가)인 경우 라면 Binder 클래스의 서브 클래스를 생성하여 구현이 가능합니다.
- 바운드 서비스가 애플리케이션에 대해 로컬이 아닌 경우(다른 프로세스에서 실행) Messenger와 Handler를 이용하여 구현하는 것이 가장 좋습니다.
Local 바운드 서비스를 먼저 구현해 보겠습니다.
BoundService 클래스는 아래와 같이 구현해주어야 합니다.
1. onBind 함수에서는 Boundservice의 IBinder 객체를 반환 합니다.
2. 현재 시간을 반환하는 getCurrentTime() 함수가 있습니다. 클라이언트가 서비스에 바인딩 되면 이 함수를 호출 할 수 있습니다.
3. inner class 로 Binder의 서브클래스인 MyLocalBinder 를 정의하고 이 클래스는 getService 라는 함수에서 서비스 인터페이스를 반환 해주는 역할을 합니다. 이렇게 함으로써 클라이언트는 BoundService의 다른 메서드에 접근 할 수 있습니다.
BoundService.kt
package com.example.localboundserviceexample
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import java.text.SimpleDateFormat
import java.util.*
class BoundService : Service() {
val myBinder: IBinder = MyLocalBinder()
override fun onBind(intent: Intent): IBinder {
return myBinder
}
fun getCurrentTime() : String {
val dateformat = SimpleDateFormat("HH:mm:ss MM/dd/yyyy", Locale.KOREA)
return dateformat.format(Date())
}
inner class MyLocalBinder: Binder() {
fun getService() : BoundService = this@BoundService
}
}
서비스에 바인딩하고자 하는 클라이언트는
1. onServiceConnected() 와 onServiceDisconnected() 메서드를 포함하는 ServiceConnection의 서브 클래스를 구현하여야 합니다. 클라이언트-서버 형태의 연결이 될 때는 onServiceConnected() 를 호출하고 끊어 질때는 onServiceDisconnected() 가 호출됩니다.
2. 서비스를 담을 객체 변수가 필요하고, 연결이 되어있는지를 판단할 boolean 변수가 필요 합니다.
connection 변수를 하나 선언하고 ServiceConnection의 서브클래스 객체를 반환 해 줍니다.
3. onServiceConnected 함수에서는 연결하려는 서비스의 binder를 얻어 온뒤 getService 함수를 호출하여 서비스를 반환 합니다. 이후 클라이언트는 연결된 서비스의 메서드를 호출 할 수 있게 됩니다.
4. 마지막으로 onStart 함수에서 bindService() 함수를 호출하여 intent와 connection을 매개변수로 전달하 면 클라이언트가 서비스에 바인딩 됩니다.
이후 버튼 클릭이벤트로 연결된 서비스의 메서드를 호출 합니다.
MainActivity.kt
package com.example.localboundserviceexample
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.IBinder
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var myService : BoundService
private var isBound = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
val binder = p1 as BoundService.MyLocalBinder
myService = binder.getService()
isBound = true
}
override fun onServiceDisconnected(p0: ComponentName?) {
isBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
if(isBound)
textView.setText(myService.getCurrentTime())
}
}
override fun onStart() {
super.onStart()
Intent(this, BoundService::class.java).also {intent->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
unbindService(connection)
isBound = false
}
}
서비스를 다른 애플리케이션이나 별도의 프로세스에서 사용하지 않는다면 위와 같은 방식으로 구현 해주면 됩니다.
서비스를 다른 애플리케이션이나 별도의 프로세스에서 사용 해야 한다면 Messenger와 Handler를 이용해서 구현해야 합니다.
이러한 경우는 메시지가 클라이언트로부터 수신될 때 작업을 수행할 Handler 인스턴스를 생성합니다. Handler는 Messenger 객체를 생성하고 이 Messenger 객체는 클라이언트에게 반환될 IBinder 객체를 onBind() 함수에서 생성합니다.
클라이언트에서 서비스에 메시지를 보낼 때마다 이 메시지 객체를 인자로 받는 핸들러의 handleMessage() 함수가 호출 됩니다. 아래에서는 클라이언트에게 받은 메시지를 toast로 출력하는 서비스 입니다.
MyRemoteService.kt
package com.example.remoteboundserviceexample
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.os.Message
import android.os.Messenger
import android.widget.Toast
class MyRemoteService : Service() {
private lateinit var mMessenger: Messenger
internal class IncomingHandler(context: Context, val applicationContext: Context = context.applicationContext) : Handler() {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
val data = msg.data
val str = data.getString("MyString")
Toast.makeText(applicationContext, str, Toast.LENGTH_SHORT).show()
}
}
override fun onBind(intent: Intent): IBinder {
mMessenger = Messenger(IncomingHandler(this))
return mMessenger.binder
}
}
클라이언트에서는 Local Bound Service와 마찬가지로 ServiceConnection을 서브클래스로 가지는 객체를 가지고 있어야 합니다. Local Bound Service와는 다르게 Messenger 객체를 통해서 IBinder 객체를 주고 받습니다.
여기서도 onStart() 와 onStop() 에서 클라이언트가 서비스에 bind / unbind를 해줍니다.
버튼의 onClick 함수인 sendMessage 에서는 보낼 메시지를 저장한뒤 Messenger 객체의 send 메서드를 통해 이를 전달합니다.
MainActivity.kt
package com.example.remoteboundserviceexample
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import androidx.appcompat.app.AppCompatActivity
import android.view.View
class MainActivity : AppCompatActivity() {
private var myService : Messenger? = null
private var isBound = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
myService = Messenger(p1)
isBound = true
}
override fun onServiceDisconnected(p0: ComponentName?) {
myService = null
isBound = false
}
}
fun sendMessage(view: View) {
if(!isBound) return
val msg = Message.obtain()
val bundle = Bundle()
bundle.putString("MyString", "This is Message!")
msg.data = bundle
try {
myService?.send(msg)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onStart() {
super.onStart()
Intent(this, MyRemoteService::class.java).also { intent->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
if(isBound) {
unbindService(connection)
isBound = false
}
}
}
버튼 클릭시 토스트 메시지가 출력됨을 볼 수 있습니다.
'Android' 카테고리의 다른 글
롤(Riot) API를 활용한 롤 알림 안드로이드 앱 제작 - 1 (Kotlin) (0) | 2020.07.19 |
---|---|
Android Notification (kotlin) (0) | 2020.07.12 |
Android Service ( Start / Intent / bound) 란? - 1 (0) | 2020.06.15 |
Android 브로드 캐스트 인덴트 / 리시버 (kotlin) (2) | 2020.06.11 |
Android Realm 기본 사용법(Kotlin) (0) | 2020.05.24 |