지난시간까지 하나의 프로세스 안에서 돌아가는 여러 스레드의 통신에 대해서 알아 보았다. 주로 공유메모리(힙)를 이용한 통신이고 동시 접근이나, 스케줄링을 하는 원리에 대해서 배워 보았다. 이번 시간에는 안드로이드에서의 프로세스 간의 통신에 대해 알아보도록 하자.
안드로이드 플랫폼은 바인더(binder) 프레임워크를 통해 프로세스 경계를 넘는 통신, 즉 프로세스 간 통신(IPC)를 지원한다. 바인더 프레임워크는 스레드 사이에 공유하는 메모리 영역이 없을 떄, 데이터 트렌젝션을 관리한다.
IPC는 시그널, 파이프, 메시지 큐, 세마포어, 공유 메모리와 같은 여러 IPC 기술을 지원하는 리눅스 OS에 의해 관리된다. 안드로이드의 IPC는 RPC 메커니즘을 수행하는 바인더 프레임워크로 대체되었다.
RPC 메커니즘의 하부는 다음과 같이 구성된다.
안드로이드 프레임워크와 코어 라이브러리는 바인더 프레임워크와 안드로이드 인터페이스 정의 언어인(AIDL)를 통해 프로세스 통신을 한다.
android.os.Binder 클래스에서 지원하고 원격 인터페이스를 정의하고, 클라이언트의 스레드는 원격 객체를 통해 원격 인터페이스에 접근한다.
함수와 데이터 전송하는 원격 프로시저 호출을 트렌젝션 이라 부르고, 클라이언트 프로세스가 transact메서드를 호출 시 서버 프로세스는 onTransact 메서드로 그 호출을 받는다.
트렌젝션 데이터 전송은 바인더에 최적화된 android.os.Parcel 객체로 구성된다.
IPC는 양방향으로 동작할 수 있으며, 이 메커니즘은 비동기 RPC를 사용하기 위해 중요하다.
바인더는 또한 IBinder.FLAG_ONEWAY를 설정함으로써 비동기 트랜젝션을 지원한다. 이 플래그 설정 시 클라이언트 스레드는 transact를 호출하고 onTransact를 기다리지 않고, 즉시 반환된다.
5.2 AIDL
어떤 프로세스가 다른 프로세스에서 접근할 수 있도록 기능을 노출하고 싶을 때는 통신 계약을 정의해야 한다. 안드로이드 정의 언어(AIDL)의 인터페이스를 .aidl파일에 정의한다.
AIDL파일(.aidl)을 컴파일하면 .java 파일이 생성되고 이것은 모든 클라이언트 응용프로그램과 서버 응용 프로그램에 포함된다. 이 파일은 데이터 마샬링, 언마샬링, 트랜젝션을 다루는 Proxy와 Stub 두 개의 내부 클래스를 정의한다.
프록시와 스텁은 서버 프로세스 안에서 실행되더라도 클라인트언트가 지역적으로 메서드를 호출할 수 있도록 허용하는 두 응용프로그램을 대신하여 RPC를 관리한다. 서버는 스레드 안전할 수 있도록 여러 클라이언트 및 스레드 동시 메서드 호출을 지원해야 한다.
5.2.1 동기식 RPC
원격 메서드 호출이 서버에서 동시에 실행된다더라도, 클라이언트 프로세스의 호출 스레드는 동기식 호출로 되어야 한다.
동기식 RPC 구현 예제
.aidl 파일에 통신계약 인터페이스 정의
interface ISynchronous {
String getThreadNameFast();
String getThreadNameSlow(long sleep);
String getThreadNameBlocking();
String getThreadNameUnblock();
}
서버프로세스 구현
private final ISynchronous.Stub mBinder = new ISynchronous.Stub() {
CountDownLatch mLatch = new CountDownLatch(1);
@Override
public String getThreadNameFast() throws RemoteException {
return Thread.currentThread().getName();
}
@Override
public String getThreadNameSlow(long sleep) throws RemoteException {
SystemClock.sleep(sleep);
return Thread.currentThread().getName();
}
@Override
public String getThreadNameBlocking() throws RemoteException {
try {
mLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return Thread.currentThread().getName();
}
@Override
public String getThreadNameUnblock() throws RemoteException {
mLatch.countDown();
return Thread.currentThread().getName();
}
};
클라이언트에서 Proxy 구현체를 가져오고 원격에서 실행할 메서드 호출
ISynchronous mISynchronous = ISynchronous.Stub.asInterface(mBinder);
String remoteThreadName = null;
try {
remoteThreadName = mISynchronous.getThreadNameFast();
} catch (RemoteException e) {
e.printStackTrace();
}
예제 설명 )
원격으로 수명이 짧은 작업을 호출
getThreadNameFast() : 런타임이 통신을 처리할 수 있는 만큼 빠르게 반환하고 호출하는 클라이언트 스레드를 잠시 차단한다.
원격으로 수명이 긴 작업을 호출
getThreadNameSlow(long sleep) : 반환된 값을 가져올 떄까지 클라이언트 스레드 차단
차단된 메서드 호출
getThreadNameBlocking() : 바인더 스레드 차단도 원격 메서드가 완료될때까지 클라이언트 스레드를 차단.
공유 상태를 이용한 메서드 호출
getThreadNameBlocking과 getThreadNameUnblock은 CountdownLatch를 공유하지만 동시에 여러 스레드의 접근을 보호하진 않는다.
5.2.2 비동기식 RPC
동기식은 이해하고 구현하기 쉽지만, 여러 스레드에서 접근할 경우 스레드를 차단할 수 있는 위험이 있다. 일반적으로 UI스레드는 UI스레드에서 비동기적으로 동작하는 작업자 스레드에서 모든 원격 호출을 실행함으로 피할 수 있지만, 서버 스레드가 차단된 경우 클라이언트 스레드는 모두 차단된다. 이는 메모리 누수의 위험성을 가진다.
이를 해결하기 위해선 비동기식 RPC를 사용해야 한다.
클라이언트 스레드는 비동기식RPC로 실행하고 즉시 반환한다. 바인더는 서버 프로세스로 트랜젝션을 제공한 다음에 클라이언트에서 서버로의 연결을 바로 받는다.
비동기 메서드는 반드시 void를 반환해야 하며, out 또는 input으로 선언된 인수가 없어야 하고 결과값을 얻기 위한 콜백을 구현해야 한다.
비동식 RPC는 oneway 키워드를 붙여 AIDL 안에 정의한다.
예제를 통해서 알아보자.
// 비동기 인터페이스 (모두 비동기)
oneway interface IAsynchronousInterface {
void method1();
void method2();
}
// 비동기 메서드 (mothod1()만 비동기)
interface IAsynchronousInterface {
oneway void method1();
void method2();
}
// 메서드 호출 시 콜백 인터페이스 정의
interface IAsynchronous1 {
oneway void getThreadNameSlow(IAsynchronousCallback callback);
}
// 콜백 인터페이스 정의
interface IAsynchronousCallback {
void handleResult(String name);
}
// 서버에서 인터페이스 구현
// server
IAsynchronous1.Stub mIAynchronous1 = new IAsynchronous1.Stub() {
@Override
public void getThreadNameSlow(IAsynchronousCallback callback) throws RemoteException {
String threadName = Thread.currentThread().getName();
SystemClock.sleep(10000);
callback.handleResult(threadName);
}
};
// 클라이언트에서 콜백으로 받음
// client
private IAsynchronousCallback.Stub mCallback = new IAsynchronousCallback.Stub() {
@Override
public void handleResult(String name) throws RemoteException {
Log.d("SIM" , "remoteThreadName = " + name);
Log.d("SIM" , "currentThreadName = " + Thread.currentThread().getName());
}
};
5.3 바인더를 이용한 메시지 전달
스레드간 통신은 4강에서 배웠다 시피 Message 객체가 스레드 공유 메모리 안에 위치하기 때문에 쉽게 공유가 가능하다. 그러나 프로세스간 공유는 어떤 공유메모리를 갖지 않기 때문에 바인더 프레임워크를 이용하여 프로세스 경계를 넘어 전달되어야 한다.
바인더를 통한 메시지 전달은 android.os.Messenger를 이용하여 Message객체를 보낸다.
이 과정은 다음과 같은 두 단계로 이루어진다.
1. 메신저 참조를 클라이언트 프로세스로 전달
2. 서버 프로세스로 메시지를 송신한다.
5.3.1 단방향 통신
예제. 서비스가 서버 프로세스에서 실행되고 클라이언트 프로세스의 액티비티와 통신한다.
서비스 코드
public class WorkerThreadService extends Service {
WorkerThread mWorkerThread;
Messenger mWorkerMessenger;
@Override
public void onCreate() {
super.onCreate();
mWorkerThread = new WorkerThread();
mWorkerThread.start();
}
private void onWorkerPrepared() {
mWorkerMessenger = new Messenger(mWorkerThread.mWorkerHandler);
synchronized (this) {
notifyAll();
}
}
public WorkerThreadService() {
}
@Override
public IBinder onBind(Intent intent) {
synchronized (this) {
while (mWorkerMessenger == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mWorkerMessenger.getBinder();
}
private class WorkerThread extends Thread {
Handler mWorkerHandler;
@Override
public void run() {
Looper.prepare();
mWorkerHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
//메시지 처리
Log.e("SIM", msg.toString());
}
};
onWorkerPrepared();
Looper.loop();
}
public void quit() {
mWorkerHandler.getLooper().quit();
}
}
}
// 클라이언트
public class MainActivity extends AppCompatActivity {
private boolean mBound = false;
private Messenger mRemoteService = null;
private ServiceConnection mRemoteConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
mRemoteService = new Messenger(iBinder);
mBound = true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mRemoteService = null;
mBound = false;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void onBindClick(View v) {
Intent intent = new Intent(this, WorkerThreadService.class);
bindService(intent, mRemoteConnection, Context.BIND_AUTO_CREATE);
}
public void onUnBindClick(View v) {
if(mBound) {
unbindService(mRemoteConnection);
mBound = false;
}
}
public void onSendClick(View v) {
if (mBound) {
try {
mRemoteService.send(Message.obtain(null, 2, 0, 0));
} catch (RemoteException e) {
}
}
}
}
5.3.2 양방향 통신
// 클라이언트에서 다시 메세지를 받을 수 있도록 replyTo에 핸들러를 정의 함
public void onSendClickTwoWay(View v) {
if (mBound) {
try {
Message msg = Message.obtain(null, 1, 0, 0);
msg.replyTo = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
Log.d("SIM", "Message sent back - msg.what = " + msg.what);
}
});
mRemoteService.send(msg);
} catch (RemoteException e) {
Log.e("SIM", e.getMessage());
}
}
}
// 서버에서는 해당 msg 가 왔을때 replyTo로 send해줌
@Override
public void run() {
Looper.prepare();
mWorkerHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1 :
Log.d("SIM", "Message receive 1");
try {
msg.replyTo.send(Message.obtain(null, msg.what, 0, 0));
} catch (RemoteException e) {
e.getMessage();
}
break;
case 2 :
Log.d("SIM", "Message receive 2");
break;
}
}
};
onWorkerPrepared();
Looper.loop();
}
5.4 마치며
응용프로그램에서 프로세스간 통신은 바인더 저수준 메커니즘으로 RPC, Messenger 통신을 이용하여 데이터를 주고 받는다고 배웠다. 성능을 높이고자 하면 RPC방법으로 구현하기 쉬운 방법으로는 Messanger 를 이용하도록 하자.