Android에서 Remote Service를 구현하는 방법은 AIDL(Android Interface Definition Language) 또는 Messenger를 이용하는 방법이 있습니다. Messenger는 내부적으로 AIDL을 이용하여 IPC(Inter-process communication)를 수행합니다.
AIDL과 Messenger의 차이점은 다음과 같습니다.
- AIDL : Android는 Binder를 이용하여 IPC를 수행합니다. AIDL은 인터페이스 정의로, 정의된 인터페이스에 필요한 Binder 코드들을 컴파일 과정에서 자동 생성합니다.
AIDL로 Remote service를 구현하면 멀티쓰레드에서 여러 요청을 동시에 요청하거나 처리할 수 있습니다. 물론 쓰레드에 안전하도록 구현을 해야 합니다.
- Messenger : 내부적으로 AIDL을 이용하여 구현되어있습니다. AIDL을 사용하는게 번거롭기 때문에 쉽게 사용할 수 있는 Messenger 클래스를 제공합니다.
외부로부터 전달된 데이터는 단일 쓰레드에서 처리되기 때문에 쓰레드에 안전하도록 구현할 필요는 없습니다. 다만 Handler에서 순차적으로 Task가 처리되기 때문에 동시에 처리할 수는 없습니다.
이 글에서는 AIDL을 이용하여 Remote Service를 구현하는 방법을 소개합니다.
이 글에서 사용된 샘플은 GitHub에 있습니다.
Remote Service 정의
Service를 구현하고 실행하려면 먼저 다음과 같이 AndroidManifest.xml
에 선언해야 합니다.
이렇게 선언하면 Local Service로 실행됩니다. 즉, 이 Service는 Application의 프로세스와 동일한 프로세스에서 실행됩니다.
<application>
...
<service android:name=".LocalService" />
</application>
Remote Service는 Application과 다른 프로세스에서 실행되며, 다음과 같이 android:process
속성에 프로세스 이름을 입력하면 그 이름으로 프로세스가 실행되며, 서비스는 이 프로세스에서 동작합니다.
<application>
...
<service android:name=".RemoteService" android:process=":remote" />
</application>
서비스를 실행한 뒤에 adb shell
명령어로 프로세스 리스트를 확인해보면 다음과 같이 두개의 프로세스를 찾을 수 있습니다. Remote Service는 com.example.remoteservice:remote
프로세스에서 동작합니다.
$ adb shell ps -ef | grep remoteservice
u0_a140 3560 1776 4 22:32:12 ? 00:00:00 com.example.remoteservice
u0_a140 3658 1776 2 22:32:13 ? 00:00:00 com.example.remoteservice:remote
서비스 구현
서비스는 다음과 같이 구현할 수 있습니다. 코드에서 보이는 IRemoteService
, IRemoteServiceCallback
은 AIDL으로 자동 생성된 클래스입니다.
AIDL으로 인터페이스를 정의하는 방법은 뒤에서 알아보겠습니다.
class RemoteService : Service() {
private val binder = object : IRemoteService.Stub() {
override fun addCallback(callback: IRemoteServiceCallback): Boolean {
return true;
}
override fun removeCallback(callback: IRemoteServiceCallback?): Boolean {
return true;
}
}
override fun onBind(p0: Intent?): IBinder? {
return binder
}
}
AIDL 인터페이스 정의
AIDL은 프로세스간 통신을 위한 인터페이스가 정의할 때 사용합니다. AIDL 파일을 만들면 정의된 인터페이스를 기반으로 바인더 코드가 자동 생성됩니다.
안드로이드 스튜디오에서 [File] -> [New] -> [AIDL] -> [AIDL File]
을 누르시면 다음과 같이 AIDL 파일을 만들 수 있습니다.
IRemoteService
, IRemoteServiceCallback
을 생성하시면 다음과 같이 aidl 폴더에 파일들이 생성됩니다.
파일을 열어보면 자바로 구현된 샘플 코드가 입력되어 있습니다. AIDL은 Java를 사용하여 인터페이스를 정의해야 합니다.
이제 샘플 코드를 지우고, 인터페이스를 정의해보겠습니다.
IRemoteService.aidl
은 다음과 같이 코드를 변경해 줍니다.
package com.example.remoteservice;
import com.example.remoteservice.IRemoteServiceCallback;
interface IRemoteService {
boolean addCallback(IRemoteServiceCallback callback);
boolean removeCallback(IRemoteServiceCallback callbac);
}
IRemoteServiceCallback.aidl
은 다음과 같이 코드를 변경해 줍니다.
package com.example.remoteservice;
interface IRemoteServiceCallback {
void onItemAdded(String name);
void onItemRemoved(String name);
}
AIDL 파일을 구현했다고 바로 코드가 자동 생성되는 것은 아닙니다.
[Build] -> [Make Project]
를 누르면 AIDL이 컴파일되어 코드가 자동 생성됩니다.
자동 생성된 코드는 다음 경로에 생성됩니다.
app/build/generated/aidl_source_output_dir/debug/out/com/example/remoteservice$ ls
IRemoteServiceCallback.java IRemoteService.java
다음과 같이 자동 생성된 코드를 GitHub에 올려두었습니다. 디버깅 목적으로 로그도 추가하였습니다. 궁금하시면 여기서 간단히 코드를 확인하실 수 있습니다.
Remote Service 구현
다음은 제가 구현한 RemoteService의 전체 코드입니다. 여기서 중요한 것은 binder입니다. App이 이 Service에 바인딩할 때 binder를 리턴합니다. App은 바인더에 정의된 인터페이스를 이용하여 Service와 통신할 수 있습니다.
class RemoteService : Service() {
companion object {
const val TAG = "RemoteService"
}
val listeners = arrayListOf<IRemoteServiceCallback>()
private val binder = object : IRemoteService.Stub() {
override fun addCallback(callback: IRemoteServiceCallback): Boolean {
Log.d(TAG, "Add callback : $callback")
listeners.add(callback)
return true;
}
override fun removeCallback(callback: IRemoteServiceCallback?): Boolean {
Log.d(TAG, "Remove callback : $callback")
listeners.remove(callback)
return true;
}
}
override fun onBind(p0: Intent?): IBinder? {
handler.sendEmptyMessageDelayed(0, 5000)
return binder
}
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
listeners.forEach { listener ->
listener.onItemAdded("new item");
listener.onItemRemoved("old item");
}
sendEmptyMessageDelayed(0, 5000)
}
}
}
Binder 구현
위의 코드에서 Binder 구현 코드를 좀 더 자세히 보면, Binder는 IRemoteService.Stub
을 상속받습니다.
IRemoteService.Stub
는 IRemoteService.aidl
파일이 컴파일될 때 자동 생성되는 클래스입니다.
아래 코드는 익명 클래스로 구현했지만, 클래스를 정의하고 그것을 생성해도 됩니다.
private val binder = object : IRemoteService.Stub() {
override fun addCallback(callback: IRemoteServiceCallback): Boolean {
Log.d(TAG, "Add callback : $callback")
listeners.add(callback)
return true;
}
override fun removeCallback(callback: IRemoteServiceCallback?): Boolean {
Log.d(TAG, "Remove callback : $callback")
listeners.remove(callback)
return true;
}
}
Service에서 App으로 이벤트 전달
IRemoteService에 대한 바인더를 App에 전달하면, App만 서비스를 호출할 수 있습니다. 즉, 서비스는 App에게 어떤 이벤트도 전달할 수 없습니다.
만약 App이 Service에게 바인더를 전달해준다면, Service는 App에게 이벤트를 전달할 수 있습니다.
위에서 정의된 IRemoteService.Stub
의 addCallback()
은 App이 Service로 IRemoteServiceCallback
객체를 전달하기 위해 만든 메소드입니다.
만약 바인딩이 되면, 서비스가 App 쪽으로 5초 간격으로 onItemAdded()
, onItemRemoved()
를 호출하도록 하였습니다.
override fun onBind(p0: Intent?): IBinder? {
handler.sendEmptyMessageDelayed(0, 5000)
return binder
}
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
listeners.forEach { listener ->
listener.onItemAdded("new item");
listener.onItemRemoved("old item");
}
sendEmptyMessageDelayed(0, 5000)
}
}
위의 예제는 단순히 Service에서 App의 바인더를 이용하여 인터페이스를 호출하는 것을 보기 위한 것입니다.
실제로는 이런 패턴으로 사용되지 않을 수 있습니다. 단순히 참고만 해주세요.
App에서 서비스 바인더 얻기
다음 코드는 App에서 서비스로 바인딩하고, IRemoteService
바인더를 얻습니다.
그리고 App은 이 바인더를 통해 IRemoteServiceCallback
바인더를 서비스에 전달합니다.
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
private var bound = false;
private var iRemoteService: IRemoteService? = null
private var callback = object : IRemoteServiceCallback.Stub() {
override fun onItemAdded(name: String?) {
Log.d(TAG, "onItemAdded: $name")
bound = true
}
override fun onItemRemoved(name: String?) {
Log.d(TAG, "onItemRemoved: $name")
bound = false
}
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Log.d(TAG, "onServiceConnected: $className")
iRemoteService = IRemoteService.Stub.asInterface(service)
iRemoteService!!.addCallback(callback)
}
override fun onServiceDisconnected(className: ComponentName) {
Log.d(TAG, "onServiceDisconnected: $className")
iRemoteService = null
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
override fun onResume() {
super.onResume()
if (!bound) {
val intent = Intent(this, RemoteService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
}
override fun onStop() {
super.onStop()
if (bound) {
iRemoteService!!.removeCallback(callback)
unbindService(connection)
}
}
}
바인딩, 바인더 변환
서비스에 바인딩하는 코드를 좀 더 자세히 알아보겠습니다.
다음과 같은 코드를 실행하면, 서비스에 바인딩을 시도합니다.
val intent = Intent(this, RemoteService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE);
서비스에 연결되면 onServiceConnected()
이 callback되며 IRemoteService가 인자로 전달됩니다.
private var iRemoteService: IRemoteService? = null
private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Log.d(TAG, "onServiceConnected: $className")
iRemoteService = IRemoteService.Stub.asInterface(service)
iRemoteService!!.addCallback(callback)
}
override fun onServiceDisconnected(className: ComponentName) {
Log.d(TAG, "onServiceDisconnected: $className")
iRemoteService = null
}
}
인자를 보면 IBinder로 전달됩니다. IRemoteService는 IBinder를 상속하는 구조이기 때문에, IBinder를 IRemoteService로 변환할 수 있습니다.
IRemoteService.aidl
은 컴파일될 때 IRemoteService.Stub.asInterface()
메소드를 생성합니다.
다음과 같이 이 메소드를 사용하여 IBinder를 IRemoteService로 변환할 수 있습니다.
iRemoteService: IRemoteService? = IRemoteService.Stub.asInterface(service)
확인
앱을 실행하고 의도한대로 잘 동작하는지 로그로 확인해보았습니다. PID를 보시면 MainActivity와 RemoteService가 다른 프로세스에서 실행되는 것을 볼 수 있습니다. 그리고 바인더를 통해 서로에게 이벤트를 전달하는 것을 볼 수 있습니다.
08-22 12:32:58.310 5905 5905 D MainActivity: onServiceConnected: ComponentInfo{com.example.remoteservice/com.example.remoteservice.RemoteService}
08-22 12:32:58.311 5940 5959 D RemoteService: Add callback : com.example.remoteservice.IRemoteServiceCallback$Stub$Proxy@ea9718b
08-22 12:33:03.306 5905 5924 D MainActivity: onItemAdded: new item
08-22 12:33:03.307 5905 5924 D MainActivity: onItemRemoved: old item
08-22 12:33:08.313 5905 5924 D MainActivity: onItemAdded: new item
08-22 12:33:08.313 5905 5924 D MainActivity: onItemRemoved: old item
다른 앱에서 RemoteService에 바인딩
지금까지 자신의 앱에서 구현한 Remote Service에 바인딩하는 방법을 알아보았습니다.
다른 앱에서 구현한 서비스에 바인딩하는 방법에 대해서 더 알아보고 싶다면 다른 앱의 Service에 바인딩을 참고해주세요.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력