Android 서비스 AIDL 예제

Android에서 AIDL을 사용하여 예제 서비스를 구현해 보았다.

GitHub에서 관련 예제를 몇 개 찾을 수 있었지만, 모두 안드로이드 스튜디오에서 빌드하면 잘 되었으나, AOSP 환경에서 빌드하면 실패하였다. 그래서 직접 안드로이드 스튜디오와 AOSP에서 모두 빌드가 잘 되도록 구성해 보았고 기록을 남긴다.

Android service

Android service는 Activity, Broadcast, Contents Provider와 함께 안드로이드 4대 컴포넌트 중 하나로 자세한 사항은 [서비스 정보] 페이지를 참조한다.

서비스 bind 종류

  • Unbounded service
    • App 컴포넌트가 startService()를 호출하면, 서비스는 실행되고(started) 그 서비스를 실행한 컴포넌트가 종료되어도 할 일을 모두 마칠 때까지 서비스는 종료되지 않는다.
    • 따라서 서비스가 할일을 다하여 종료시키고 싶다면, 서비스 내에서 stopSelf()를 호출하여 스스로 종료되도록 하거나, 다른 컴포넌트에서 stopService()를 호출하여 종료시켜야 한다.
    • 액티비티 등의 앱 컴포넌트는 startService()를 호출함으로써 서비스를 실행할 수 있고, 이때 어떤 서비스를 실행할지에 대한 정보와 그 외에 서비스에 전달해야할 데이터를 담고 있는 인텐트를 인자로 넘길 수 있다. 그리고 서비스의 onStartCommand() 콜백 메소드에서 매개변수로 그 인텐트를 받게 된다.
  • Bounded service
    • App 컴포넌트가 startService() 메소드 대신에 bindService() 메소드를 호출하면 서비스가 시작되고 바인딩된다. (이를 Service Bind 혹은 Bound Service라고 함)
    • 바인드된 서비스는 구성요소가 서비스와 상호작용하고, 요청을 보내고, 결과를 수신하고, 프로세스 간 통신(IPC)을 통해 프로세스 전반에서 이러한 작업을 할 수 있는 클라이언트-서버 인터페이스를 제공한다.
    • 이는 마치 클라이언트-서버와 같이 동작을 하는데 서비스가 서버 역할을 한다. (액티비티는 서비스에게 어떠한 요청을 할 수 있고, 서비스로부터 요청에 대한 결과를 받을 수 있음)
    • 하나의 서비스에 다수의 액티비티 연결이 가능하며, 서비스 바인딩은 연결된 액티비티가 사라지면 서비스도 소멸된다. (즉 백그라운드에서 무한히 실행되지는 않음)
    • 클라이언트가 서비스와의 접속을 마치려면 unbindService()를 호출한다.

서비스 구현 방법

Service 구현은 다음 3가지 방법 중의 하나로 구현할 수 있다.

  • Binder 클래스 확장: 공개 메소드에 직접 액세스하는 방법으로, 서비스가 애플리케이션을 위해 단순히 백그라운드에서 작동하는 요소에 그치는 경우 선호된다.
  • Messenger: 프로세스 간 통신(IPC)을 실행하는 가장 간단한 방법으로 인터페이스가 여러 프로세스에서 작동해야 하는 경우에 선호된다. 메신저는 내부적으로 AIDL을 이용하여 구현되었고, 쉽게 사용할 수 있는 Messenger 클래스를 제공한다. 메신저는 하나의 쓰레드에서 모든 클라이언트들의 요청을 처리하므로 간단한 메시징에는 적합하지만, 여러 클라이언트가 동시에 복잡한 데이터를 처리해야 할 경우에는 적합하지 않다.
  • AIDL: AIDL 파일을 이용한다. AIDL은 복잡한 IPC 요구 사항을 처리할 수 있는 강력한 도구로, 양방향 통신, 다중 클라이언트 처리, 대량의 데이터 전달이 가능하며, 서비스와 클라이언트 간에 명확한 인터페이스 정의를 제공한다. 특히, 커스텀 객체를 포함한 다양한 데이터 구조를 처리할 수 있어 유연성이 높다.

서비스의 생명 주기

각각 unbounded 서비스와 bounded 서비스의 생명 주기는 아래 다이어그램과 같다.

내 Android 서비스 예제

