简介
远程服务:Android 系统与Windows系统的通信原则基本一致,进程就是安全策略的边界,不同的APP属于不同进程 Process,一个进程不能直接访问其他进程的资源。需要实现多进程间的通信,就要使用IPC(Inter Process Commnication)进程间通信技术。Android 系统的 IPC 机制是基于AIDL(AndroidInterfaceDefinition Language)接口定义语言定制进程间的通讯规则的。系统会基于AIDL 规则把信息进行序列化处理,然后发送到另一个进程当中,Android 系统把这种基于跨进程通信的服务称作 Remote Service 。
AIDL:是一种IDL语言,定义Android中两个进程间通信的规则。因为在Android中每个应用程序都是一个单独的JVM,就像两个独立的小岛,过着自己的生活,进行自己的操作,互不相干,虽然都是在地球上,但无法进行联系,这时候AIDL就像一座桥连接着两个岛,桥制定规则,规定人怎么来往,哪些人可以来往。在进程间就规定数据怎么进行传输。
通信方式比较:其实进程间通信还可以使用BroadcastReceiver , Messenger 等,但是BroadcastReceiver占用资源较多,并且它优先级较低,如果在这个广播之前有系统级广播,那它就会延迟执行,对于那些及时性的通信要求高的应用显然不合适;而Messenger 通信虽然实现简单,但它是以队列的方式进行,显然对那些要求并发执行的应用也不合适,那AIDL优势也就出来了,可以传输复杂的数据量,也可以支持多进程并发情况的进程间通信。
远程服务应用:当今有很多企业都有多个独立的APP,如阿里巴巴旗下就天猫、淘宝、聚划算、支付宝等多个APP,这时候就有需要把Service服务放在一独立的后台进程当中,作为多个APP之间信息交换的桥梁。这样如用户信息,用户登录,身份验证等多个共用的模块都可以在Service服务中实现,以供不同的APP进行调用。
编写
那我们如何编写AIDL文件呢,其实AIDL语法和Java语法基本上差不多,只不过有点细微差别:
1.AIDL文件后缀名是.aidl,java文件是.java;
2.aidl默认支持的数据类型是:
* Java八种基本数据类型(byte/short/int/long/float/double/boolean/char)
* String 与CharSequence类型。
* List类型:List中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是实现parcelable接口的
* Map类型:Map中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是实现parcelable接口的,Map是不支持泛型的。
3.AIDL应用的目标文件即使与你正在编写的文件在同一个包下,也是需要导包的,但是java是不需要的;
4.AIDL中的定向 tag 表示了在跨进程通信中数据的流向,这个点放在下一篇(Android 远程服务 通过AIDL进行进程间复杂类型数据交换)详细叙述,
in 表示数据只能由客户端流向服务端,表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;
out 表示数据只能由服务端流向客户端,表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改,之后客户端将会同步变动;
inout 则表示数据可在服务端与客户端之间双向流通,表现为服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。
5.AIDL文件大概分为两类,一类是定义parcelable对象,给其它AIDL文件使用AIDL支持的默认数据类型外的数据;一类是用来定义接口的。看到没,AIDL都是在定义规则,而没有具体实现,所以它为什么叫接口定义语言了。
例子
我们先做个简单的例子来体会下AIDL是什么东西(我这demo把远程服务端与客户端写在了一个app里)
先新建一个AIDL文件,如图:
输入文件名,我这里是IMathInterface,然后就是这样
这是编译器自动帮我们新建的,可以发现AIDL文件所在包名跟我们主程序包名是一样的。
在这个文件里我定义了两个方法,代码如下:
package com.mangoer.remotemathservicedemo;
//远程服务接口的定义
interface IMathInterface {
int add(int a,int b);
void log(String tag);
}
然后需要生成与之对应的java接口文件,点击AS顶部工具栏的Build,然后选择MakeProject,然后在如图位置就会生成对应的java文件
我们来看下这个文件
public interface IMathInterface extends android.os.IInterface {
public int add(int a, int b) throws android.os.RemoteException;
public void log(java.lang.String tag) throws android.os.RemoteException;
/**本地IPC实现类Stub */
public static abstract class Stub extends android.os.Binder implements com.mangoer.remotemathservicedemo.IMathInterface {
private static final java.lang.String DESCRIPTOR = "com.mangoer.remotemathservicedemo.IMathInterface";
/** Stub钩子方法,并绑定到接口 */
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* asInterface(IBinder) 是Stub内部的远程服务接口,调用者可以通过该方法获得远程服务的实例
*/
public static com.mangoer.remotemathservicedemo.IMathInterface asInterface(android.os.IBinder obj) {
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
//判断android.os.IInterface实例是否为本地服务 若是返回android.os.IInterface
//若不是本地服务 构造Proxy对象并返回之
if (((iin!=null)&&(iin instanceof com.mangoer.remotemathservicedemo.IMathInterface))) {
return ((com.mangoer.remotemathservicedemo.IMathInterface)iin);
}
return new com.mangoer.remotemathservicedemo.IMathInterface.Stub.Proxy(obj);
}
//实现了android.os.IInterface接口定义的asBinder()方法
@Override
public android.os.IBinder asBinder() {
return this;
}
/*
* Parcel是Android系统应用程序间传递数据的容器,能够在两个进程中完成打包和拆包的工作 但Parcel不同于通用意义上的序列化
* Parcel的设计目的是用于高性能IPC传输 不能将其保存在持久存储设备上
* 接收Parcel对象,并从中逐一读取每个参数,然后调用Service内部制定的方法,将结果写进另一个Parcel对象,
* 准备将这个Parcel对象返回给远程的调用者
*/
@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_add: {
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();
int _result = this.add(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_log: {
data.enforceInterface(DESCRIPTOR);
java.lang.String _arg0;
_arg0 = data.readString();
this.log(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
//用来实现远程服务调用
private static class Proxy implements com.mangoer.remotemathservicedemo.IMathInterface {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
//以一定顺序将所有参数写入Parcel对象,以供Stub内部的onTransact()方法获取参数
@Override
public int add(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_add, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override
public void log(java.lang.String tag) 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(tag);
mRemote.transact(Stub.TRANSACTION_log, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_log = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
}
方法有什么用,我都加了注释,这个文件是自动生成的,不要手动修改逻辑。现在要实现远程服务了,新建MathService类继承Service
public class MathService extends Service {
private String TAG = "MathService";
/*
* 建立IMathInterface.Stub实例,并实现IMathInterface这个AIDL文件定义的几个远程服务接口
*在onBind方法中将mBind返回给远程调用者
* */
private IMathInterface.Stub mBind = new IMathInterface.Stub() {
@Override
public int add(int a, int b) throws RemoteException {
Log.e(TAG,"add");
return a+b;
}
@Override
public void log(String tag) throws RemoteException {
log_e(tag);
}
};
private void log_e(String tag) {
while (true) {
Log.e(tag,"math");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG,"onCreate process id = " + Process.myPid());
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.e(TAG,"onBind");
return mBind;
}
@Override
public boolean onUnbind(Intent intent) {
Log.e(TAG,"onUnbind");
return false;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG,"onDestroy");
}
}
然后需要注册该服务
<service android:name=".MathService"
android:process=":remote">
<intent-filter>
<action android:name="com.mangoer.remotemathservicedemo.MathService" />
</intent-filter>
</service>
可以看到我给服务加了一个属性android:process:如果客户端与服务端在同个App中,被设置的进程名是以一个冒号(:)开头的,则这个新的进程对于这个应用来说是私有的,代表Service在单独的进程中运行,进程名称为:App-packageName:remote。
如果这个进程的名字是以小写字符开头的(比如remote),则这个服务将运行在一个以这个名字命名的全局的进程中,
当然前提是它有相应的权限。这将允许在不同应用中的各种组件可以共享这个进程,从而减少资源的占用。
我还加了一个intent-filter:这是为了隐式启动远程服务所用。
讲到intent-filter就得讲还有一个属性android:exported:代表是否能被其他应用隐式调用,其默认值是由service中有无intent-filter决定的,如果有intent-filter,默认值为true,否则为false。手动设置false的情况下,即使有intent-filter匹配,也无法打开,即无法被其他应用隐式调用。
现在要开始远程调用了,在布局里加几个按钮
然后在MainActivity里开始写,在绑定按钮里开始启动服务
@OnClick(R.id.bind)
public void bind() {
Log.e(TAG,"bind");
//隐式启动服务,android5.0后要设置包名
Intent serviceIntent = new Intent();
serviceIntent.setAction("com.mangoer.remotemathservicedemo.MathService");
serviceIntent.setPackage("com.mangoer.remotemathservicedemo");
bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
}
因为不处于一个进程,需要隐式启动,而且在android5.0以前,我们只要设置Action就行了,但是之后需要把包名也一起设置进来,从源码可以看出如果启动service的intent的component和package都为空并且版本大于LOLLIPOP(5.0)的时候,直接抛出异常。
启动之前需要构建一个ServiceConnection
private IMathInterface mathInterface;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mathInterface = IMathInterface.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mathInterface = null;
}
};
在onServiceConnected里获取远程服务的实例,这时候我们看下日志
可以看出Mainactivity打印出来的进程id是6821,进程名称是包名,再看下远程服务的日志
进程id是7151,进程名是 包名:remote,这也就证明了应用程序与远程服务不处于同一个进程。可以看出远程服务回调了onCreate,onBind两个方法。
然后点击计算按钮
@OnClick(R.id.math)
public void math() {
if (mathInterface == null) {
Toast.makeText(this,"远程服务未绑定",Toast.LENGTH_LONG).show();
return;
}
try {
int result = mathInterface.add(3,2);
Log.e(TAG,"result="+result);
} catch (RemoteException e) {
e.printStackTrace();
}
}
通过在onServiceConnected方法里拿到的远程服务实例,调用add方法,
在日志了看到主进程里打印了
11-09 14:52:48.5686821-6821/com.mangoer.remotemathservicedemo E/MainActivity: result=5
在远程服务进程打印了,也就是调用了add方法
11-09 14:52:48.567 7151-7171/com.mangoer.remotemathservicedemo:remoteE/MathService: add
这个操作是在客户端(MainActivity)里调用服务端(MathService)的add方法,并把两个int型数据传递给服务端,服务端计算完,把结果再返回给客户端(MainActivity),两个进程间就通过IMathInterface这个接口类进行数据交换。这就基本完成了两个进程间的简单通信。
这里传递的都是简单的数据类型,打包解包都是自动进行的,对于一些自定义的数据类型,需要实现Parcelable接口,将其转换成Parcel对象,使其穿越进程边界,这里我们下一篇继续(Android 远程服务 通过AIDL进行进程间复杂类型数据交换)。