본문 바로가기

소프트웨어/안드로이드

[Android] AIDL을 구현해보자!

들어가기 전에

 안드로이드의 각 앱 사이에서 데이터를 주고받으려면 AIDL(Android Interface Definition Language)을 사용하면 됩니다. 최근 middle-ware 역할을 하는 어플리케이션 프로젝트를 진행하였고 여기에서 사용했던 AIDL에 관한 내용을 간단하게 정리해봅니다.

AIDL 레퍼런스 : https://developer.android.com/guide/components/aidl?hl=ko

 

안드로이드 인터페이스 정의 언어(AIDL)  |  Android Developers

AIDL(Android Interface Definition Language)은 전에 다뤄본 다른 IDL과 유사합니다. 클라이언트와 서비스가 모두 동의한 프로그래밍 인터페이스를 정의하여 프로세스 간 통신(IPC)으로 서로 통신하게 할 수 있습니다. Android에서는 일반적으로 한 프로세스가 다른 프로세스의 메모리에 액세스할 수 없습니다. 따라서 객체들을 운영체제가 이해할 수 있는 원시 유형으로 해체하고 해당 경계에 걸쳐 마샬링해야 합니다. 이 마샬링을

developer.android.com

앱 간 데이터 전달은 sendBroadcast / Messenger로도 충분하지 않음?

 일반적인 어플리케이션을 제작할 때에는 한 개의 어플리케이션에 거의 모든 기능이 들어갈 수 있도록 하며 만약 다른 어플리이션에 정보를 전달하고자 할 때에는 일반적으로 sendBroadcast - BroadcastReceiver를 사용하며 일반적인 사용에서는 큰 문제가 없습니다. Messenger라는 친구도 있어서 AIDL보다 더 쉽게 앱 간 데이터 통신을 구현할 수 있습니다.

 

 사실 AIDL은 처음 구현할 때 해야 할 것들이 많기에 쉽게 접근하기 어렵습니다. Service binding, Thread, Handler 등에서 좀 더 심화학습을 거쳐야 AIDL을 자유자재로 사용할 수 있습니다. 이렇게 보면 AIDL은 굳이 사용할 필요가 없어 보이기도 하는데요, stackoverflow에서 누군가가 정리해놓은 내용을 보겠습니다.

BroadcastReceiver
- It is an Asynchronous communication.
- Complexity is low- It is the easiest way to communicate between processes.
- One to All communication- A broadcast is transferring a message to all recipients simultaneously.Android OS intent based communication between application components.
- BroadcastReceiver.onReceive always run in the main thread(UI thread)
- When sending data via an intent, you should be careful to limit the data size to a few KB. Sending too much data can cause the system to throw a TransactionTooLargeException exception. https://developer.android.com/guide/components/activities/parcelables-and-bundles
- The statements that Intents could transfer up to 1Mb worth of data are definitely wrong, 500Kb is more accurate. https://www.neotechsoftware.com/blog/android-intent-size-limit"
- Security: A broadcast is transmitted across the Android OS and that could introduce a security threat. Other apps can listen to broadcasts. Any sensitive data should not be broadcasted.
Messenger:
- Asynchronous communication.
- A reference to a Handler that can be sent to a remote process via an Intent.
- Complexity is medium.
- Messages sent by the remote process via the messenger are delivered to the local handler.When using Messenger, it creates a queue of all the client requests that the Service receives one at a time. All this happens on a single thread.
- In case you want your Service to handle multiple requests simultaneously then you’ll need to make use of AIDL directly and make sure your Service is capable of multi-threading and also ensure thread-safety.
AIDL:
- It is Synchronous and Asynchronous inter process communication. By default, the AIDL communication is synchronous. In order to make AIDL communication asynchronous, use “oneway” keyword.
- Complexity is high - AIDL interface sends simultaneous requests to the service, which must handle multi-threading.
- One to One communication
- Using the underlying Android OS Binder framework
- Requires writing thread-safe code.
- The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all transactions in progress for the process. https://developer.android.com/reference/android/os/TransactionTooLargeException.html"
- Security: AIDL is allows developers to expose their interfaces to other application. Both the client and service agree upon in order to communicate with each other.

