1.AIDL文本解释
在软件工程中,接口定义语言(IDL)已经成为通用术语,是用来描述软件组件接口的特定语言。在Android中,该IDL被称为Android接口定义语言(AIDL),它是纯文本文件,使用Java类似语法编写。但是,编写Java接口的编写AIDL文件还有有些不同的。
首先,对所有的非原始类型参数,需要指定如下三种类型方向指示符之一:in,out,inout。in类型方向指示符只用于输入,客户端不会看到Service对对象的修改。out类型表明输入对象不包含相关的数据,但会由Service生成相关的数据。inout类型是上面两种类型的结合。切记只使用需要的类型,因为每种类型都有相应的消耗。
另一个需要记住的是,所有用于通信的自定义类都需要创建一个AIDL文件,是用来声明该类实现了Parcelable接口。
2.Parcel
Binder事务通常会传递事务数据。这种数据被称为parcel(包裹)。Android提供了相应的API。允许开发者为大多数Java对象创建parcel。
Android中的parcel和JAVA SE中的序列化对象类似。不同之处在于,开发者需要使用parcelable实现对象的编解码工作。该接口定义了两个编写Parcel对象的方法,以及一个静态的不可被复写的Creator对象,该对象用来从Parcel中读取相应的对象。如下所示:
public class CustomData implements Parcelable { public static final Creator<CustomData> CREATOR=new Creator<CustomData>(){ @Override public CustomData createFromParcel(Parcel source) { CustomData customData=new CustomData(); customData.mName=source.readString(); customData.mReferences=new ArrayList<>(); source.readStringList(customData.mReferences); customData.mCreated=new Date(source.readLong()); return customData; } @Override public CustomData[] newArray(int size) { return new CustomData[size]; } }; private String mName; private List<String> mReferences; private Date mCreated; public CustomData(){ this.mName="";//默认为空字符串 this.mReferences=new ArrayList<String>(); this.mCreated=new Date();//默认为当前时间 } public Date getmCreated() { return mCreated; } public List<String> getmReferences() { return mReferences; } public String getmName() { return mName; } public void setmName(String mName) { this.mName = mName; } public void setmReferences(List<String> mReferences) { this.mReferences = mReferences; } public void setmCreated(Date mCreated) { this.mCreated = mCreated; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(this.mName); dest.writeStringList(this.mReferences); dest.writeLong(this.mCreated.getTime()); } @Override public boolean equals(Object o) { if(this==o)return true; if(o==null || getClass()!=o.getClass())return false; CustomData that=(CustomData)o; return this.mCreated.equals(that.mCreated) && this.mName.equals(that.mName); } @Override public int hashCode() { int result=this.mName.hashCode(); result=31*result+this.mCreated.hashCode(); return result; } }
前面的代码显示了实现Parcelable接口的CustomData类。注意CREATOR成员对象的实现,以及createFromParcel()如何使用Parcel.readStringList()方法来读取整个List对象,而不需要指定列表的长度,Parcel对象内部会处理这种情况。
实现该接口后就可以通过Binder IPC在应用间发送该类的对象了。
3.Binder简介
Binder IPC通信示意图:
Binder通讯遵循客户端-服务器模式。客户端使用客户端代理来处理与内核驱动程序的通信。在服务器端,Binder框架维护了一系列Binder线程。内核驱动会使用服务器端的Binder线程把消息从客户端代理分发给接受对象。这一点需要特别注意,因为当通过Binder接受Service调用时,它们并不会运行在应用程序的主线程上。这样一来,客户端到远程Service的连接就不会阻塞应用的主线程。
开发者使用Binder基类以及IBinder实现Binder机制。Service组件的Service.onBind()方法会返回实现IBinder接口的类。当Service发布远程API时,开发者通常使用AIDL文件生成IBinder类。
使用Binder通信时,客户端需要知道远程Binder对象的地址。然而,Binder的设计要求只有实现类,比如要调用的Service才知道地址。开发者使用Intent解析来进行Binder寻址。客户端使用action字符串或者组件名来构建Intent对象,然而使用它初始化与远程应用程序的通信,然而,Intent只是实际Binder地址的抽象描述,为了能够建立通信,还需要翻译成实际的地址。
ServiceManager是一个特殊的Binder节点,它巡行在Android系统服务内,管理所有的地址解析,是唯一一个有全局地址的Binder节点。因为所有的Android组件都使用Binder进行通信,它们需要使用ServiceManager进行注册,通过如上所述的地址来进行通信。
客户端想要和Service或者其他组件通信,需要隐式地通过Intent查询ServiceManager来接受Binder地址。
4. 完整是实例
AIDL设计到的知识都在前面做了详细的介绍,下面将跑一跑代码验证其可行性。
下面的代码片段是一个名为CustomData.aidl的AIDL文件示例。它应该和Java元代码文件放在同一个包里。
package com.example.liyuanjing.myapplication; parcelable CustomData;
然后需要在AIDL文件中引入所有需要自定义的类,如下所示:
package com.example.liyuanjing.myapplication; import com.example.liyuanjing.myapplication.CustomData; interface ApiInterfaceV1 { boolean isPrime(long value);//用于检查数字是否为素数的简单远程方法 void getAllDataSince(long timestamp, out CustomData[] result);//检索timestamp以后的所有CustomData对象,至多获取result.length个对象 void storeData(in CustomData data);//存储CustomData对象 }
写完上面的代码,忘记提醒开发者,原始类型参数不需要方法指示符。
切记,一旦实现了客户端代码,就不能在修改或者移除AIDL文件中的方法。可以在文件末尾添加新的方法,但是因为AIDL编译器会为每个方法生成标识符,所以不能修改现存的方法,否则不能向后兼容老版本。需要处理新版的API时,建议创建一个新的AIDL文件。这样做允许保持与老版本客户端的兼容。正如上面ApiInterfaceV1文件名,在天际新方法就可以创建一个V2结尾的文件,以此类推。
准备好AIDL文件后,服务器端需要实现
public class AidlService extends Service { private ArrayList<CustomData> mCustomDataCollection; @Override public void onCreate() { super.onCreate(); this.mCustomDataCollection=new ArrayList<CustomData>(); } @Override public IBinder onBind(Intent intent) { return mBinder; } private void getDataSinceImpl(CustomData[] result,Date since){ int size=this.mCustomDataCollection.size(); int pos=0; for (int i=0;i<size && pos<result.length;i++){ CustomData storedValue=this.mCustomDataCollection.get(i); if(since.after(storedValue.getmCreated())){ result[pos++]=storedValue; } } } private void storeDataImpl(CustomData data){ int size=this.mCustomDataCollection.size(); for (int i=0;i<size;i++){ CustomData customData=this.mCustomDataCollection.get(i); if(customData.equals(data)){ this.mCustomDataCollection.set(i,data); return ; } } this.mCustomDataCollection.add(data); } public static boolean isPrimeImpl(long number){ return true; } private final ApiInterfaceV1.Stub mBinder=new ApiInterfaceV1.Stub(){ @Override public boolean isPrime(long value) throws RemoteException { return isPrimeImpl(value); } @Override public void getAllDataSince(long timestamp, CustomData[] result) throws RemoteException { getDataSinceImpl(result,new Date(timestamp)); } @Override public void storeData(CustomData data) throws RemoteException { storeDataImpl(data); } }; }
上面的代码中Service在代码末尾实现了ApiInterfaceV1.Stub.该对象也会在onBind()方法中返回给绑定到Service的客户端。注意,每次对ServiceAPI的调用都运行在自身的线程上,因为Binder提供了一个线程池用于执行所有的客户端调用。这意味着使用这种方法时,客户端不会阻塞Service所属的主线程.
服务器端配置文件配置服务如下:
<service android:name=".AidlService" android:enabled="true" android:exported="true" > <intent-filter> <action android:name="com.example.liyuanjing.myapplication.AIDL_SERVICE"/> </intent-filter> </service>
android:exported="true"表示能被其他应用程序调用
android:enabled="true" 服务处于激活状态
以上是服务器端所需要的全部代码
下面的Activity展示了如何绑定到一个远程Service以及检索ApiInterfaceV1接口。如果是该API的唯一用户,可以同时管理客户端和服务器端的版本,那么这是首选的解决方案。
public class MainActivity extends Activity implements ServiceConnection { private Button start; private ApiInterfaceV1 mService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.start=(Button)findViewById(R.id.start); } @Override protected void onResume() { super.onResume(); bindService(new Intent("com.example.liyuanjing.myapplication.AIDL_SERVICE"),this,BIND_AUTO_CREATE); } @Override protected void onPause() { super.onPause(); unbindService(this); } public void onCheckForPrime(View v)throws Exception{ long number=11; boolean isPrime=mService.isPrime(number); String message=isPrime?"你知道的这是正确啊":"你不知道的这是错误啊"; Toast.makeText(this,message, Toast.LENGTH_LONG).show(); } @Override public void onServiceConnected(ComponentName name, IBinder service) { this.mService=ApiInterfaceV1.Stub.asInterface(service); this.start.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { try { onCheckForPrime(v); } catch (Exception e) { e.printStackTrace(); } } }); } @Override public void onServiceDisconnected(ComponentName name) { this.mService=null; } }
当然客户端只有上面的代码是远远不能调用服务器端的Service。需要将编译后的AIDL文件和Parcel包裹拷贝到客户端下,并且包的名称要和服务器端的一致否则会出现错误提示。
编译后的AIDL文件在如下图所示的路径中:
客户端拷贝后的项目缩略图如下图所示:
一定要注意从服务器拷贝文件到客户端中包名必须一样否则不仅不能实现你所要的功能,而且会报错。