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 ファイルでプロセスを使用して独立したプロセスを開かないようにします。この場合、サービスプロセスはデフォルトでクライアントと同じプロセスに属します。結果は以下の通りです:
異なるプロセス#
サービスを作成する際に、AndroidManifest.xml ファイルでプロセスを使用して独立したプロセスを開くようにします。これは前述の通りです。この場合、サービスプロセスとクライアントプロセスは異なるプロセスに位置します。結果は以下の通りです:
明らかに、サービスとクライアントが異なるプロセスにある場合、つまり一般に言われるプロセス間通信は、具体的には 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 developer");
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 の紹介はこれで終了です。