출처: https://stackoverflow.com/questions/34384460/what-is-the-difference-between-broadcast-receiver-aidl-and-messenger

 

간단히 정리하자면 다음과 같습니다.

  • BroadcastReceiver : 구현 난이도 쉬움 / 다른 프로세스에서 데이터 획득 가능 / UI Thread에서 동작 -> 일반적인 사용
  • Messenger : 구현 난이도 중간 / 싱글 스레드에서 동작 -> Service - Activity 간 통신에서 사용
  • AIDL : 구현 난이도 어려움 / 멀티 스레드에서 동작 -> 데이터 요청이 자주 있거나 여러 앱에서 요청 및 브로드캐스팅이 필요하나 그 외의 앱들에게 데이터를 공개하고 싶지 않을 때.

필요에 따라 사용방법이 다르니 잘 선택하여 가장 효율적이고 목적에 맞는 방법으로 구현 진행을 하면 됩니다.

 

아무튼 이 포스팅은 AIDL을 알아보도록 한 것이니 AIDL을 어떻게 실제로 구현하는지 알아보도록 합시다.

 

시작하기 전에

 우선 만들기 전에 뭘 만들어야 하는지 확실하게 정하고 시작합시다. 

 

각 어플리케이션은 외부 서버의 사용자 등록 상태나 기타 데이터를 조회해야합니다. 물론 각 어플리케이션에 모든 기능을 각각 구현하여 개발할 수 있지만 개발 중 프로토콜 변화나 기능 추가의 경우 각 어플리케이션에서 모두 작업을 진행해줘야하는 경우가 생기겠죠. 

 

 이런 경우 데이터 관리만 하는 서버 역할의 어플리케이션이 있고 이 어플리케이션에게 데이터 요청이나 상태 확인만 하는 애플리케이션(들)으로, 클라이언트 역할을 하는 어플리케이션들이 굳이 번잡하게 기능 구현을 할 필요 없이 서버 역할 어플리케이션에서만 구현을 하고 서비스 연결 구현만하고 필요한 데이터 호출만 하도록 만들겠습니다. 

 

(※클라이언트는 전체 인터페이스가 아닌 클라이언트에서만 필요한 일부 인터페이스만 가지고 있어도 됩니다)

 

서버 어플리케이션은 다음의 두 가지 기능을 대신 해줄 것 입니다.

  1. 현재 서비스 상태 제공
  2. 서비스 상태 변경 시 클라이언트 측 어플리케이션들에게 알림

 

실제로 사용하는 것은 좀 더 다르겠지만 간단하게 진행해보겠습니다.

 

 

이제 만들어보자

타겟 SDK 버전은 28 (오레오)입니다.

먼저 요청을 받고 데이터를 반환하는 서버 역할을 하는 어플리케이션 구현부터 시작하겠습니다.

 

프로젝트 생성은 별다른 설정 변경없이 Next 연타를 하여 생성하면 되며 Client와 Server 테스트 어플리케이션 화면은 다음과 같이 구성될 것입니다.

Client 어플리케이션의 화면

Client에서는 Update를 눌렀을 때 Server 어플리케이션의 상태를 획득하여 화면에 표시를 해주며 아래의 callback value: 에서는 Client에서 콜백을 수신하였을 때 값이 업데이트 되도록 되어 있습니다.

Server 어플리케이션의 화면

 Server 측 화면에서는 예시를 위해 Client가 Update 버튼을 눌렀을 때 획득하는 true/false 값을 변경할 수 있도록 switch를 하나 넣어두었고 EditText의 숫자를 연결된 Client들에게 전달할 수 있도록 UI 요소를 넣어놓았습니다.

 

 

File > New >  폴더 단위에서 우클릭 후 New > Directory 후 aidl 폴더를 생성합니다.

이름은 안드로이드 스튜디오에서 기본 생성해주는 것으로 만들겠습니다. 마음에 안들면 변경해주세요. 그냥 aidl 폴더 생성 후 aidl 파일을 수작업으로 해줘도 됩니다.

 

위의 두 가지 기능을 구현해주기 위해 MyAidlInterface.aidl을 다음과 같이 편집해줍시다.

// IMyAidlInterface.aidl
package com.tistory.oysu.aidl_server;

import com.tistory.oysu.aidl_server.IServiceStateCallback;

// Declare any non-default types here with import statements

interface IMyAidlInterface {
    boolean isAvailable(); // 서비스 상태 체크
    boolean registerCallback(IServiceStateCallback callback); // 서비스 상태 변경 콜백 등록
    boolean unregisterCallback(IServiceStateCallback callback); // 서비스 상태 변경 콜백 등록 해제
}

그리고 Callback에 등록/해제하는 IServiceStateCallback.aidl도 생성해줍니다.

// IServiceStateCallback.aidl
package com.tistory.oysu.aidl_server;

// Declare any non-default types here with import statements

interface IServiceStateCallback {
    void onServiceStateChanged(int status);
}

AIDL은 기본 데이터만 주고받을 수 있고 Parcleable로 한꺼번에 묶어서 주고받을 수 있으나 글이 더 길어질 수 있기 때문에 boolean, int 데이터만 주고받는 것으로 진행하겠습니다. 이제 AIDL 파일을 생성했습니다. 안드로이드 스튜디오에서는 이 AIDL 파일들로 실제 코드를 자동으로 생성해줍니다. 다음과 같은 모양이 됩니다.

 

 

Build > Make Project를 누르고 별일이 없다면 다음과 같은 코드가 자동 생성됩니다.

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: D:\\AIDL_SAMPLE_ROOT\\AIDL_SAMPLE\\app\\src\\main\\aidl\\com\\tistory\\oysu\\aidl_server\\IMyAidlInterface.aidl
 */
package com.tistory.oysu.aidl_server;
// Declare any non-default types here with import statements

public interface IMyAidlInterface extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.tistory.oysu.aidl_server.IMyAidlInterface
{
private static final java.lang.String DESCRIPTOR = "com.tistory.oysu.aidl_server.IMyAidlInterface";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
 * Cast an IBinder object into an com.tistory.oysu.aidl_server.IMyAidlInterface interface,
 * generating a proxy if needed.
 */
public static com.tistory.oysu.aidl_server.IMyAidlInterface asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.tistory.oysu.aidl_server.IMyAidlInterface))) {
return ((com.tistory.oysu.aidl_server.IMyAidlInterface)iin);
}
return new com.tistory.oysu.aidl_server.IMyAidlInterface.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
java.lang.String descriptor = DESCRIPTOR;
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(descriptor);
return true;
}
case TRANSACTION_isAvailable:
{
data.enforceInterface(descriptor);
boolean _result = this.isAvailable();
reply.writeNoException();
reply.writeInt(((_result)?(1):(0)));
return true;
}
case TRANSACTION_registerCallback:
{
data.enforceInterface(descriptor);
com.tistory.oysu.aidl_server.IServiceStateCallback _arg0;
_arg0 = com.tistory.oysu.aidl_server.IServiceStateCallback.Stub.asInterface(data.readStrongBinder());
boolean _result = this.registerCallback(_arg0);
reply.writeNoException();
reply.writeInt(((_result)?(1):(0)));
return true;
}
case TRANSACTION_unregisterCallback:
{
data.enforceInterface(descriptor);
com.tistory.oysu.aidl_server.IServiceStateCallback _arg0;
_arg0 = com.tistory.oysu.aidl_server.IServiceStateCallback.Stub.asInterface(data.readStrongBinder());
boolean _result = this.unregisterCallback(_arg0);
reply.writeNoException();
reply.writeInt(((_result)?(1):(0)));
return true;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
}
private static class Proxy implements com.tistory.oysu.aidl_server.IMyAidlInterface
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public boolean isAvailable() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
boolean _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_isAvailable, _data, _reply, 0);
_reply.readException();
_result = (0!=_reply.readInt());
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public boolean registerCallback(com.tistory.oysu.aidl_server.IServiceStateCallback callback) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
boolean _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));
mRemote.transact(Stub.TRANSACTION_registerCallback, _data, _reply, 0);
_reply.readException();
_result = (0!=_reply.readInt());
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public boolean unregisterCallback(com.tistory.oysu.aidl_server.IServiceStateCallback callback) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
boolean _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));
mRemote.transact(Stub.TRANSACTION_unregisterCallback, _data, _reply, 0);
_reply.readException();
_result = (0!=_reply.readInt());
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_isAvailable = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_registerCallback = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_unregisterCallback = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
}
public boolean isAvailable() throws android.os.RemoteException;
public boolean registerCallback(com.tistory.oysu.aidl_server.IServiceStateCallback callback) throws android.os.RemoteException;
public boolean unregisterCallback(com.tistory.oysu.aidl_server.IServiceStateCallback callback) throws android.os.RemoteException;
}