AIDL을 이용하여 Android bounded 서비스의 server와 client 앱을 아래와 같이 구현하였다.
서비스가 제공하는 함수는 랜덤 넘버를 얻는 함수와, 콜백 함수를 등록/해제하는 함수이다.
참고로 전체 소스는 https://github.com/yrpark99/MyServiceAidlExample.git 페이지에서 확인할 수 있다.

서비스 server 단 (MyServiceServer)

아래와 같이 apps/MyServiceServer/app/src/main/aidl/com/my/myserviceserver/IMyRemoteService.aidl 파일을 작성하였다.

package com.my.myserviceserver;

import com.my.myserviceserver.IMyRemoteServiceCallback;

interface IMyRemoteService {
    int getRandomNumber();
    boolean registerCallback(IMyRemoteServiceCallback callback);
    boolean unregisterCallback(IMyRemoteServiceCallback callback);

또, apps/MyServiceServer/app/src/main/aidl/com/my/myserviceserver/IMyRemoteServiceCallback.aidl 파일을 아래와 같이 작성하였다.

package com.my.myserviceserver;

interface IMyRemoteServiceCallback {
    void onMyServiceStateChanged(int state);
}


이 AIDL 파일은 AOSP에 포함된 aidl 툴로 수동으로 Java 파일로 변환할 수는 있으나, 자동으로 빌드되는 방법이 좀 더 좋으므로, 나는 자동으로 빌드되는 방법을 사용하였다. 이를 위해서는 안드로이드 스튜디오와 AOSP 빌드 환경에서 필요한 설정이 다른데, 나는 두 환경 모두 지원하도록 하였다.
안드로이드 스튜디오를 위해서는 app/build.gradle.kts 파일에서 아래와 같이 추가되어야 안드로이드 스튜디오에서 메뉴 File → New → AIDL로 aild 파일을 추가할 수 있다. (이후 빌드하면 자동으로 java 파일이 생성됨)

android {
    buildFeatures {
        aidl = true
    }
}

또, AOSP 빌드를 위해서 Android.bp 파일에 아래 내용을 추가하였다. (이 방법을 알아내느라 고생 좀 했다. 😓)

java_library {
    name: "myserviceserver-aidl",
    srcs: [
        "app/src/main/aidl/com/my/myserviceserver/IMyRemoteServiceCallback.aidl",
        "app/src/main/aidl/com/my/myserviceserver/IMyRemoteService.aidl",
    ],
    aidl: {
        local_include_dirs: ["app/src/main/aidl"],
        export_include_dirs: ["app/src/main/aidl"],
    },
    sdk_version: "current",
}

android_app {
    static_libs: [
        "myserviceserver-aidl",
    ],
}


AndroidManifest.xml 파일에서는 application 밑에 아래 내용을 추가하여 서비스를 export 시켰다.

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


이후 app/src/main/java/com/my/myserviceserver/MyRemoteService.java 파일을 다음과 같이 구현하였다. (getRandomNumber() 함수 구현, 3초마다 등록된 콜백 함수를 서비스 state(테스트로 1부터 1씩 증가) 값을 넘겨서 호출함)

package com.my.myserviceserver;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;

import java.util.Random;

public class MyRemoteService extends Service {
    private final String TAG = "MyServiceServer";
    private final Random mGenerator = new Random();
    private final RemoteCallbackList<IMyRemoteServiceCallback> callbacks = new RemoteCallbackList<>();
    private final Handler handler = new Handler(Looper.getMainLooper());
    private boolean isRunning = false;
    private int state = 1;

    private final Runnable periodicTask = new Runnable() {
        @Override
        public void run() {
            if (isRunning) {
                Log.i(TAG, "Call callback function with state: " + state);
                try {
                    callRegisteredCallback(state);
                } catch (RemoteException e) {
                    Log.e(TAG, "RemoteException", e);
                }
                state++;
                handler.postDelayed(this, 3000);
            }
        }
    };

    public IMyRemoteService.Stub binder = new IMyRemoteService.Stub() {
        @Override
        public int getRandomNumber() {
            int randomNum = mGenerator.nextInt(1000);
            Log.i(TAG, "getRandomNumber() return randomNum: " + randomNum);
            return randomNum;
        }

        @Override
        public boolean registerCallback(IMyRemoteServiceCallback callback) {
            boolean ret = callbacks.register(callback);
            Log.d(TAG, "registerCallback: " + ret);
            isRunning = true;
            handler.post(periodicTask);
            return ret;
        }

        @Override
        public boolean unregisterCallback(IMyRemoteServiceCallback callback) {
            boolean ret = callbacks.unregister(callback);
            Log.d(TAG, "unregisterCallback: " + ret);
            isRunning = false;
            handler.removeCallbacks(periodicTask);
            return ret;
        }
    };

    public void callRegisteredCallback(int state) throws RemoteException {
        Log.d(TAG, "callRegisteredCallback() state:" + state);
        int num = callbacks.beginBroadcast();
        for (int i = 0; i < num; i++) {
            IMyRemoteServiceCallback item = callbacks.getBroadcastItem(i);
            if (item != null) {
                Log.i(TAG, "Call onMyServiceStateChanged() with state: " + state);
                item.onMyServiceStateChanged(state);
            }
        }
        callbacks.finishBroadcast();
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind()");
        return binder;
    }
}

서비스 client 단 (MyServiceClient)

Server 단에서 작성한 IMyRemoteService.aidl, IMyRemoteServiceCallback.aidl 파일을 복사하였고, app/build.gradle.kts 파일과 Android.bp 파일도 동일하게 구성하였다.
AndroidManifest.xml 파일에서는 아래 내용을 추가하여 사용할 서비스를 query 할 수 있도록 하였다.

<queries>
    <package android:name="com.my.myserviceserver"/>
</queries>


이후 app/src/main/java/com/my/myserviceclient/MainActivity.java 파일을 다음과 같이 작성하였다.

package com.my.myserviceclient;

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.Button;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

import com.my.myserviceserver.IMyRemoteService;
import com.my.myserviceserver.IMyRemoteServiceCallback;

public class MainActivity extends AppCompatActivity {
    private final String TAG = "MyServiceClient";
    private IMyRemoteService mRemoteService = null;
    private Button btnRandomNumber;
    private TextView tvRandomNumber, tvStateFromCallback;

    /** Callbacks for service binding */
    private final ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName className, IBinder service) {
            Log.i(TAG, "onServiceConnected() Service is bounded");
            mRemoteService = IMyRemoteService.Stub.asInterface(service);
            registerCallback();
        }

        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            Log.i(TAG, "onServiceDisconnected() Service is unbounded");
            mRemoteService = null;
        }
    };

    /* Callbacks for service callback */
    private IMyRemoteServiceCallback mCallback = new IMyRemoteServiceCallback.Stub() {
        @Override
        public void onMyServiceStateChanged(int state) {
            Log.i(TAG, "onMyServiceStateChanged() state: " + state);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    tvStateFromCallback.setText("Callback state: " + String.valueOf(state));
                }
            });
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "onCreate()");
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnRandomNumber = findViewById(R.id.btnRandomNumber);
        tvRandomNumber = findViewById(R.id.tvRandomNumber);
        tvStateFromCallback = findViewById(R.id.tvStateFromCallback);
    }

    @Override
    protected void onStart() {
        Log.i(TAG, "onStart()");
        super.onStart();
        bindToMyRemoteService();
    }

    /* Bind to service: Create Intent with service name defined in server app manifest */
    private void bindToMyRemoteService() {
        Log.i(TAG, "bindToMyRemoteService()");
        Intent intent = new Intent("MyRemoteService");
        intent.setPackage("com.my.myserviceserver");
        bindService(intent, serviceConnection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onResume() {
        super.onResume();
    }

    public void onRandomNumberButtonClick(View view) {
        if (mRemoteService == null) {
            Log.i(TAG, "Service is not bounded yet");
            return;
        }
        try {
            // Call service for a random number
            int randomNum = mRemoteService.getRandomNumber();
            Log.i(TAG, "Got randomNum: " + randomNum);
            tvRandomNumber.setText(String.valueOf(randomNum));
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException: " + e.getMessage());
        }
    }

    @Override
    protected void onStop() {
        Log.i(TAG, "onStop() Unbind service");
        super.onStop();
        unRegisterCallback();
        unbindService(serviceConnection);
        mRemoteService = null;
    }

    private void registerCallback() {
        if (mRemoteService == null) {
            return;
        }
        try {
            mRemoteService.registerCallback(mCallback);
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException: " + e.getMessage());
        }
    }

    private void unRegisterCallback() {
        if (mRemoteService == null) {
            return;
        }
        try {
            mRemoteService.unregisterCallback(mCallback);
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException: " + e.getMessage());
        }
    }
}

위 소스는 대략 다음과 같은 작업을 한다.

  • 시작시 bindToMyRemoteService() 함수에서 bindService()를 호출하여 서비스를 bind 시킨다.
  • 서비스가 연결되면 onServiceConnected() 함수가 호출되고, 여기서 콜백 함수를 등록한다.
  • 콜백 함수는 서비스에 의해 3초 간격으로 호출되며, 아규먼트로 받은 서비스 state 값을 화면에 표시한다.
  • 랜덤 버튼 클릭시마다 서비스가 제공하는 랜덤 넘버를 얻어서 화면에 표시한다.

실행 및 결과

MyServiceClient 앱을 실행시키거나 콘솔로 아래와 같이 실행시켠 된다. (사전에 MyServiceServer 앱을 실행시키지 않아도 됨)

$ am start -n com.my.myserviceclient/.MainActivity

기대대로 랜덤 버튼을 누르면 랜덤값이 출력되고, 3초마다 콜백 state 값이 출력되는 것을 확인할 수 있다.
또한 logcat을 확인해 보면, 아래와 같이 기대대로 로그가 출력되는 것을 확인할 수 있다.

15:20:27.541  3995  3995 I MyServiceClient: onCreate()
15:20:27.593  3995  3995 I MyServiceClient: onStart()
15:20:27.593  3995  3995 I MyServiceClient: bindToMyRemoteService()
15:20:27.737  4015  4015 I MyServiceServer: onBind()
15:20:27.739  3995  3995 I MyServiceClient: onServiceConnected() Service is bounded
15:20:27.740  4015  4035 D MyServiceServer: registerCallback: true
15:20:27.744  4015  4015 I MyServiceServer: Call callback function with state: 1
15:20:27.744  4015  4015 D MyServiceServer: callRegisteredCallback() state:1
15:20:27.744  4015  4015 I MyServiceServer: Call onMyServiceStateChanged() with state: 1
15:20:27.744  3995  4007 I MyServiceClient: onMyServiceStateChanged() state: 1
15:20:30.532  4015  4035 I MyServiceServer: getRandomNumber() return randomNum: 583
15:20:30.532  3995  3995 I MyServiceClient: Got randomNum: 583
15:20:30.752  4015  4015 I MyServiceServer: Call callback function with state: 2
15:20:30.752  4015  4015 D MyServiceServer: callRegisteredCallback() state:2
15:20:30.752  4015  4015 I MyServiceServer: Call onMyServiceStateChanged() with state: 2
15:20:30.752  3995  4034 I MyServiceClient: onMyServiceStateChanged() state: 2
15:20:32.333  4015  4035 I MyServiceServer: getRandomNumber() return randomNum: 497
15:20:32.333  3995  3995 I MyServiceClient: Got randomNum: 497
15:20:33.756  4015  4015 I MyServiceServer: Call callback function with state: 3
15:20:33.756  4015  4015 D MyServiceServer: callRegisteredCallback() state:3
15:20:33.756  4015  4015 I MyServiceServer: Call onMyServiceStateChanged() with state: 3
15:20:33.756  3995  4136 I MyServiceClient: onMyServiceStateChanged() state: 3
15:20:35.632  4015  4035 I MyServiceServer: getRandomNumber() return randomNum: 424
15:20:35.632  3995  3995 I MyServiceClient: Got randomNum: 424
15:20:36.760  4015  4015 I MyServiceServer: Call callback function with state: 4
15:20:36.760  4015  4015 D MyServiceServer: callRegisteredCallback() state:4
15:20:36.760  4015  4015 I MyServiceServer: Call onMyServiceStateChanged() with state: 4
15:20:36.760  3995  4008 I MyServiceClient: onMyServiceStateChanged() state: 4
15:20:39.764  4015  4015 I MyServiceServer: Call callback function with state: 5
15:20:39.764  4015  4015 D MyServiceServer: callRegisteredCallback() state:5
15:20:39.764  4015  4015 I MyServiceServer: Call onMyServiceStateChanged() with state: 5
15:20:39.764  3995  4008 I MyServiceClient: onMyServiceStateChanged() state: 5
15:20:41.286  3995  3995 I MyServiceClient: onStop() Unbind service
15:20:41.287  4015  4027 D MyServiceServer: unregisterCallback: true

카테고리:

업데이트: