简介
远程服务: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进行进程间复杂类型数据交换)。