IBinder 객체 바인딩
앞 절에서 살펴본 것처럼 서비스를 실행하는 함수를 2개 제공하는 이유는 서비스를 이용하는 상황을 2가지로 구분하기 위해서이다.
예를 들어 액티비티*에서 startService() 함수로 서비스를 실행했다고 가정해 보자.
*서비스를 실행한 곳은 액티비티가 아닌 '다른' 컴포넌트일 수도 있다.
그러면 백그라운드 작업은 필요하지만 액티비티와 데이터를 주고받을 일이 없는 등 서로 관련이 없다면 startService() 함수로 서비스를 실행하면 된다. 그런데 어떤 경우에는 서비스와 액티비티가 상호작용 해야 할 때가 있다. bindService()는 이러한 목적으로 호출하는 함수이다.
bindService() 함수 이름에서 'bind'는 서비스가 실행되면서 자신을 실행한 곳에 객체를 바인딩한다, 즉 객체를 전달한다는 의미이다. 예를 들어 액티비티에서 bindService() 함수로 서비스를 실행하면 서비스에서 넘어온 객체를 가지고 있다가 이 객체의 함수를 호출하여 데이터를 전달한다.
서비스 코드
bindService() 함수로 서비스를 실행하면 생명주기 함수에서 onBind()가 실행되는데, 이 함수에는 반환 타입이 선언되어 있다.
// 서비스에서 객체 바인딩(객체 전달)
class MyBinder : IBinder() { // onBind() 함수를 위한 IBinder 인터페이스를 구현함
fun funA(arg: Int) {
}
fun funB(arg: Int): Int {
return arg * arg
}
}
override fun onBind(intent: Intent): IBinder? { // 서비스 안에서 onBind()(서비스에서 필수인 생명주기 함수)를
// 재정의하면서 IBinder 인터페이스를 상속받음
// 나중에 onBind() 함수 호출되면 서비스를 실행한 곳에 IBinder 인터페이스를 구현한 객체를 전달(바인딩)함
return Mybinder() // onBind() 함수는 IBinder 인터페이스를 반환타입으로 가짐
}
onBind() 함수의 반환 타입은 IBinder 인터페이스이다. 즉, onBind() 함수가 호출되면 서비스를 실행한 곳에 IBinder 인터페이스를 구현한 객체를 전달한다. 그러면 서비스를 실행한 곳에서 이 클래스의 함수를 호출하면서 매개변수와 반환값으로 데이터를 주고받는다.
액티비티 코드
서비스를 bindService() 함수로 실행한 곳에서는 서비스의 onBind() 함수에서 반환한 객체를 ServiceConnection 인터페이스를 구현한 객체의 onServiceConnected() 함수로 받을 수 있다.
(액티비티에서 서비스에서 반환한 객체를 어떻게 받느냐.)
// 액티비티에서 객체를 전달받아 사용
val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: componentName?, service: IBinder?) {
// onServiceConnected 함수의 두번째 매개변수로 서비스에서 전달한 객체를 받음
serviceBinder = service as MyService.MyBinder
// 서비스에서 바인딩한 객체로 새로운 객체 만듦
}
ovdrride fun onServiceDisconnected(name: componentName?) {
}
}
onServiceConnected() 함수의 두 번째 매개변수가 서비스에서 전달한 객체이다. 이렇게 서비스의 객체를 전달받은 후 필요한 순간에 이 객체의 함수를 호출하면서 매개변수나 반환값으로 데이터를 주고받는다.
// 서비스에서 바인딩(전달)한 객체의 함수 호출
serviceBinder.funA(10) // 서비스에서 바인딩한 객체로 만든 객체로 함수 호출함
❓서비스와 액티비티 간의 데이터를 주고받을 때 이전에 살펴봤던 인텐트의 엑스트라 데이터를 이용하면 되지 않나요?
❗️인텐트의 엑스트라 데이터는 인텐트로 특정 컴포넌트를 실행할 때 전달해야 하는 데이터를 의미하고, 반면에 바인드 서비스는 인텐트로 서비스가 실행된 상태에서 발생하는 데이터를 주고받는 방법이다. 즉, 액티비티와 서비스가 이미 실행되고 있는 상황에서 서로 데이터를 주고받는 것이다.
메신저 바인딩
bindService() 함수로 서비스를 실행한 곳에는 앞에서 살펴본 IBinder를 구현한 객체를 바인딩한다. 그런데 API에서 제공하는 Messenger(를 구현한 객체를) 객체를 바인딩하는 방법도 있다.
Messenger 객체를 이용하는 방법은 프로세스 간 통신(inter-process communication, IPC)할 때도 사용할 수 있다. 즉, 외부 앱과 연동하여 프로세스끼리 통신할 때도 사용한다. 안드로이드에서 프로세스 간 통신하는 방법에는 1️⃣AIDL도 있지만 2️⃣메신저를 이용하면 코드를 더 간단하게 작성할 수 있다. AIDL은 메신저를 이용하는 방법 이후에 알아보자~
서비스 코드
Messenger 객체를 이용하는 서비스 쪽 코드는 다음과 같다.
// 메신저 객체를 이용하는 서비스 코드
class MyService : Service() {
lateinit var messenger: Messenger
internal class IncomingHandler(
context: Context,
private val applicationContext: Context = context.applicationContext
) : Handler(Looper.getMainLooper()) { // IncomingHandler는 Handler를 상속받음
override fun handleMessage(msg: Message) {
// 외부에서 handleMessage()에 전달하는 함수는 Message타입이다
when (msg.what) { // 전달받은 Message의 what값으로 어떤 성격의 데이터인지 구분함.
10 ->
Toast.makeText(applicationContext, "${msg.obj}",
// obj 속성으로 전달된 데이터 가져옴
Toast.LENGTH_SHORT).show()
20 ->
Toast.makeText(applicationContext, "${msg.obj}",
Toast.LENGTH_SHORT).show()
else -> super.handleMessage(msg)
}
}
}
override fun onBind(intent: Intent): IBinder? { // 바인드 서비스에서 Messenger를 이용하기 위해
messenger = Messenger(IncomingHandler(this))
// 1️⃣ onBind 함수의 반환값으로 Messenger 객체를 생성하면서
// 2️⃣ 생성자 매개변수로 Handler를 구현한 객체(IncomingHandler(this))를 지정함
return messenger.binder
// 3️⃣ 그리고 Messenger 객체(messenger)의 binder 속성을 onBind() 함수의 결괏값으로 반환한다.
}
}
IncomingHandler는 Handler 클래스를 상속받아 작성한다. IncomingHandler 클래스에 재정의한 handleMessage() 함수는 외부에서 서비스에 데이터를 전달할 때 자동으로 호출된다. 이때 외부에서 전달한 데이터는 Message 타입으로, IncomingHandler() 함수의 매개변수로 받는다. 전달받는 Message의 what값으로는 어떤 성격의 데이터인지를 구분하여 obj 속성으로는 전달된 데이터를 가져온다.
바인드 서비스에서 Messenger를 이용하려면 onBind() 함수의 반환값으로 Messenger 객체를 생성하면서 생성자 매개변수로 Handler를 구현한 객체를 지정한다. 그리고 Messenger 객체의 binder 속성을 onBind() 함수의 결괏값으로 반환해 준다.
액티비티 코드
다음은 Messenger 객체를 이용하는 액티비티 쪽 코드이다.
// 메신저 객체를 이용하는 액티비티 코드
class MainActivity : AppcompatActivity() {
lateinit var messenger: Messenger
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(... 생략 ...)
val intent = Intent(this, MyService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE)
// IBinder를 구현한 객체를 전달받을 때와 같이 bindService() 함수를 이용해 서비스를 실행함.
}
(... 생략 ...)
val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: componentName?, service: IBinder?) {
// 서비스에서 넘어온 객체(service)를 onServiceConnected() 함수의 매개변수로 받음
// ? service: IBinder 가 아니라 service: Messenger 여야하는 거 아닌가?
messenger = Messenger(service)
// 단지 서비스로부터 넘어온 객체를 Messenger의 생성자 매개변수에 지정해주면 됨
}
override fun onServiceDisconnected(name: ComponentName?) {
}
}
}
Messenger를 이용하는 액티비티 코드는 IBinder를 전달받는 코드와 큰 차이가 없다. bindService() 함수로 서비스를 실행하면 되고, 서비스에서 넘어온 객체는 onServiceConnected() 함수의 매개변수로 받는다. 단지 서비스로부터 넘어온 객체를 Messenger의 생성자 매개변수에 지정만 해주면 된다.
이렇게 서비스를 실행한 후 서비스에 데이터를 전달하고 싶을 때는 다음처럼 Messenger의 send() 함수를 호출한다.
// 서비스에 데이터 전달
val msg = message()
msg.what = 10
msg.obj = "hello"
messenger.send(msg)
// messenger의 send() 함수 호출해서 매개변수로 서비스에 데이터(msg객체) 전달함
// => 액티비티에서 send() 함수 호출하는 순간 서비스에서 handleMessage() 함수가 자동 호출됨
send() 함수의 매개변수는 전달하는 데이터를 추상화한 Message 객체이다. 액티비티에서 send() 함수를 호출하는 순간 서비스의 handleMessage() 함수가 자동으로 호출된다.
외부 앱 연동
Messenger 방식으로 외부 앱의 서비스를 bindService() 함수로 실행하려면 먼저 서비스를 등록한 매니페스트에 외부 앱을 연동할 수 있게끔 <intent-filter>가 선언되어 있어야 한다.
// 매니페스트에 인텐트 필터 선언
<service
android:name=".MyService"
android:exported="true">
<intent-filter>
<action android:name="ACTION_OUTER_SERVICE" />
</intent-filter>
</service>
그리고 이 서비스를 bindService() 함수로 실행하는 앱에서는 외부 앱에 접근할 수 있도록* 매니페스트에 다음처럼 선언한다. <package> 태그의 name 속성값에는 연동하고자 하는 앱의 패키지명을 지정한다.
* 13장에서 설명한 것처럼 안드로이드 11 버전부터는 패키지 공개 상태를 적용해야 한다.
// 매니페스트에 패키지 정보 등록
<manifest ... 생략 ... >
(... 생략 ...)
<queries>
<package android:name="com.exampple.texst_outter" />
// package 태그의 name 속성값에 연동하고자 하는 앱의 패키지명을 지정함
</queries>
</manifest>
그리고 내부 앱의 서비스를 bindService() 함수로 실행할 때는 필요가 없지만, 외부 앱을 연동하고자 한다면 bindService() 함수로 발생하는 인텐트에 실행 대상인 앱의 패키지명을 명시해야 한다.
// 실행할 앱의 패키지명 명시
val intent = Intent("ACTION_OUTER_SERVICE") // bindService()함수로 발생하는 인텐트
intent.setPackage("com.example.text_outter") // 인텐트의 패키지속성에 실행대상인 앱의 패키지명을 명시함
bindService(intent, connection, Context.BIND_AUTO_CREATE)
// bindService() 함수로 인텐트 발생시킴(시스템에 인텐트 전달)
그리고 같은 앱에서 데이터를 주고받을 때 사용한 것처럼 문자열 데이터 Message의 obj 속성을 설정하여 외부 앱과 주고받으면 다음같은 오류가 발생한다.
java.lang.RuntimeException: Can't marshal non-Parcelable objects across processes.
프로세스 간 통신에서는 주고받는 데이터는 Parcelable이나 Bundle 타입이어야 한다. 따라서 다음처럼 1️⃣ 데이터를 Bundle에 담고 2️⃣ 다시 Message 객체에 담아서 전달한다.
// 번들 객체 이용
val bundle = Bundle() // 번들 객체 생성
bundle.putString("data1", "hello")
bundle.putInt("data2", 10) // 번들에는 여러 데이터 담을 수 있음
val msg = Message() // 주고받을 메시지 객체 생성
msg.what = 10 // 메시지 객체에 데이터 넣음(?)
msg.obj = bundle // 데이터를 Bundle에 담음
messenger.send(msg) // 데이터를 다시 Message 객체에 담아서 전달함
AIDL 통신 기법
AIDL(Android interface definition language)은 두 프로세스 사이에 데이터를 주고받는 프로세스 간 통신을 구현할 때 사용하는 기법으로, 서비스 컴포넌트의 bindService() 함수를 이용한다. 안드로이드에서는 기본적으로 하나의 프로세스에서 다른 프로세스의 메모리에 접근할 수 없다. 따라서 데이터를 시스템에 전달한 후에 시스템이 다른 프로세스에 전달해 줘야 한다.
그런데 시스템에 전달하는 데이터는 시스템이 해석할 수 있는 원시 타입으로 변환해야 하고 전송하는 데 적합한 형식으로 변환하는 마샬링(marshalling)과정을 거쳐야 한다. 그런데 AIDL을 이용하면 이러한 작업을 대신 처리해주므로 편리하다.
프로세스 간 통신은 앞에서 살펴본 메신저를 이용하면 AIDL보다 더 쉽게 구현할 수 있다. 하지만 메신저를 이용하는 방법은 플랫폼에서 제공하는 API를 이용해야 하므로 (메신저보다 AIDL이 나은 이유)1️⃣ 주고받는 데이터의 종류가 많을 때는 효율이 떨어질 수 있다. 또한 (이유)2️⃣ 메신저는 모든 외부 요청을 싱글 스레드에서 처리하지만 AIDL은 여러 요청이 들어오면 멀티 스레드 환경에서 동시에 실행한다. 3️⃣그리고 메신저도 내부적으로는 AIDL 기법을 이용한다.
서비스를 제공하는 앱
프로세스 간 통신에서 먼저 서비스 컴포넌트를 제공하는 앱을 살펴보자. 이 앱은 외부 앱과 AIDL로 데이터를 주고받으면서 작업을 처리한다. AIDL을 이용하려면 우선 확장자가 aidl인 파일을 만들어야 한다.
모듈의 aidl 디렉터리에 확장자가 '*.aidl'인 파일을 만들었다. 이 파일을 만드는 방법은 15-5절의 실습에서 자세히!
이 파일은 확장자가 aidl일 뿐 실제로는 자바로 작성한 인터페이스이다.
// AIDL 파일 내용
package com.example.text_aidl;
interface MyAIDLInterface {
void funA(String data); // 추상함수가 선언되어 있음
int funB();
}
AIDL 파일이라고 해서 특별한 작성 규칙은 X. 인터페이스에 추상 함수만 선언되어 있다. 이 함수는 외부 앱에서 AIDL 방식으로 처리하는 작업을 의뢰할 때 호출한다. 함수를 호출할 때 매개변수와 반환값으로 데이터를 외부 앱과 주고받는다.
AIDL 파일에는 외부와 통신하는 데 필요한 함수만 정의되어 있다. 따라서 어디선가 이 함수의 구체적인 로직을 구현해야 하는데 그 역할을 서비스 컴포넌트가 한다.
// 서비스 컴포넌트 구현
class MyAIDLService : Service() {
override fun onBind(intent: Intent): IBinder {
return object : yAIDLInterface.Stub() { // Stub 주목!
// AIDL 파일을 구현한, 즉 실제 로직을 구현한 객체가 아니라
// 프로세스 간 통신을 대행해 주는 객체인 Stub을 전달함
override fun funA(data: String?) { // AIDL 파일의 함수 재정의: 로직 구체화
// ➡️서비스에서 구현되었지만 실제로는 시스템에서 처리할 내용
}
override fun funB() {
return 10
}
}
}
}
AIDL 파일에 선언된 함수를 구현해 실제 작업을 처리하는 내용을 작성하는 곳은 서비스이다. 위 코드처럼 AIDL 파일에 선언된 funA(), funB() 함수를 재정의해서 적절한 로직을 추가한다. 이렇게 하면 외부 앱이 funA(), funB()를 호출할 때 이 서비스에서 구현한 함수가 실행된다.
AIDL은 바인드 서비스를 이용하므로 onBind() 함수에서 서비스를 인텐트로 실행한 곳에 객체를 전달해 줘야 한다. 이때 AIDL 파일을 구현한 객체가 아니라 프로세스 간 통신을 대행해 주는 Stub를 전달한다. 즉, 실제 로직이 구현된 객체가 아니라 프로세스 간 통신을 대행해 주는 객체를 전달한다. 이 Stub 객체는 개발자가 직접 만들지 않고, MyAIDLInterface.Stub()처럼 AIDL 파일명 뒤에 Stub()라고 선언만 해주면 된다.
AIDL을 구현한 서비스는 외부 앱과 연동하는 것이 목적이므로 매니페스트에 암시적 인텐트로 실행되게끔 <intent-filter>를 추가한다.
// 인텐트 필터 선언
<service
android:name=".MhyAIDLService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="ACTION_AIDL_SERVICE" />
</intent-filter>
</service>
서비스를 이용하는 외부 앱
이번에는 AIDL 서비스를 이용하는 앱의 코드를 살펴보자. 인텐트로 외부 앱의 서비스를 실행할 때 bindService() 함수를 이용하여 인텐트에 패키지 정보를 포함해야 한다. 그런데 안드로이드 11 버전부터는 외부 앱의 패키지 정보에 접근할 때 패키지 공개 상태에 영향을 받으므로 다음처럼 매니페스트에 등록해줘야 한다.
// 매니페스트에 패키지 정보 등록
<queries>
<package android:name="com.example.text_outter">
</queries>
AIDL 서비스를 이용하는 앱도 AIDL 서비스를 제공하는 앱에서 만든 AIDL 파일을 가지고 있어야 한다.
이제 bindService() 함수를 이용해 외부 앱의 서비스를 실행한다.
// 외부 앱의 서비스 실행
class MainActivity : AppCompatActivity() {
lateinit var aidlSErvice: MyAIDLInterface
(... 생략 ...)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(... 생략 ...)
val intent = Intent("ACTION_AIDL_SERVICE")
intent.setPackage("com.example.test_outter")
bindService(intent, connection, Context.BIND_AUTO_CREATE)
// bindService()를 이용해 외부 앱의 서비스를 실행
}
(... 생략 ...)
val connection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// 두번째 매개변수 객체가 서비스에 전달한 객체임
// 이 객체는 AIDL(프로세스 간 통신)이 목적이므로
// 서비스를 포함하는 앱과 프로세스 간 통신을 대행해주는 Stub임
aidlService=MyAIDLInterface.Stub.asinterface(service)
// 그리고 그 객체를 이 줄처럼 AIDL파일에 선언한 인터페이스 타입으로 받음
}
override fun onServiceDisconnected(name: componentName?) {
Log.d("kkang", "onServiceDisconnected")
}
}
}
bindService() 함수로 서비스를 실행할 때 자동으로 호출되는 onServiceConnected() 함수의 두 번째 매개변수가 서비스에서 전달한 객체이다. 이 객체는 AIDL이 목적이므로 서비스를 포함하는 앱과 프로세스 간 통신을 대행해 주는 Stub이다. 이 객체를 aidlService=MyAIDLInterface.Stub.asInterface(service)처럼 AIDL 파일에 선언한 인터페이스 타입으로 받으면 된다.
이제 AIDL을 제공하는 앱과 연동해야 할 때 인터페이스의 함수만 호출하면 된다. 그러면 AIDL 서비스를 제공하는 앱의 함수가 실행된다.
// AIDL에 선언된 함수 호출
aidlService.funA("hello")
'깡샘 코틀린' 카테고리의 다른 글
15-4 잡 스케줄러 (0) | 2023.07.08 |
---|---|
15-3 백그라운드 제약 (0) | 2023.07.07 |
15-1 서비스 컴포넌트 (0) | 2023.07.06 |
14-2 시스템 상태 파악하기 (0) | 2023.07.03 |
14-1 브로드캐스트 리시버 이해하기 (0) | 2023.07.03 |