Skip to content

Instantly share code, notes, and snippets.

@tigerjj
Last active January 28, 2021 11:16
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tigerjj/9762586 to your computer and use it in GitHub Desktop.
Save tigerjj/9762586 to your computer and use it in GitHub Desktop.
Android RPC example - Client / Server with service

Android RPC example - Client / Server with service

Android의 Binder를 활용하여 RPC(Remote procedure call)을 구현하는 예제를 만들어 보았다. 본 예제에서는 Server/Client가 별도의 Application이 되며 Server의 Service에 Client의 Activity가 Bind되는 구조이다. 그리고 Client는 Server의 Service에 직접 Call을 하여 결과를 받는 예제이다.

RPC를 이해하기 위해서 우선 다음 두가지 개념을 이해해여야 한다.

  • IPC(Inter-process communication)
  • Binder

Server-side programming

다음과 같은 순서로 작업을 진행한다.

  1. aidl 인터페이스 생성 및 빌드
  2. 서비스에 Binder 생성
  3. 인터페이스 구현 우선 aidl을 통해 Client에게 노출되고 호출될 interface를 정의한다.(IRemoteService.aidl 참조) 그리고 빌드를하게 되면 aidl에 정의한 java class가 생긴다!! 이제 그럼 이를 구현할 Service를 만든다.(RemoteServer.java 참조) Service에서 할일은 onBind시 IBinder를 return해주면 끗이다. IBinder는 생성된 java class(본예제에서는 IRemoteService.java 첨부는안했지롱)를 Implement한다. 여기서 Client가 호출하면 할일을 정의하면 된다. 여기까지가 Server쪽 구현내용이다. aidl에 Callback을 등록하는 과정이 있는데 이는 호출한 결과를 Callback 형태로 돌려주기 위함이다. 간단한 작업은 void 대신 return에 정의를 해도된다. 작업이 오래걸리는 작업들은 당연히 callback형태로 해야한다. 이를 위해 별도의 aidl을 만들어 client가 callback 받을 인터페이스를 먼저 정의한다. (IRemoteCallback.aidl 참조) 그리고 여기서 중요한 점이 한가지 있는데... aidl에 정의도니 인터페이스에서 서로 주고받는 데이터는 primitive 형태의 간단한 데이터만 가능하다. 그리고 별도의 aidl interface는 전달이 가능한데 이를 통해 Callback이 가능하다.

Client-side programming

  1. 서비스 Binding 구조 생성 (ServiceConnection)
  2. IBinder 인터페이스를 통한 호출 Client에서는 우선 Remote Service에 Binding할 구조를 만들어야된다.(MainActivity.java 참조) bindService/unbindService > ServiceConnection > IBinder 순으로 구현을 하면된다. 그리고 이를 위해서 Server쪽에서 구현된 jar가 필요하다. 왜냐면 호출할 Interface를 알아야되니까! 여기서 aidl로 정의해서 java class가 빛을 발휘한다. 즉 연결된 서비스가 어떤 Inetface를 노출하였고 RPC를 호출할수 있는지 알수 있게 하는 역할을 한다. 그리고 Server에서 Callback형태로 결과를 받게 했으므로 이 결과를 받기 위해서는 결과를 받는 IRemoteCallback도 구현을 해야한다. 마지막으로 AndroidMenifest.xml에는 Service를 하는데 별도의 process에서 동작하므로 exported 옵션을 추가한다.(android:exported="true")

Parcelable

