AIDL(Android インターフェース定義言語)は、クライアントとサーバープロセス間の通信(IPC)のプログラミングインターフェースを定義するために使用できます。Android では、プロセス間でメモリ(ユーザースペース)を共有することができないため、異なるプロセス間の通信は一般的に AIDL を使用して処理されます。
主な流れは、.aidl ファイルで AIDL インターフェースを定義し、それをアプリケーションプロジェクトの src ディレクトリに追加することです。作成が完了したら、再ビルドを行います。Android SDK ツールは、自動的にその .aidl ファイルに基づいて IBinder
インターフェースを生成します。具体的なビジネスオブジェクトがこのインターフェースを実装し、この具体的なビジネスオブジェクトも IBinder
オブジェクトです。サービスをバインドする際には、実際の状況に応じて具体的な通信オブジェクト(ローカルまたはプロキシ)を返します。最後に、クライアントがそのサービスにバインドされ、IBinder
内のメソッドを呼び出してプロセス間通信(IPC)を行うことができます。以下のいくつかの側面から AIDL の使用を学びます:
- .aidl ファイルの作成
- .aidl ファイルから生成されたインターフェースを実装する具体的なビジネスオブジェクト
- クライアントにインターフェースを公開する
- クライアントのリモート呼び出し
- AIDL の検証
.aidl ファイルの作成#
AIDL では、引数と戻り値を持つ 1 つ以上のメソッドを使用してインターフェースを宣言できます。引数と戻り値は任意の型であり、AIDL でサポートされているデータ型は以下の通りです:
- Java の 8 種類のデータ型:byte、short、int、long、float、double、boolean、char
- それに加えて、String、charSequence、List、Map をサポート
- カスタムデータ型
ビジネスメソッドの引数または戻り値の型が List または Map の場合:
List のすべての要素は、AIDL がサポートするデータ型、他の AIDL 生成インターフェース、または自分で宣言したパッケージ可能な型でなければなりません。List を「汎用」クラス(例:List)として使用することもできます。もう一方で実際に受信する具体的なクラスは常に ArrayList ですが、生成されたメソッドは List インターフェースを使用します。
Map のすべての要素は、AIDL がサポートするデータ型、他の AIDL 生成インターフェース、または自分で宣言したパッケージ可能な型でなければなりません。汎用 Map(例:Map<String,Integer> 形式の Map)はサポートされていません。もう一方で実際に受信する具体的なクラスは常に HashMap ですが、生成されたメソッドは Map インターフェースを使用します。
もちろん、AIDL もカスタムデータ型をサポートしており、後で紹介します。
まず、プロジェクトの src ディレクトリに .aidl ファイルを作成します。具体的には以下の図のようになります:
次に、.aidl ファイルに具体的なビジネスメソッドを追加し、ファイル内容は以下の通りです:
// IPersonAidlInterface.aidl
package com.manu.aidldemo;
// Declare any non-default types here with import statements
interface IPersonAidlInterface {
//具体的なビジネス
void setName(String name);
void setAge(int age);
String getInfo();
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
次に、プロジェクトを再ビルドすると、Android SDK ツールが対応するディレクトリに .aidl ファイルと同名の .java インターフェースファイルを生成します。具体的なディレクトリは以下の通りです:
.aidl ファイルから生成されたインターフェースを実装する具体的なビジネスオブジェクト#
前のステップでは AIDL を使用して基本的なビジネス操作を定義しました。再ビルド後、.aidl と同じファイル名の .java ファイルが生成されます。この生成されたインターフェースファイルには、Stub という名前のサブクラスが含まれています。このサブクラスは親インターフェースの抽象実装であり、主に .aidl ファイル内のすべてのメソッドを生成するために使用されます。Stub クラスの具体的な宣言は以下の通りです:
// Stub
public static abstract class Stub extends android.os.Binder implements com.manu.aidldemo.IPersonAidlInterface
明らかに、Stub はローカルインターフェースを実装し、Binder オブジェクトを継承しています。Binder オブジェクトがシステムの底層でサポートされているため、Stub オブジェクトはリモートデータ転送の能力を持っています。Stub オブジェクトを生成する際に asInterface メソッドが呼び出されます。具体的には以下の通りです:
// asInterface
public static com.manu.aidldemo.IPersonAidlInterface asInterface(android.os.IBinder obj){
if ((obj==null)) {
return null;
}
// 检索 Binder 对象是否是本地接口的实现
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.manu.aidldemo.IPersonAidlInterface))) {
return ((com.manu.aidldemo.IPersonAidlInterface)iin);
}
return new com.manu.aidldemo.IPersonAidlInterface.Stub.Proxy(obj);
}
asInterface メソッドは Stub が作成されるときに呼び出され、主な機能は Binder オブジェクトがローカルインターフェースの実装であるかどうかを検出することです。queryLocalInterface () メソッドの戻り値に基づいてプロキシオブジェクトを使用するかどうかを判断します。この検出プロセスはシステムの底層でサポートされるべきです。もし戻り値が null であれば、Stub のプロキシオブジェクトを作成し、そうでなければローカルオブジェクトを使用してデータを転送します。次に、Binder がなぜリモート通信の能力を持っているのかを見てみましょう。Stub が Binder クラスを継承しているため、Binder クラスは以下の通りです:
// Binder
public class Binder implements IBinder {
//...
}
以下は公式サイトによる IBinder インターフェースの説明です:
リモートオブジェクトの基礎インターフェースであり、軽量リモートプロシージャコールメカニズムのコア部分で、高性能なプロセス内およびプロセス間呼び出しを実行するために特別に設計されています。このインターフェースは、リモートオブジェクトと相互作用するための抽象プロトコルを記述します。このインターフェースを直接実装しないでください。Binder から拡張してください。
ここで、Binder が IBinder インターフェースを実装していることがわかります。つまり、Binder はリモート通信の能力を持っています。異なるプロセス間(リモート)で通信する場合、明らかに Stub のプロキシオブジェクトが使用されます。このプロキシクラスには具体的なビジネス処理ロジックが含まれています。以下のメソッドは具体的には以下の通りです:
//具体的なビジネス
@Override
public void setName(java.lang.String name) throws android.os.RemoteException{
// データをシリアライズ
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(name);
// このメソッドは最終的に onTransact メソッドを呼び出します
mRemote.transact(Stub.TRANSACTION_setName, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
ここでは、データをシリアライズし、システムのプロセス間サポートの下で最終的に onTransact () メソッドを呼び出します。以下は onTransact () メソッドの具体的な内容です:
@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_setName:
{
//...
// 最終的に Stub 内のビジネスメソッドを呼び出します
this.setName(_arg0);
//...
}
}
}
明らかに、このメソッドはシステムが開発者にコールバックを行うときに、返される code が定数であることを示しています。プロセス間通信では、各具体的なサービス(メソッド)には対応する番号があり、その番号に基づいて相応のサービス(ビジネス)を実行します。ここで、最終的に実行される具体的なビジネスについて言及しましたが、そのビジネスはどのように具現化されるのでしょうか。上記からわかるように、Stub は抽象クラスであり、提供される具体的なビジネスは必然的に具体的な実装クラスによって完了する必要があります。以下にこの具体的なビジネスクラスを実装します。具体的には以下の通りです:
/**
* Created by jzman
* Powered by 2018/3/8 0008.
*/
public class IPersonImpl extends IPersonAidlInterface.Stub {
private String name;
private int age;
@Override
public void setName(String name) throws RemoteException {
this.name = name;
}
@Override
public void setAge(int age) throws RemoteException {
this.age = age;
}
@Override
public String getInfo() throws RemoteException {
return "私の名前は "+name+"、年齢は "+age+"歳です!";
}
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
}
このクラスは外部に提供される具体的なビジネスクラスであり、そのインスタンスも Binder オブジェクトです。
クライアントにインターフェースを公開する#
具体的なビジネスを外部に提供するために Service を作成します。具体的には以下の通りです:
// Service
public class PersonService extends Service {
public PersonService() {
}
@Override
public IBinder onBind(Intent intent) {
return new IPersonImpl();
}
}
外部が bindService () メソッドを呼び出してサービスをバインドすると、onBind () メソッドが呼び出され、IBinder オブジェクトが返されます。この IBinder オブジェクトも具体的なビジネスオブジェクトであり、ここでの onBind () メソッドが返すのも具体的なビジネスオブジェクトです。両者は統一されています。また、作成した Service は AndroidManifest.xml ファイルに宣言する必要があります。具体的には以下の通りです:
<service
android:name=".PersonService"
android:enabled="true"
android:exported="true"
android:process=":remote">
</service>
ここで process キーワードを使用して、このサービスに独立したプロセスを開くことを示します。remote は任意で、プロセス名を示します。":" は主プロセス(プロセス名はパッケージ名)に新しい名前を追加して新しいプロセスの名前を付けます。例えば、com.manu.study は com.manu.study に変わります。
クライアントのリモート呼び出し#
上記のステップを通じてサービスの構築が完了し、サービスを独立したプロセスで実行しています。次に、クライアントの具体的な呼び出しを行います。具体的な実装は以下の通りです:
// Client
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private IPersonAidlInterface iPersonAidlInterface;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void bindServiceClick(View view) {
Log.i(TAG,"サービスをバインドしています...");
Intent intent = new Intent(this,PersonService.class);
// サービスをバインドする際に自動的にサービスを作成
bindService(intent,conn, Context.BIND_AUTO_CREATE);
}
public void unbindServiceClick(View view) {
Log.i(TAG,"サービスのバインドを解除しています...");
unbindService(conn);
}
public void callRemoteClick(View view) {
Log.i(TAG,"具体的なサービスをリモート呼び出ししています...");
try {
iPersonAidlInterface.setName("Tom");
iPersonAidlInterface.setAge(10);
String info = iPersonAidlInterface.getInfo();
System.out.println("これはリモート呼び出しのサービス情報です:"+info);
} catch (RemoteException e) {
e.printStackTrace();
}
}
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 実際の状況に応じて IBinder のローカルオブジェクトまたはそのプロキシオブジェクトを返す
iPersonAidlInterface = IPersonAidlInterface.Stub.asInterface(service);
System.out.println("具体的なビジネスオブジェクト:"+iPersonAidlInterface);
}
@Override
public void onServiceDisconnected(ComponentName name) {
// サービスが予期せず中断されたときに呼び出されます
}
};
}
上記のコードはクライアントが具体的なサービスを呼び出すプロセスです。
AIDL の検証#
前のステップを通じて、サーバーとクライアントが完成しました。次に、具体的なビジネスを呼び出すことができるかどうかを検証します。ここでは 2 つの状況に分かれます:
同じプロセス#
サービスを作成する際に、AndroidManifest.xml ファイルで process を使用して独立したプロセスを開かないようにします。この場合、サービスプロセスはデフォルトでクライアントと同じプロセスに属します。結果は以下の通りです:
異なるプロセス#
サービスを作成する際に、AndroidManifest.xml ファイルで process を使用して独立したプロセスを開きます。これは前述の通りです。この場合、サービスプロセスとクライアントプロセスは異なるプロセスに位置します。結果は以下の通りです:
明らかに、サービスとクライアントが異なるプロセスにある場合、つまり一般的に言われるプロセス間通信は、具体的には IBinder オブジェクトのプロキシオブジェクトによって完了します。逆に、ローカルオブジェクト、つまり AIDL のビジネスクラスによって生成されたオブジェクトを使用して完了します。
カスタムタイプ#
AIDL でカスタムタイプを使用する方法は以下の通りです:
カスタムタイプの作成#
カスタムタイプはエンティティオブジェクトを転送します。このエンティティクラスは Parcelable インターフェースを実装する必要があります。具体的には以下の通りです:
// カスタムタイプ
public class Work implements Parcelable {
private String title;
private String content;
// getter、setter、Parcelable は省略
}
カスタムタイプの宣言#
作成した .aidl ファイルで先ほど定義したタイプを宣言します。具体的にはビジネス .aidl ファイルとは異なります。具体的には以下の通りです:
// .aidl ファイルでカスタムタイプを宣言
package com.manu.aidldemo;
parcelable Work;
カスタムタイプに関連するビジネスの定義#
具体的なビジネスの .aidl ファイルでカスタムタイプに関連するビジネスを定義します。具体的には以下の通りです:
// Declare any non-default types here with import statements
import com.manu.aidldemo.Work; //注意
interface IPersonAidlInterface {
//カスタムタイプに関連するビジネスを定義
Work getWorkInfo();
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
注意:カスタムタイプをインポートしないと認識されません。
ビジネスエンティティクラスのオーバーライド#
具体的なビジネスが追加されたため、getWorkInfo () を実装する必要があります。具体的には以下の通りです:
//カスタムタイプ関連のビジネスを追加
@Override
public Work getWorkInfo() throws RemoteException {
Work work = new Work();
work.setTitle("AIDLを学ぶ");
work.setContent("Android 開発者");
return work;
}
リモート呼び出し#
サービスをバインドに成功した後、IBinder オブジェクトを取得し、asInterface () を通じて IPersonAidlInterface オブジェクトを取得して呼び出すことができます。具体的には以下の通りです:
public void callRemoteClick(View view) {
Log.i(TAG,"具体的なサービスをリモート呼び出ししています...");
try {
//リモート呼び出し
Work work = iPersonAidlInterface.getWorkInfo();
System.out.println("これはリモート呼び出しのサービス情報です:title="+work.getTitle()+",content="+work.getContent());
} catch (RemoteException e) {
e.printStackTrace();
}
}
ログの実行スクリーンショットは以下の通りです:
AIDL の紹介はこれで終了です。