AIDL은 인터페이스만을 생성해주기 때문에 실제로 사용을 하기 위해서는 이를 implement해야하므로 IMyAidlInterfaceImpl이라는 클래스로 생성하겠습니다. 여기서는 싱글톤을 위해 object 클래스로 구현하였습니다.

 

참고로 MyData 역시 object 클래스이며 

package com.tistory.oysu.aidl_server

import android.os.RemoteCallbackList
import android.util.Log

object IMyAidlInterfaceImpl : IMyAidlInterface.Stub() {

    private const val TAG = "TAG"
    private val callbacks = RemoteCallbackList<IServiceStateCallback>()

    // isAvailable을 외부에서 호출 시 어떻게 반환해줄지 구현
    override fun isAvailable(): Boolean {
        Log.d(TAG, "isAvailable requested.")
        return MyData.boolState
    }

    // 외부에서 콜백 등록 호출 시 RemoteCallbackList에 추가하도록 구현
    override fun registerCallback(callback: IServiceStateCallback?): Boolean {
        val ret = callbacks.register(callback)
        Log.d(TAG, "registerCallback: $ret")
        return ret
    }

    // 외부에서 콜백 해제 호출 시 RemoteCallbackList에서 제거하도록 구현
    override fun unregisterCallback(callback: IServiceStateCallback?): Boolean {
        val ret = callbacks.unregister(callback)
        Log.d(TAG, "unregisterCallback: $ret")
        return ret
    }

    // aidl에는 구현되어있지 않지만 Server app에서 clients들에게 콜백을 호출 하기 위한 함수.
    fun broadcastToCurrentStateToClients(status: Int) {
        val n = callbacks.beginBroadcast()
        Log.d(TAG, "broadcast size:$n")

        for (i in 0 until n) {
            callbacks.getBroadcastItem(i)?.onServiceStateChangedWithCode(status)
        }
        callbacks.finishBroadcast()
    }
}

 

이제 외부 어플리케이션이 서버 어플리케이션에 연결할 때 바인더를 리턴해줄 서비스 클래스는 다음과 같이 간단하게 작성하였습니다.

package com.tistory.oysu.aidl_server

import android.app.Service
import android.content.Intent
import android.os.IBinder
import android.util.Log


class MyService : Service() {

    companion object {
        private const val TAG = "Server app service"
    }

    // 바인더 객체
    private var iMyAidlInterface: IMyAidlInterfaceImpl? = null

    override fun onCreate() {
        super.onCreate()
        iMyAidlInterface = IMyAidlInterfaceImpl // 서비스 생성 시 바인더 객체 생성
        Log.d(TAG, "onCreate")
    }
    
    // 외부에서 서비스 바인딩 시 바인딩 리턴
    override fun onBind(intent: Intent?): IBinder? {
        return iMyAidlInterface
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "onDestroy")
    }

}

AndroidManifest에 등록을 하며 외부에서 접속을 할 때 사용할 action 을 IntentFilter에 명시해줍니다.

<service
        android:name=".MyService">
    <intent-filter>
        <action android:name="oysu.server.service"/>
    </intent-filter>
</service>

이제 테스트를 위한 UI 구현을 합니다. 

간단하게 값 저장을 위해 SharedPreference를 사용하였습니다. broadcast를 위한 버튼과 boolean 값을 수정하기 위해 switch를 달아두었습니다.

package com.tistory.oysu.aidl_server

import android.app.Activity
import android.content.SharedPreferences
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.widget.CompoundButton
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*

const val KEY_SERVICE_STATE = "service_state_boolean"

class MainActivity : Activity(), CompoundButton.OnCheckedChangeListener {

    private lateinit var pref: SharedPreferences

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

        // get SharedPreference
        pref = PreferenceManager.getDefaultSharedPreferences(this)