Parcelable은 IPC 통신시 안드로이드에서 데이터를 주고 받기 위해 필요한 Interface이다. Android의 대부분의 클래스(정확히 176개!!!)는 Parcelable을 implement하였다. Seriablizable과 개념자체는 동일(?)해 보이며 Binder에 최적화 되어있다. (자세한 성능 차이는 Parcelable vs Serializable을 참고하기 바란다.) 본 예제에서도 이를 활용하였는데 Bitmap이 Parcelable을 implement한 클래스이므로 매우 간단하게 그냥 pass할 수 있었다. RPC 인터페이스에서 Custom으로 만든 Class는 Parcelable을 implement 하지 않으면 전달할 수 없다. 이는 다소 귀찮은 작업을 구현해야 하게 하는데 writeToParcel / readToParcel 같은 작업이 필요하다. 그렇기에 Intent/Bundle에 단순한 primitive를 넣을 수 있도록 설계 되어 있으며 이를 활용하는것도 좋은 방법이다.

package com.example.sampleremoteserver;
import android.graphics.Bitmap;
interface IRemoteCallback{
void onRespose(in Bitmap data);
}
package com.example.sampleremoteserver;
import com.example.sampleremoteserver.IRemoteCallback;
interface IRemoteService{
void registerCallback(in IRemoteCallback callback);
void unRegisterCallback(in IRemoteCallback callback);
void createBitmap();
}
package com.example.sampleremoteclient;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.Menu;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.sampleremoteserver.IRemoteCallback;
import com.example.sampleremoteserver.IRemoteService;
public class MainActivity extends Activity {
protected static final String TAG = "ClientActivity";
private IRemoteService mService;
private TextView mTextView;
private ImageView mImageView;
private boolean mIsBound = false;
protected IRemoteCallback mCallback = new IRemoteCallback.Stub() {
@Override
public void onRespose(Bitmap destBitmap) throws RemoteException {
if(destBitmap == null){
mTextView.append("onRespose, passed data is null");
}else{
mTextView.append("onRespose, passed data is not null");
mImageView.setVisibility(View.VISIBLE);
mImageView.setImageBitmap(destBitmap);
}
}
};
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
// this callback will be invoked when remote service is dead
mTextView.setText("unBinded , onServiceDisconnected");
mIsBound = false;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mTextView.setText("Binded , onServiceConnected");
mIsBound= true;
mService = IRemoteService.Stub.asInterface(service);
try {
mService.registerCallback(mCallback);
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
mService.createBitmap();
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.textview);
mImageView = (ImageView) findViewById(R.id.imageview);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
public void onClickedBindService(View view){
Intent intent = new Intent();
intent.setClassName("com.example.sampleremoteserver", "com.example.sampleremoteserver.RemoteServer");
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
public void onClickedUnbindService(View view){
if(mService!=null){
try {
mService.unRegisterCallback(mCallback);
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(mIsBound){
unbindService(mServiceConnection);
mIsBound = false;
}
}
}
package com.example.sampleremoteserver;
import java.util.Random;
import android.app.Service;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
public class RemoteServer extends Service{
private static final String TAG = "RemoteServer";
private RemoteCallbackList<IRemoteCallback> mCallbacks;
private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
@Override
public void registerCallback(IRemoteCallback callback) throws RemoteException {
if(callback!=null){
Log.e(TAG, "registerCallback");
mCallbacks.register(callback);
}
}
@Override
public void unRegisterCallback(IRemoteCallback callback) throws RemoteException {
if(callback!=null){
Log.e(TAG, "unRegisterCallback");
mCallbacks.unregister(callback);
}
}
@Override
public void createBitmap() throws RemoteException {
sendResponse();
}
};
@Override
public IBinder onBind(Intent intent) {
Log.e(TAG, "onBind");
mCallbacks = new RemoteCallbackList<IRemoteCallback>();
return mBinder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "onStartCommand");
return super.onStartCommand(intent, flags, startId);
}
public void sendResponse(){
Log.e(TAG, "sendResponse");
if(mCallbacks!=null){
int length = mCallbacks.beginBroadcast();
for(int i = 0; i < length; i++){
try {
int random = new Random(1000).nextInt();
Log.e(TAG, String.valueOf(random));
Bitmap sourceBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
mCallbacks.getBroadcastItem(i).onRespose(sourceBitmap);
sourceBitmap.recycle();
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment