관리 메뉴

드럼치는 프로그래머

[안드로이드] Android Service 및 AIDL 본문

★─Programing/☆─Android

[안드로이드] Android Service 및 AIDL

드럼치는한동이 2013. 4. 19. 08:57

서비스란 단순히 Background에서만 동작하는 것이 Service가 아니다.

Android에서 Service란 Linux에서 Daemon의 의미인 것이다.

즉, 오랫동한 수행해야될 작업을 처리하기 위한 시작점이 바로 Service이다.

서비스의 형태에는 2가지 형태가 있다.

 

1) Local Service로 구현하는 방법 : Service와 Service를 이용하는 Application이 항상 동일한 Process에서 작동하는 경우에 해당 한다.

이 경우에는 bindService()의 결과로 바로 해당 Service에 접근해서 원하는 API를 호출 할 수 있습니다.

 

2) Remote Service로 구현하는 방법 :

하지만 실제로 Service가 돌고 있는 Process가 아닌 별개의 Process에서 API를 호출하고자 할 때는 반드시 IBinder와 AIDL을 통해서

RPC(Remote Procedure Call)이 이루어 져야 한다.

방법이 어려울것 같에서 Google 최근 원격 호출관련 API를 제공 하였으나 그 내용도 어렵다.

 

본격적으로 Remote Service를 구현해 보자.

 

클라이언트에게 자신의 메서드를 제공하려면 자신의 메서드 목록을 인터페이스로 정의해야 한다.

이는 단순히 메서드의 원형을 선언하는 것과는 수준이 다른데 원격에서 호출되는 메서드는 응용 프로그램의 경계를 넘어서 인수를 전달해야 하는 어려움이 있다. 각 응용 프로그램이 사용하는 메모리가 완전히 분리되어 있어 통상의 방법으로는 인수를 넘기기 어렵다.

즉 우리가 호출하려고하는 service가 존재하는 process의 경우 서로 package가 다를 수 있음을 물론이고 응용 프로그램 수준보다 더 아래쪽의 저수준일 수도 있으며 심지어 자바가 아닌 Native 언어일 수도 있다. 따라서 전달할 수 있는 인수의 종류가 자바 기본 타입Parcelable 정도로 제한되며 그 외에도 몇 가지 제약이 존재 한다.

Android에서는 원격 인터페이스를 직접 정의하기위해 AIDL이라는 별도의 언어를 제공하며 AIDL 컴파일러가 인터페이스 정의를 구현하는 Stub까지 생성해 준다.

AIDL 소스를 작성하여 프로젝트에 포함시켜 놓으면 AIDL 컴파일러가 이 인터페이스를 구현하는 자바 파일을 생성하여 gen 디렉터리에 배치한다.

다음 소스는 ICalc 인터페이스에 최소 공배수를 조사하는 메서드와 소수 여부를 조사하는 매서드의 원형을 선언한다. 간단한 수학적 연산 이지만 서비스를 통해 클라이언트에게 기능을 제공해 보자.

 

ICalc.aidl

package com.eslab.jaynux;

interface ICalc {
int getLCM(in int a, in int b);
boolean isPrime(in int n);


}

 

이 파일을 소스폴더에 저장해 놓으면 AIDL 컴파일러가 인터페이스를 구현하는 자바 파일을 생성해 준다. gen 폴더를 보면 Icalc.java라는 파일이 생성되어 있으며 응용 프로그램 간에 인수를 전달하는 코드가 모두 작성 되어 있다.

이 코드를 보면 우리가 정의한 Interface의 code양에 비해 굉장히 복잡해 보이는 것을 알 수 있다.

gen 폴더에 있는 ICalc.java file이다.

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: C:\\Android\\AndroidLocalRepository\\RemoteServiceExam\\src\\com\\eslab\\jaynux\\Icalc.aidl
 */
package com.eslab.jaynux;

public interface ICalc extends android.os.IInterface {
 /** Local-side IPC implementation stub class. */
 public static abstract class Stub extends android.os.Binder implements
   com.eslab.jaynux.ICalc {
  private static final java.lang.String DESCRIPTOR = "com.eslab.jaynux.ICalc";

  /** Construct the stub at attach it to the interface. */
  public Stub() {
   this.attachInterface(this, DESCRIPTOR);
  }

  /**
   * Cast an IBinder object into an com.eslab.jaynux.ICalc interface,
   * generating a proxy if needed.
   */
  public static com.eslab.jaynux.ICalc asInterface(android.os.IBinder obj) {
   if ((obj == null)) {
    return null;
   }
   android.os.IInterface iin = (android.os.IInterface) obj
     .queryLocalInterface(DESCRIPTOR);
   if (((iin != null) && (iin instanceof com.eslab.jaynux.ICalc))) {
    return ((com.eslab.jaynux.ICalc) iin);
   }
   return new com.eslab.jaynux.ICalc.Stub.Proxy(obj);
  }

  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 {
   switch (code) {
   case INTERFACE_TRANSACTION: {
    reply.writeString(DESCRIPTOR);
    return true;
   }
   case TRANSACTION_getLCM: {
    data.enforceInterface(DESCRIPTOR);
    int _arg0;
    _arg0 = data.readInt();
    int _arg1;
    _arg1 = data.readInt();
    int _result = this.getLCM(_arg0, _arg1);
    reply.writeNoException();
    reply.writeInt(_result);
    return true;
   }
   case TRANSACTION_isPrime: {
    data.enforceInterface(DESCRIPTOR);
    int _arg0;
    _arg0 = data.readInt();
    boolean _result = this.isPrime(_arg0);
    reply.writeNoException();
    reply.writeInt(((_result) ? (1) : (0)));
    return true;
   }
   }
   return super.onTransact(code, data, reply, flags);
  }

  private static class Proxy implements com.eslab.jaynux.ICalc {
   private android.os.IBinder mRemote;

   Proxy(android.os.IBinder remote) {
    mRemote = remote;
   }

   public android.os.IBinder asBinder() {
    return mRemote;
   }

   public java.lang.String getInterfaceDescriptor() {
    return DESCRIPTOR;
   }

   public int getLCM(int a, int b) throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    int _result;
    try {
     _data.writeInterfaceToken(DESCRIPTOR);
     _data.writeInt(a);
     _data.writeInt(b);
     mRemote.transact(Stub.TRANSACTION_getLCM, _data, _reply, 0);
     _reply.readException();
     _result = _reply.readInt();
    } finally {
     _reply.recycle();
     _data.recycle();
    }
    return _result;
   }

   public boolean isPrime(int n) 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.writeInt(n);
     mRemote
       .transact(Stub.TRANSACTION_isPrime, _data, _reply,
         0);
     _reply.readException();
     _result = (0 != _reply.readInt());
    } finally {
     _reply.recycle();
     _data.recycle();
    }
    return _result;
   }
  }

  static final int TRANSACTION_getLCM = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
  static final int TRANSACTION_isPrime = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
 }

 public int getLCM(int a, int b) throws android.os.RemoteException;

 public boolean isPrime(int n) throws android.os.RemoteException;
}
위와 같이 자동으로 생성된 인터페이스 안에는 Stub이라는 추상 클래스가 선언되어 있으며
이 클래스 안에 원격 메서드를 호출하는 코드가 작성되어 있다.
그러나 원격 호출 코드만 작성되어 있을 뿐 실제 연산을 하는 코드는 없으므로 이 부분은 서비스에서 채워 넣어야 한다.
인터페이스를 상속받는 객체를 생성하고 메서드를 구현하여 onBind에서 리턴한다.
간단한 수학 연산을 하므로 구현은 쉽다.
 

CalcService.java

public class CalcService extends Service{


@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
}

@Override
public void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return mBinder; // Service object return
}

ICalc.Stub mBinder = new ICalc.Stub() {

//two method define as AIDL interface
@Override
public boolean isPrime(int n) throws RemoteException {
// TODO Auto-generated method stub

int i;
for(i = 2; i < n; i++){
if(n%i == 0) return false;
}

return true;
}

@Override
public int getLCM(int a, int b) throws RemoteException {
// TODO Auto-generated method stub
int i;
for(i = 1; ; i++){
if(i % a == 0 && i % b ==0) break;
}
return i;
}
};
}

ICalc.Stub 타입의 mBinder 객체를 생성하되 getLCM 메서드와 isPrime 메서드에 코드를 채워 넣었다.

이렇게 생성된 mBinder 객체를 onBind에서 리턴하면 클라이언트는 mBinder를 통해 서비스의 메서드를 호출할 수 있다.

AndroidManifest.xml에는 다음과 같이 등록을 하면 된다.

AndroidManifest.xml

<service android:name="CalcService" android:enabled="true">
<intent-filter>
<action android:name="com.eslab.janux.CALC" />
</intent-filter>
</service>

이렇게 외부에서도 호출 가능하도록 인텐트 필터를 정의해 주면 된다. 이제 모든 준비가 완료 되었으므로 클라이언트에서

이 서비스를 사용할 수 있다.

이제 본격적으로 Remote Service를 이용하는 Code를 작성해 보자. !!

▣ 본격 클라이언트 Soruce 작성

클라이언트 프로그램은 3가지 경우로 나누어 볼 수있다.

1) 같은 package에서 서비스를 요청하는 경우