        btnBroadcast.setOnClickListener {
            // It will broadcast current state to Clients after 10 seconds.
            Handler().postDelayed({
                val text = etCode.text
                if (text.isNullOrEmpty()) {
                    Toast.makeText(this@MainActivity, "Text is null!", Toast.LENGTH_SHORT).show()
                } else {
                    IMyAidlInterfaceImpl.broadcastToCurrentStateToClients(text.toString().toInt())
                }
            }, 10 * 1000)
        }

        // init bool value to MyData
        MyData.boolState = getServiceState()

        // init switch UI.
        swSwitch.isChecked = getServiceState()
        // add listener
        swSwitch.setOnCheckedChangeListener(this)
    }

    // saves bool value when switch state changes
    override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
        saveServiceState(isChecked)
    }

    // save bool value in shared preference.
    private fun saveServiceState(state: Boolean) {
        MyData.boolState = state
        pref.edit().putBoolean(KEY_SERVICE_STATE, state).apply()
    }

    // get bool value from shared preference.
    private fun getServiceState(): Boolean {
        return pref.getBoolean(KEY_SERVICE_STATE, false)
    }
}

 

이제 Client쪽을 구현합니다. aidl 파일은 Server 어플리케이션을 생성했을때와 마찬가지로 동일하게 구성해줍니다.

 

Client는 Activity에 다 때려박아서 한 파일로 정리가 되어있습니다. 실제 프로젝트 진행시에는 이렇게 하지 않으시길 바랍니다 ^^;

package com.tistory.oysu.aidl_sample_client;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import com.tistory.oysu.aidl_server.IMyAidlInterface;
import com.tistory.oysu.aidl_server.IServiceStateCallback;

public class MainActivity extends Activity {

    private static String TAG = "RemoteMainActivity";

    private TextView tvState, tvStateFromCallback;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btnUpdate).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mRemoteService!=null) {
                    try {
                        // update bool value from Server application
                        tvState.setText(String.valueOf(mRemoteService.isAvailable()));
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                } else {
                    Toast.makeText(MainActivity.this, "Service not connected..", Toast.LENGTH_SHORT).show();
                }
            }
        });

        tvState = findViewById(R.id.tvState);
        tvStateFromCallback = findViewById(R.id.tvStateFromCallback);
        connectService(); // connect service at onCreate
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        disconnectService(); // disconnect service at onDestroy
    }

    private void connectService() {
        Intent i = new Intent();
        i.setAction("oysu.server.service"); // action declared in Server application
        i.setPackage("com.tistory.oysu.aidl_server");
        bindService(i, mConnection, BIND_AUTO_CREATE);
    }

    private void disconnectService(){
        unRegisterCallback();
        unbindService(mConnection);
    }

    private void registerCallback() {
        if(mRemoteService!=null){
            try {
                mRemoteService.registerCallback(mCallback);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }

    private void unRegisterCallback(){
        if(mRemoteService!=null){
            try {
                mRemoteService.unregisterCallback(mCallback);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }

    private IMyAidlInterface mRemoteService;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "onServiceConnected");
            mRemoteService = IMyAidlInterface.Stub.asInterface(service);
            registerCallback();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG, "onServiceDisconnected");
            mRemoteService = null;
        }
    };

    private IServiceStateCallback mCallback = new IServiceStateCallback.Stub() {
        @Override
        public void onServiceStateChangedWithCode(final int status) {
            runOnUiThread(new Runnable() { // it comes through non-UI thread, so it need to post it to the Main thread!
                @Override
                public void run() {
                    tvStateFromCallback.setText(String.valueOf(status));
                }
            });
        }
    };
}

서비스 연결 시 action 과 package를 반드시 주의하여 넣어주기 바랍니다. 이 두개가 틀리다면 서비스 바인딩이 제대로 되지 않습니다.

 

제대로 구현이 되었다면 아래처럼 동작하게 됩니다.

Client에서는 Server의 boolean 상태값을 얻을 수 있고(첫번째) Server에서 broadcast한 사항을 전달 받을 수 있습니다(두번째)

 

Update
Callback

 

 

전체 소스는 아래 링크를 참조하세요~

https://gitlab.com/iroiroys/aidl-test-sample