프로세스 간 통신(IPC
)을 사용하여 통신하기 위해 클라이언트와 서비스 모두 동의하는 프로그래밍 인터페이스를 정의하는 것
안드로이드에서 일반적으로는 프로세스에서 다른 프로세스의 메모리에 접근할 수 없음, 통신하려면 객체를 운영체제가 이해할 수 있게 기본형으로 분해하고 객체를 마샬링해 전해줌, 이런 마샬링하는 코드를 직접 작성하기 지루하기 때문에 안드로이드는 AIDL
을 사용해 이를 대신 처리함
노트:
AIDL
은IPC
를 위해 다른 앱의 클라이언트들이 서비스에 액세스할 수 있도록 허용하고 동시에 멀티스레딩을 처리하고 싶을때만 사용하십시오. 만약 다른 앱과 동시에IPC
를 수행할 필요가 없다면Binder
를 구현해 인터페이스를 생성하십시오. 만약IPC
를 수행하고 싶지만 멀티스레딩을 처리하고 싶지 않다면,Messenger
를 사용해 인터페이스를 구현하십시오. 그럼에도AIDL
을 구현해야 한다면 하기 전에 연결된 서비스를 확실하게 이해하고 하십시오.
AIDL
인터페이스를 설계하기 전에 AIDL
인터페이스에 대한 호출은 직접적인 함수 호출한다는 것을 알아야 함, 호출이 일어나는 곳이 로컬 프로세스의 스레드인지 원격 프로세스인지에 따라 일어나는 일이 다름
로컬 프로세스에서 일어나는 호출은 호출을 하는 동일한 스레드에서 실행됨, 그 스레드가 만약 메인 UI 스레드라면, 그대로 AIDL
인터페이스에서 실행됨, 만약 다른 스레드라면 서비스에서 해당 스레드로 코드가 실행됨, 만약 로컬 스레드만 서비스에 액세스한다면, 실행될 스레드를 완전히 제어할 수 있지만 이런 경우라면 AIDL
을 사용하지 말고 Binder
를 구현해야 함
원격 프로세스에서 일어나는 호출은 플랫폼이 프로세스 내에 유지되는 스레드 풀에서 전달됨, 모르는 스레드에서 호출이 올 수 있고, 많은 호출들이 동시에 올 수도 있으므로 대비해야 함, 그러므로 AIDL
을 구현하는 인터페이스는 스레드로부터 안전하게 설계되어야 함, 한 스레드에서 동일한 원격 객체에 대해 여러번 호출하면 순서대로 수신됨
oneway
키워드는 원격 호출의 방식을 수정함, 이 키워드가 사용되면 원격 호출이 차단되지 않고 정보를 전달하면 즉시 반환됨, 이 인터페이스의 구현은 Binder
스레드의 일반 원격 호출에서 결국 일반 호출로 전달받음, 이 키워드가 로컬 호출에서 사용되면 아무 효과가 없고 호출이 동기적으로 작동함
AIDL
인터페이스 정의AIDL
인터페이스를 정의하려면 .aidl
파일에 자바 언어로 작성해 저장하고 서비스를 호스팅하는 앱과 연결을 하려는 앱 둘다 src/
경로에 저장해야 함.aidl
파일이 내장된 앱을 빌드할 때 안드로이드 SDK 툴은 이 파일에 기반한 IBinder
인터페이스를 프로젝트의 gen/
폴더에 저장함, 서비스는 반드시 IBinder
인터페이스를 적절하게 구현해야 함, 그러면 클라이언트 앱은 IPC
를 수행하기 위해 서비스에 연결하고 메소드를 호출할 수 있음AIDL
을 사용하는 연결된 서비스를 생성하는 단계.aidl
파일 생성.aidl
파일을 기반으로 자바 인터페이스를 생성함, 이 인터페이스는 Binder
를 확장하고 AIDL
인터페이스의 메소드를 구현한 Stub
이라고 명명된 내부 추상 클래스를 가지고 있음, 이 클래스를 상속받아 메소드를 구현해야 함onBind()
를 오버라이드해 Stub
클래스의 구현체를 반환함주의: 서비스를 이용하는 다른 앱을 손상하지 않도록
AIDL
인터페이스를 한번 출시했다면 변경을 할 때 반드시 이전 버전과 호환되게 만들어야 합니다. 왜냐하면.aidl
파일이 다른 앱이 서비스의 인터페이스에 액세스하기 위해 복제되었기 때문에 반드시 원래 인터페이스에 대한 지원을 유지해야 합니다.
.aidl
파일 생성AIDL
은 매개 변수와 반환 값을 가지는 하나 이상의 메소드를 가지는 인터페이스를 간단한 문법으로 정의할 수 있게 함, 매개 변수와 반환값은 어떤 타입이든 가능함, 다른 AIDL
기반도 가능함
.aidl
파일은 반드시 자바 프로그래밍 언어로 구성되어야 함, 이 파일은 반드시 하나의 인터페이스만 정의해야 하며 인터페이스 선언과 메소드 시그니처만 필요함
AIDL
이 지원하는 데이터 타입모든 자바 프로그래밍 언어의 기본형, 단 short는 예외
모든 타입의 배열
String
CharSequence
List
: 모든 원소는 이 리스트에 있는 데이터 타입이거나 다른 AIDL
에 의해 생성된 인터페이스나 당신이 선언한 parcelable
이어야 함, List<String>
과 같이 파라미터화된 타입도 사용 가능함, 메소드는 List
인터페이스를 사용하도록 생성되지만 반대편에서 실제로 받는 구체적 클래스는 ArrayList
임
Map
: 모든 원소든 이 리스트에 있는 데이터 타입이거나 다른 AIDL
에 의해 생성된 인터페이스나 당신이 선언한 parcelable
이어야 함, Map<String, Integer>
와 같이 파라미터화된 타입은 지원하지 않음, 메소드는 Map
인터페이스를 사용하도록 생성되지만 반대편에서 실제로 받는 구체적 클래스는 HashMap
임, Map
의 대체로 Bundle
을 사용하는 것을 고려해볼 것
위의 리스트에 존재하지 않는 추가 타입들은 인터페이스와 같은 패키지에 있더라도 반드시 import
구문을 포함해야 함
서비스의 인터페이스를 정의해야할 때 알아야 할 것
메소드는 매개 변수가 0개 이상임, 값 또는 void
를 반환할 수 있음
기본형이 아닌 타입들은 in
, out
, inout
과 같은 방향 태그를 필요로 함
String
, IBinder
, AIDL
기반 인터페이스는 기본적으로 in
이며 변경할 수 없음주의: 매개변수를 마샬링하는 것은 비용이 많이 들기 때문에 실제로 필요한 방향으로만 제한하십시오.
.aidl
파일에 import
와 패키지 구문 이전의 주석을 제외한 모든 주석들은 생성된 IBinder
인터페이스에 포함됨
const int VERSION = 1;
과 같이 String
과 int
상수를 정의할 수 있음
메소드 호출은 인터페이스의 메소드 인덱스에 기반해 transact()
코드에 의해 전달됨, 이게 버전 관리를 어렵게 하기 때문에 void method() = 10
과 같이 메소드에 트랜잭션 코드를 직접 할당할 수 있음
매개 변수 및 반환 값이 NULL
을 허용한다면 반드시 @nullable
주석을 달아야 함
.aidl
파일 예제
// IRemoteService.aidl
package com.example.android;
// 여기에 기본 타입이 아닌 타입을 import 문으로 선언할 수 있습니다.
/** 서비스 인터페이스 예제 */
interface IRemoteService {
/** 서비스의 프로세스 ID를 요청합니다. */
int getPid();
// AIDL에서 매개 변수 및 반환 값으로 사용할 수 있는 기본 타입 예시.
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
.aidl
파일을 프로젝트의 src/
경로에 저장해야 함, 그러면 SDK 툴이 IBinder
인터페이스를 프로젝트의 gen/
경로에 생성해줌, 생성된 파일은 .aidl
파일의 이름과 같고 확장자가 .java
로 저장됨, 예를 들면 IRemoteService.aidl
파일은 IRemoteService.java
파일로 생성됨Gradle
툴이 다음에 앱을 빌드할 때 바인더 클래스를 생성함, 코드와 생성된 클래스가 연결되기 위해 .aidl
파일을 작성한 직후에 gradle assembleDebug
또는 gradle assembleRelease
로 프로젝트를 빌드해야 함.aidl
인터페이스 파일과 같은 이름으로 .java
파일을 생헝함, 생성된 인터페이스는 Stub
이라고 불리는 서브 클래스를 포함하고 있음, 이는 부모 인터페이스의 구현체임노트:
Stub
은 몇가지 도우미 메소드도 정의하는데 대표적으로asInterface()
가 있습니다. 이 메소드는 보통 클라이언트의onServiceConnected()
메소드에 전달되는IBinder
를 받아서stub
인터페이스의 인스턴스를 반환합니다. 자세한 내용은IPC
메소드 호출 부분에서 다룹니다.
.aidl
파일의 인터페이스를 구현하려면 생성된 Binder
인터페이스(예: YourInterface.Stub
)를 확장하고 메소드를 구현하면 됨IRemoteService.aidl
에서 정의된 인터페이스를 구현하는 예제
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int =
Process.myPid()
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// 아무것도 하지 않음.
}
}
AIDL
인터페이스를 구현하기 전에 알아야 할 몇가지IPC
호출은 동기식임. 서비스가 요청을 완료하는데 시간이 좀 걸린다면 메인스레드에서 호출하지 않아야 함, 그렇지 않으면 안드로이드가 ANR
대화상자를 띄울 수 있음, 이런 상황에서는 클라이언트의 별도의 스레드에서 호출해야 함Parcel.writeException()
문서에 나열된 예외만 호출자에게 전달됨Service
를 확장해 onBind()
를 구현하고 생성된 Stub
의 구현체를 반환해 서비스의 인터페이스를 노출할 수 있음IRemoteService
에서 인터페이스를 노출하는 예제
class RemoteService : Service() {
override fun onCreate() {
super.onCreate()
}
override fun onBind(intent: Intent): IBinder {
// 인터페이스를 반환합니다.
return binder
}
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int {
return Process.myPid()
}
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// 아무것도 하지 않음.
}
}
}
bindService()
를 호출해 서비스와 연결하면 클라이언트의 onServiceConnected()
콜백에서 서비스의 onBind()
메소드에서 반환되는 binder
인스턴스를 받음.aidl
파일이 있어야 함, 그래야 android.os.Binder
인터페이스를 생성해 클라이언트가 AIDL
메소드에 액세스할 수 있음onServiceConnected()
콜백에서 IBinder
를 받으면 인터페이스의 asInterface()
를 통해 형변환을 해야 함클라이언트가 IRemoteService
와 연결했을 때 IBinder
를 형변환하는 예제
var iRemoteService: IRemoteService? = null
val mConnection = object : ServiceConnection {
// 서비스와 연결이 성립되었을 때 호출됩니다.
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// 앞서 설명한 AIDL 인터페이스 예시를 따라,
// 서비스에 호출할 때 사용할 수 있도록 IRemoteInterface 인스턴스를 얻습니다.
iRemoteService = IRemoteService.Stub.asInterface(service)
}
// 서비스와 연결이 예기치 못하게 해제되었을 때 호출됩니다.
override fun onServiceDisconnected(className: ComponentName) {
Log.e(TAG, "Service has unexpectedly disconnected")
iRemoteService = null
}
}
IPC
를 통해 객체 전달Parcelable
객체를 AIDL
에서 직접 정의할 수 있음, AIDL
인터페이스 인자에 지원되는 타입과 다른 parcelable
들도 여기서 지원됨, 이는 사용자 지정 클래스의 마샬링 코드를 작성하는 추가 작업을 할 필요가 없게 함, 하지만 이렇게 하면 빈 구조체가 생성됨, 사용자 지정 접근자나 다른 기능이 필요하다면 Parcelable
을 직접 구현해야 함package android.graphics;
// Rect를 선언하여 AIDL이 이를 찾고, parcelable 프로토콜을 구현함을 알립니다
parcelable Rect {
int left;
int top;
int right;
int bottom;
}
left
, top
, right
, bottom
을 가지는 자바 클래스를 자동으로 생성해줌, 추가 구현이 필요 없이 마샬링 코드가 자동으로 구현됨으로 객체를 직접 사용할 수 있음IPC
인터페이스를 통해 다른 프로세스에 사용자 지정 클래스를 전송할 수 있음, 하지만 해당 클래스의 코드가 IPC
채널의 반대편에 있어야 하고 안드로이드 시스템이 객체를 기본형으로 분해하고 마샬링해 프로세스간 전송이 가능하도록 Parcelable
인터페이스를 지원해야 함Parcelable
을 지원하는 클래스를 만드는 방법
Parcelable
인터페이스를 구현Parcel
에 쓰기 위한 writeToParcel
구현CREATOR
라는 정적 필드를 추가함, 값은 Parcelable.Creator
을 구현하는 객체parcelable
클래스를 선언한 .aidl
파일 생성.aidl
파일을 추가하지 않아야 함, C 언어의 헤더 파일처럼 .aidl
파일은 컴파일되지 않음AIDL
은 코드에서 생성되는 메소드와 필드를 사용해 객체를 마샬링 및 언마샬링함parcelable
인 Rect
클래스를 생성하는 Rect.aidl
파일의 예제
package android.graphics;
// Rect를 선언하여 AIDL이 이를 찾고, parcelable 프로토콜을 구현함을 알립니다.
parcelable Rect;
Parcelable
인터페이스 구현하는 Rect
클래스의 예제
import android.os.Parcel
import android.os.Parcelable
class Rect() : Parcelable {
var left: Int = 0
var top: Int = 0
var right: Int = 0
var bottom: Int = 0
companion object CREATOR : Parcelable.Creator<Rect> {
override fun createFromParcel(parcel: Parcel): Rect {
return Rect(parcel)
}
override fun newArray(size: Int): Ar<rayRect?> {
return Array(size) { null }
}
}
private constructor(inParcel: Parcel) : this() {
readFromParcel(inParcel)
}
override fun writeToParcel(outParcel: Parcel, flags: Int) {
outParcel.writeInt(left)
outParcel.writeInt(top)
outParcel.writeInt(right)
outParcel.writeInt(bottom)
}
private fun readFromParcel(inParcel: Parcel) {
left = inParcel.readInt()
top = inParcel.readInt()
right = inParcel.readInt()
bottom = inParcel.readInt()
}
override fun describeContents(): Int {
return 0
}
}
Rect
클래스를 마샬링하는 것은 간단함, Parcel
에 쓸 수 있는 다양한 값을 확인하려면 Parcel
의 다른 메소드를 확인해야 함주의: 다른 프로세스에서 받는 데이터의 보안 문제를 항상 염두에 두세요.
Rect
의 경우Parcel
에서 네 숫자를 읽지만, 이 값의 범위가 호출자가 하려는 작업에 적합한지는 개발자에게 달려있습니다. 애플리케이션을 멀웨어로부터 안전하게 보호하는 방법에 관한 자세한 내용은 보안 도움말을 참고하세요.
Parcelable
이 포함된 Bundle
인수를 사용하는 메소드parcelable
이 포함될 것으로 예상되는 Bundle
객체를 허용한다면 Bundle.setClassLoader(ClassLoader)
를 호출해 번들에서 값을 읽기 전에 Bundle
의 클래스로더를 설정해야 함, 그러지 않으면 앱에 parcelable
이 제대로 정의되어 있어도 ClassNotFoundException
이 발생할 수 있음Rect
를 포함한 Bundle
인수를 사용하는 .aidl
파일 예제
// IRectInsideBundle.aidl
package com.example.android;
/** 서비스 인터페이스 예제 */
interface IRectInsideBundle {
/** Rect parcelable은 bundle의 "rect" 키로 저장됩니다. */
void saveRect(in Bundle bundle);
}
위 예제에 따라 Bundle
에서 Rect
를 읽기 전에 ClassLoader
를 설정하는 예제
private val binder = object : IRectInsideBundle.Stub() {
override fun saveRect(bundle: Bundle) {
bundle.classLoader = classLoader
val rect = bundle.getParcelable<Rect>("rect")
process(rect) // parcelable로 추가 작업 수행.
}
}
IPC
메소드 호출AIDL
로 정의된 원격 인터페이스를 호출하는 방법
src/
경로에 .aidl
파일을 포함시키기AIDL
을 기반으로 생성된 IBinder
인터페이스의 인스턴스 선언ServiceConnection
구현Context.bindService()
를 호출해 ServiceConnection
구현체를 전달onServiceConnected()
의 구현에서 service
라고 불리는 IBinder
객체를 받아서 YourInterfaceName.Stub.asInterface((IBinder) service)
를 호출해 받은 매개변수를 YourInterface
타입으로 형변환DeadObjectException
, 두 프로세스간의 AIDL
정의가 충돌할 때 발생하는 SecurityException
예외를 항상 처리해야 함Context.unbindService()
를 호출IPC
서비스를 호출할 때, 프로세스 간에 객체는 참조 횟수가 계산되고, 메소드 인수로 익명 객체를 전송할 수 있다는 점을 염두에 두어야 함아래는 ApiDemos 프로젝트의 Remote Service 샘플에서 가져온, AIDL로 생성된 서비스를 호출하는 샘플 코드입니다.
private const val BUMP_MSG = 1
class Binding : Activity() {
/** 서비스의 기본 인터페이스입니다. */
private var mService: IRemoteService? = null
/** 서비스의 또 다른 인터페이스입니다. */
internal var secondaryService: ISecondary? = null
private lateinit var killButton: Button
private lateinit var callbackText: TextView
private lateinit var handler: InternalHandler
private var isBound: Boolean = false
/**
* 서비스의 메인 인터페이스와 상호작용하는 클래스입니다.
*/
private val mConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// 서비스와의 연결이 성립되면 호출되며,
// 서비스와 상호작용할 수 있는 서비스 객체를 제공합니다.
// IDL 인터페이스를 통해 서비스와 통신하므로,
// 원시 서비스 객체에서 클라이언트 측 표현을 얻습니다.
mService = IRemoteService.Stub.asInterface(service)
killButton.isEnabled = true
callbackText.text = "Attached."
// 서비스에 연결된 동안 서비스를 모니터링하려고 합니다.
try {
mService?.registerCallback(mCallback)
} catch (e: RemoteException) {
// 이 경우, 서비스가 무언가를 하기도 전에 크래시가 발생합니다.
// 곧 연결이 끊길 것이라 확신하기 때문에
// (재시작될 수 있다면 다시 연결될 것입니다.)
// 여기서는 특별히 할 일이 없습니다.
}
// 샘플의 일부로 사용자에게 상황을 알립니다.
Toast.makeText(
this@Binding,
R.string.remote_service_connected,
Toast.LENGTH_SHORT
).show()
}
override fun onServiceDisconnected(className: ComponentName) {
// 이 메소드는 서비스의 연결이 예상하지 못하게
// 끊겼을 때(예: 서비스 프로세스가 중단되었을 때) 호출됩니다.
mService = null
killButton.isEnabled = false
callbackText.text = "Disconnected."
// 샘플의 일부로 사용자에게 상황을 알립니다.
Toast.makeText(
this@Binding,
R.string.remote_service_disconnected,
Toast.LENGTH_SHORT
).show()
}
}
/**
* 서비스의 두번째 인터페이스와 상호작용하기 위한 클래스입니다.
*/
private val secondaryConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// 두번째 인터페이스에 연결하는 것도 다른 인터페이스와 동일합니다.
secondaryService = ISecondary.Stub.asInterface(service)
killButton.isEnabled = true
}
override fun onServiceDisconnected(className: ComponentName) {
secondaryService = null
killButton.isEnabled = false
}
}
private val mBindListener = View.OnClickListener {
// 서비스와 몇 가지 연결을 설정합니다.
// 인터페이스 이름으로 바인딩하면,
// 동일한 인터페이스를 구현하는 다른 애플리케이션이
// 원격 서비스를 대체할 수 있습니다.
val intent = Intent(this@Binding, RemoteService::class.java)
intent.action = IRemoteService::class.java.name
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
intent.action = ISecondary::class.java.name
bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE)
isBound = true
callbackText.text = "Binding."
}
private val unbindListener = View.OnClickListener {
if (isBound) {
// 서비스를 받았고, 등록했다면 이제 등록 해제할 시간입니다.
try {
mService?.unregisterCallback(mCallback)
} catch (e: RemoteException) {
// 서비스가 크래시되면 특별히 할 일이 없습니다.
}
// 기존 연결을 해제합니다.
unbindService(mConnection)
unbindService(secondaryConnection)
killButton.isEnabled = false
isBound = false
callbackText.text = "Unbinding."
}
}
private val killListener = View.OnClickListener {
// 서비스를 호스팅하는 프로세스를 종료하려면 PID를 알아야 합니다.
// 편리하게도 서비스에는 이 정보를 반환하는 호출이 있습니다.
try {
> secondaryService?.pid?.also { pid -
// 이 API를 사용하면 PID 기반으로 어떤 프로세스든 종료를 요청할 수 있지만,
// 커널은 실제로 종료할 수 있는 PID에 대해 표준 제한을 둡니다.
// 일반적으로 이는 애플리케이션을 실행하는 프로세스와
// 그 애플리케이션이 생성한 추가 프로세스만 해당됩니다.
// 공통 UID를 공유하는 패키지도 서로의 프로세스를 종료할 수 있습니다.
Process.killProcess(pid)
callbackText.text = "Killed service process."
}
} catch (ex: RemoteException) {
// 서버를 호스팅하는 프로세스가 죽었을 때도 정상적으로 복구합니다.
// 이 샘플에서는 알림을 표시합니다.
Toast.makeText(this@Binding, R.string.remote_call_failed, Toast.LENGTH_SHORT).show()
}
}
// ----------------------------------------------------------------------
// 콜백을 어떻게 처리하는지 보여주는 코드입니다.
// ----------------------------------------------------------------------
/**
* 이 구현은 원격 서비스로부터 콜백을 받는 데 사용됩니다.
*/
private val mCallback = object : IRemoteServiceCallback.Stub() {
/**
* 원격 서비스가 새로운 값을 알리기 위해 정기적으로 호출합니다.
* IPC 호출은 각 프로세스에서 실행되는 스레드 풀을 통해 디스패치되므로,
* 여기서 실행되는 코드는 대부분의 경우와 달리 메인 스레드에서 실행되지 않습니다.
* UI를 업데이트하려면 Handler를 사용하여 메인 스레드로 넘어가야 합니다.
*/
override fun valueChanged(value: Int) {
handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0))
}
}
/**
* 이 액티비티의 표준 초기화입니다. UI를 설정한 후,
* 사용자가 상호작용할 때까지 아무것도 하지 않습니다.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.remote_service_binding)
// 버튼 탭을 감시합니다.
var button: Button = findViewById(R.id.bind)
button.setOnClickListener(mBindListener)
button = findViewById(R.id.unbind)
button.setOnClickListener(unbindListener)
killButton = findViewById(R.id.kill)
killButton.setOnClickListener(killListener)
killButton.isEnabled = false
callbackText = findViewById(R.id.callback)
callbackText.text = "Not attached."
handler = InternalHandler(callbackText)
}
private class InternalHandler(
textView: TextView,
private val weakTextView: WeakReferenceTextView = WeakReference(textView)
) : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
BUMP_MSG - weakTextView.get()?.text = "Received from service: ${msg.arg1}"
else - super.handleMessage(msg)
}
}
}
}
원문: https://developer.android.com/develop/background-work/services/aidl