2) 다른 package에서 서비스를 요청하는 경우

3) 서로 다른 어플리케이션에서 서비스를 요청하는 경우

이 3개중 마지막에 해당하는 서로 다른 어플리케이션에서 서비스를 요청하는 경우에 대해서 다루겠다.

가장 번거로운 작업을 많이 하는 부분이기 때문이다.

클라이언트에서 서비스에 연결하거나 해제할 때는 다음 메서드를 호출하여 바인딩 한다. 바인딩이란 클라이언트와 서비스를

연결하는 동작이다.

boolean bindService (Intent service, ServiceConnection conn, int flags)

void unbindService (ServiceConnection conn)

bindSservice의 사용 예제

첫 번째 인자 설명

1) 같은 package에 있을 때 : 클래스명으로 Intent를 만들어도 괜찮다.

2) 다른 package에 있을 때 : 서비스의 action name으로 Intent를 만들어야 한다.

두 번째 인자 설명

1) ServiceConnection conn은 서비스가 연결, 해제될 때의 동작을 정의하는 연결 객체이다. 따라서 서비스를 사용하는

클라이언트는 ServiceConnection Interface를 구현해야 한다. 그 내용은 2가지로 클라이언트와 서비스가 연결되거나

해제 될 때 자동으로 호출되는 메서드 2가지에 대해서 정의하면 된다.

ServiceConnection Interface 정의 함수

private class RemoteServiceConnection implements ServiceConnection{ @Override public void onServiceConnected(ComponentName name, IBinder service) { Test test = Test.Stub.asInterface(service); try { test.printLog(); } catch (RemoteException e) { } } @Override public void onServiceDisconnected(ComponentName name) {

}

 

▣ 서비스가 연결 될 때 onServiceConnected(ComponentName name, IBinder service)가 실행 된다.

이때 인자로 전달된 IBinder 객체를 정의한인터페이스객체이름.Stub.asInterface(IBinder 객체)

형태로 넘겨 주어야한다.

즉 스텁의 asInterface 메서드로 전달하면 원격지 서비스 객체를 구할 수 있다.

이후 이 객체의 메서드를 호출함으로써 서비스의 기능들을 사용 한다.

서비스는 으용 프로그램 끼리 공유할 수 있으므로 외부 패키지에서도 호출할 수 있다. 서비스를 정의할 때

사용한 aidl파일을 프로젝트에 복사하되 패키지 경로에 맞게 폴더를 구성해야 한다.

- 정리 -

jar 파일이나 AIDL 파일로 서비스를 이용할 App과 서비스를 공급할 App에 각각 추가 시켜서 인터페이스를 등록시켜 준다.

그런다음 서비스를 제공할 App은 정의된 클래스 인터페이스 대로 Stub들을 모두 구현해 주면되고,

서비스를 이용할 놈은 bindService() 다음에 생성되는 IBinder를 Stub클래스의 asInterface의 인자로 집어넣어서

AIDL 인터페이스 클래스를 생성한다음에 사용하고 싶은 API를 호출하면 된다.

재미있는 결과

서비스를 제공하는놈 PID : 100

서비스를 이용하는놈 PID : 200

서비스를 요청해서 처리되는 Job을 LogCat이나 DDMS로 확인해보면 PID : 100으로 동작하는 것을 알 수 있다.

즉 해당 job은 프로세스의 경계를 넘어 섰다는 것이다.

▣ 3가지 구현에서의 차이점

1) 같은 package에서 할경우 : 그냥 쓰면된다.

2) 다른 package의 경우 : package name만 모두 써주면된다.

3) 다른 프로젝트 즉 다른 App일 경우 : AIDL 파일을 복사해서 붙여넣으면 될꺼라고 생각했지만, 절대로 안된다.

왜냐하면 새로운 AIDL로 인식하기 때문이다. 이름이 아무리 똑같에도 소용없다. 따라서 서비스를 제공하는 쪽의 AIDL를

jar file로 만들어서 Import 시켜야한다. 그래야 정상적으로 수행이 가능하다.

AIDL 사용 흐름

 

 

▣ 첨부 파일 설명

RemoteServiceExam : AIDL로 서비스를 제공하는 프로젝트,

또한 다른이름의 Package에서 해당 서비스를 이용하는 Activity가 존재한다.

ServiceClient : RemoteServiceExam 에서 제공하는 서비스를 이용하는 다른 Application이다.

AIDL 정리.ppt : 내가 코딩한 순서를 정리한 것이다.

참고자료

AIDL을 서로 다른 프로젝트에서 쓰기위한 개념적 설명 및 방법 : http://huewu.blog.me/110083320332?Redirect=Log

안드로이드 정복 2 김상형 : Chapter 22 서비스

Comments