Android 바인드 서비스

timothy jeong·2021년 11월 14일
0

Android with Kotlin

목록 보기
43/69

바인드 서비스

바인드된 서비스란 클라이언트-서버 인터페이스 안의 서버 역할을 맡은 컴포넌트(서비스)를 말한다. 이를 사용하면 컴포넌트(Activity 등)를 서비스에 바인딩하고, 요청을 보내고, 응답을 수신하며, 프로세스 간 통신(IPC)을 실행할 수 있다. 일반적으로 바인드된 서비스는 다른 애플리케이션 구성요소를 도울 때까지만 유지되고 백그라운드에서 무한히 실행되지 않는다.

시작된 서비스를 바인딩 시키기

바인딩 되지 않은 서비스는 자신을 실행시킨 컴포넌트와의 통신이 불가능하다. startService() 함수를 통해 시작시킨 서비스는 stopService() 를 호출할때까지 계속 백그라운드에서 돌아가게 되는데, 이미 시작된 서비스를 컴포넌트와 바인딩 시키면 해당 컴포넌트와 연결이 끊겨도 서비스가 종료되지 않도록 할 수 있다.

바인드된 서비스 만들기

서비스 클래스에서 onBind 가 반환하는 객체가 서비스를 실행시킨 컴포넌트에 반인딩 된다. IBinder 인터페이를 구현한 모든 객체가 그 대상이 될 수 있다.

바인더 클래스 확장

서비스가 애플리케이션 전용이고 클라이언트와 같은 프로세스에서 실행되는 경우, 인터페이스를 생성할 때 Binder 클래스를 상속하고 그 인스턴스를 onBind()에서 반환하는 방식을 사용한다. 클라이언트(서비스를 시작한 컴포넌트)는 Binder 를 받고, 이를 사용해서 Binder 에 구현된 함수나, Service에서 제공하는 함수에 직접 액세스 할 수 있다.

서비스가 애플리케이션을 위해 단순히 백그라운드에서 작동하는 요소에 그치는 경우 선호되며 보통 바인드 서비스를 이용하는 이유가 이 때문이다. 인터페이스를 생성할 때 이 방식을 사용하지 않는 경우는 서비스를 다른 애플리케이션이나 별도의 프로세스에서 사용할 때뿐이다.

// Binder 를 상속한 MyBinder 를 반환하도록 함.
class MyService: Service() {
    override fun onBind(intent: Intent?): IBinder {
        return MyBinder()
    }
    // 이때 MyBinder 는 내부 클래스로 정의.
    class MyBinder: Binder() {
        fun funA(arg: Int) = arg
        fun funB(arg: Int) = arg * arg
    }
}

액티비티가 클라이언트라고 할때, onStart() 에서 바인딩 하고, onStop() 에서 바인딩을 푸는 코드이다.

class MainActivity : AppCompatActivity() {

    var isBind: Boolean = false

    val connection: ServiceConnection = object: ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val binder = service as MyService.MyBinder
            val num = binder.funB(10)
            isBind = true
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            isBind = false
        }
    }

    override fun onStart() {
        super.onStart()
        Intent(this, MyService::class.java)
        bindService(intent, connection, Context.BIND_AUTO_CREATE)
    }

    override fun onStop() {
        super.onStop()
        unbindService(connection)
        isBind = false

    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

메신저 사용

인터페이스가 여러 프로세스에서 작동해야 하는 경우, Messenger로 서비스 인터페이스를 생성할 수 있다. 이 방식을 사용하면 서비스가 여러 가지 유형의 Message 객체에 응답하는 Handler를 정의한다.

이 Handler가 Messenger의 기초가 되어 클라이언트와 IBinder를 공유하고, 클라이언트는 Message 객체를 사용해 서비스에 명령어를 보낼 수 있게 된다. 그 외에도 클라이언트가 자체적으로 Messenger를 정의하여 서비스가 메시지를 돌려보내도록 할 수 있다.

이렇게 메신저를 사용하는 것이 프로세스 간 통신(IPC)을 실행하는 가장 간단한 방법이다. Messenger가 모든 요청을 단일 스레드로 큐에 저장하므로 서비스를 스레드로부터 안전하게 설계할 필요가 없기 때문이다.

메신저 서비스를 직접 만들어보자. 서비스 내부 클래스로 Handler 를 만들고, 들어오는 메시지에 따라 반응 하도록 만들자. 이때 해당 클래스는 Handler()를 상속하고, handleMessage 함수를 재정의 해야한다. 그리고 onBind() 시점에 해당 핸들러를 갖는 메신저를 클래스를 만들고, 이 메신저의 IBinder 를 반환하도록 한다.

private const val MSG_SAY_HELLO = 1

class MessageService: Service() {

    lateinit var mMessenger: Messenger


    internal class IncomingHandler(context: Context,
                                   private val applicationContext: Context = context.applicationContext)
        : Handler(Looper.getMainLooper()){
        override fun handleMessage(msg: Message) {
            when(msg.what) {
                MSG_SAY_HELLO -> Toast.makeText(applicationContext, "hello!", Toast.LENGTH_SHORT).show()
                else -> super.handleMessage(msg)
            }
        }
    }

    override fun onBind(intent: Intent?): IBinder? {
        Toast.makeText(applicationContext, "binding", Toast.LENGTH_SHORT).show()
        mMessenger = Messenger(IncomingHandler(this))
        return mMessenger.binder
    }
}

이렇게 하면 클라이언트가 서비스에서 호출할 메서드가 없다. 대신 클라이언트는 메시지(Message 객체)를 서비스로 전달하여 Handler 로 받을 수 있도록 한다.

class MainActivity : AppCompatActivity() {

    private var mService: Messenger? = null
    var isBind: Boolean = false

    val connection: ServiceConnection = object: ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            mService = Messenger(service)
            isBind = true
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            mService = null
            isBind = false
        }
    }

    // 서비스로 메시지 보내기
    fun sayHello(v: View) {
        if (!isBind) return
        val msg: Message = Message.obtain(null, MSG_SAY_HELLO, 0, 0)
        try {
            mService?.send(msg)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun onStart() { ... }
    override fun onStop() { ... }
    override fun onCreate(savedInstanceState: Bundle?) { ... }
}

액티비티에서 서비스에 메시지를 보내는 방법의 예시 코드이다. 이 코드에서는 응답을 받는 방식을 보여주지 않는다. 서비스가 응답하게 하려면 클라이언트에서 Messaenger 도 생성해야한다. 클라이언트가 onServiceConnected() 콜백을 받으면 send() 함수의 replyTo 매개변수에 클라이언트의Messenger 를 포함하는 서비스에 Meesage 를 전송한다.

외부앱 연동

메신저 방식으로 외부앱의 서비스를 bindService() 함수로 실행하려면 먼저 서비스를 등록한 메니페스트에 외부 앱을 연동할 수 있게끔 intent-filter 가 선언되어 있어야 한다.

<service
         android:name=".MessagerService"
         android:exported="true">
  <intent-filter>
    <action android:name="ACTION_OUTER_SERVICE"/>
  </intent-filter>
</service>

그리고 이 서비스를 bindService() 함수로 실행하는 앱에서는 외부 앱에 접근할 수 있도록 메니페스트에 패키지를 등록해야 한다.

<queries>
  <package android:name="com.example.test_outter" />
</queries>

외부 앱을 인텐트로 실행히키고자 한다면 역시 setPackage()를 해줘야한다.

val intent = Intent("ACTION_OUTER_SERVICE")
intent.setPackage("com.example.test_outter")
bindService(intent, connection, Context.BIND_AUTO_CREATE)

그리고 같은 앱에서는 데이터를 주고 받을때 String 이어도 상관이 없었지만, 외부 앱과 데이터를 주고 받으려면 Parcelable 이나 Bundle 타입이어야 한다. 따라서 아래 코드 처럼 Bundle에 담고 다시 Message 객체에 담아서 전달한다.

val bundle = Bundle()
bundle.putString("data1", "hello1")
bundle.putInt("data2", 10)

val msg = Message()
msg.what = 10
msg.obj = bundle
messenger.send(msg) 

AIDL 사용

Android interface definition language 는 두 프로세스 사이에 데이터를 주고 받는 프로세스간 통신을 구현할 때 사용하는 기법으로, 역시 bindService() 함수를 이용한다. 기본적으로 안드로이드 앱에서 하나의 프로세스에서 다른 프로세스의 메모리에 접근할 수 없다. 따라서 데이터를 시스템에 전달한 후 시스템이 다른 프로세스에 전달해 줘야한다.

그런데 시스템에 전달하는 데이터는 시스템이 해석할 수 있는 원시 타입으로 변환해야하고, 전송하는 데 적합한 형식으로 변환하는 마샬링 과정을 거쳐야한다. AIDL 는 이러한 작업을 대신 처리해주므로 편리하다.

앞서 살펴본 메신저는 이러한 프로세스간 통신을 AIDL보다 더 쉽게 구현할 수 있도록 해준다. 하지만 메신저를 이용하는 방법은 플롯폼에서 제공하는 API를 이용해야하므로 주고 받는 데이터의 종류가 많을 때는 효율이 떨어진다. 또한 메신저는 모든 외부 요청을 싱글 스레드로 처리하지만 AIDL은 여러 오청이 들어오면 멀티 스레드 환경에서 동시에 실행한다.

서비스를 제공하는 앱

AIDL을 이용하려면 우선 확장자가 aidl 인 파일을 만들어야 한다. 그리고 내부에 aidl 확장자 파일을 만들어야 하는데, 여기에는 java 로 인터페이스를 작성해서 넣으면 된다.

package com.example.test

interface MyAIDLInterface {
    void funA(String data);
    int funB();
}

여기에 정의된 함수는 외부 앱에서 AIDL 방식으로 처리하는 작업을 의뢰할 때 호출한다. 함수를 호출할 때 매개변수와 반환값으로 데이터를 외부 앱과 주고 받는다. AIDL 파일에는 외부와 통신하는데 필요한 함수만 저으이되어 있다. 따라서 어디선가 이 함수의 구체적인 로직을 구현해야 하는데, 그 역할을 서비스 컴포넌트가 한다.

class MyAIDLService: Service() {
    overrid fun onBind(intent: Intent): IBinder {
        return object: MyAIDLInterface.Stub() {
            override fun funA(data: String?) {}
            override fun funB(): Int = 10
        }
    }
}

AIDL은 바인드 서비스를 이용하므로, onBind 함수에서 서비스를 ㅇ니텐트로 실행한 곳에 객체로 전달해 줘야 한다. 이때 AIDL 파일을 구현한 객체가 아니라 프로세스 간 통신을 대행해 주는 Stub를 전달한다. 이 Stub 객체는 개발자가 직접 만들지 않고 MyAIDLInterface.Stub() 처럼 AIDL 파일명 뒤에 Stub() 라고 선언만 해주면 된다.

AIDL을 구현한 서비스는 외부 앱과 연동하는 것이 목적이므로 메니페스트 파일에 암시적 인텐트로 실행되게끔 intent-filter 를 추가해야한다.

서비스를 이용하는 앱

메니페스트에서 패키지를 등록해야 한다.

<queries>
  <package android:name="com.example.text"/>
</queries>

AIDL 서비스를 이용하는 앱도 AIDL 서비스를 제공하는 앱에서 만든 AIDL 파일을 가지고 있어야 한다. 그리고 bindService() 함수를 이용해 외부 앱의 서비스를 실행한다.

class MainActivity: AppCompatActivity() {
    lateinit var aidlService: MyAIDLInterface
    
    override fun onCreate(sacedInstanceState: Bundle?) {
        super.onCreate(sacedInstanceState)
        ...
        val intent = Intent("ACTION_AIDL_SERVICE")
        intent.setPackage("com.example.text")
        bindService(intent, connection, Context.BIND_AUTO_CREATE)
    }
    
    val connection: ServiceConnection = object: ServiceConnecetion {
        overrid fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            aidlService=MyAIDLInterface.Stub.asInterface(service)
        }
        overrid fun onServiceDisconnected(name: ComponentName?) {
            Log.d("Info", "Disco")
        }
    }
}

onCreate 시점에 bind 를 했고, aidlService를 인터페이스로 받아왔기 때문에, 연동된 서비스를 이용해야 한다면 aidlService.funA("hello") 이런식으로 호출하기만 하면 된다.

profile
개발자

0개의 댓글