Android四大组件之Service 远程服务解析,通过AIDL达到进程间通信交换基础数据

继上一篇介绍Service的文章后,这篇来具体分析下远程服务,具体代码请看https://github.com/Mangosir/RemoteServiceMath

简介

远程服务:Android 系统与Windows系统的通信原则基本一致,进程就是安全策略的边界,不同的APP属于不同进程 Process,一个进程不能直接访问其他进程的资源。需要实现多进程间的通信,就要使用IPC(Inter Process Commnication)进程间通信技术。Android 系统的 IPC 机制是基于AIDL(AndroidInterfaceDefinition Language)接口定义语言定制进程间的通讯规则的。系统会基于AIDL 规则把信息进行序列化处理,然后发送到另一个进程当中,Android 系统把这种基于跨进程通信的服务称作 Remote Service 。

AIDL:是一种IDL语言,定义Android中两个进程间通信的规则。因为在Android中每个应用程序都是一个单独的JVM,就像两个独立的小岛,过着自己的生活,进行自己的操作,互不相干,虽然都是在地球上,但无法进行联系,这时候AIDL就像一座桥连接着两个岛,桥制定规则,规定人怎么来往,哪些人可以来往。在进程间就规定数据怎么进行传输;其底层采用的是Binder机制。

通信方式比较:其实进程间通信还可以使用BroadcastReceiver , Messenger 等,但是BroadcastReceiver占用资源较多,并且它优先级较低,如果在这个广播之前有系统级广播,那它就会延迟执行,对于那些及时性的通信要求高的应用显然不合适;而Messenger 通信虽然实现简单,但它是以队列的方式进行,显然对那些要求并发执行的应用也不合适,那AIDL优势也就出来了,可以传输复杂的数据量,也可以支持多进程并发情况的进程间通信。

远程服务应用:当今有很多企业都有多个独立的APP,如阿里巴巴旗下就天猫、淘宝、聚划算、支付宝等多个APP,这时候就有需要把Service服务放在一独立的后台进程当中,作为多个APP之间信息交换的桥梁。这样如用户信息,用户登录,身份验证等多个共用的模块都可以在Service服务中实现,以供不同的APP进行调用。

AIDL规则

那我们如何编写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基本步骤

  • 声明文件:在服务端创建AIDL文件,用来声明Java Bean等传输的数据,传输调用的接口
  • 创建服务:在服务端创建远程Service,并继承接口实现类
  • 客户端绑定远程Service,并获得远程服务的代理对象,即获取远程Binder对象的接口的本地实现
  • 客户端通过接口调用远程Service提供的方法

使用样例

我们先做个简单的例子来体会下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文件

我们来看下这个文件

//继承IInterface接口,目的是将此接口与远程Binder对象关联
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;

    /**Binder本地对象,即Server进程里面的Binder对象*/
    public static abstract class Stub extends android.os.Binder implements com.mangoer.remotemathservicedemo.IMathInterface {

		//Binder的唯一标识
        private static final java.lang.String DESCRIPTOR = "com.mangoer.remotemathservicedemo.IMathInterface";

        public Stub() {
        	//调用这个方法,将描述符与Binder对象关联
            this.attachInterface(this, DESCRIPTOR);
        }
        /**
         * IBinder是一个接口,代表了跨进程传输能力
         *
         * 这个方法用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象
         * 如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,因为Stub就是Binder的本地对象
         * 否则返回服务端的Binder代理,也就是内部类Proxy
         *
         * 这就好比你在北京,A商城(Stub)也在北京,你想买A商城的苹果,就直接去本地A商城买东西
         * 但是如果你在西藏,那你只能去A商城在西藏的代理点(Proxy)去买它的苹果
         */
        public static com.mangoer.remotemathservicedemo.IMathInterface asInterface(android.os.IBinder obj) {
            if ((obj==null)) {
                return null;
            }
            //通过描述符查找本地接口
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);

            if (((iin!=null)&&(iin instanceof com.mangoer.remotemathservicedemo.IMathInterface))) {
            	//处于同一个进程
                return ((com.mangoer.remotemathservicedemo.IMathInterface)iin);
            }
            //跨进程,就构建IBinder这个接口的代理对象返回,通过代理对象调用远程服务方法
            return new com.mangoer.remotemathservicedemo.IMathInterface.Stub.Proxy(obj);
        }
        
        //返回与此接口关联的Binder对象,这里是Binder本地对象
        //因为Stub类继承Binder类,而Binder又实现了IBinder接口,所以直接返回自己
        @Override
        public android.os.IBinder asBinder() {
            return this;
        }
        
        /*
        * Parcel是Android系统应用程序间传递数据的容器,能够在两个进程中完成打包和拆包的工作 但Parcel不同于通用意义上的序列化
        * Parcel的设计目的是用于高性能IPC传输 不能持久化在存储设备上
        * 接收Parcel对象,并从中逐一读取每个参数,然后调用Service内部制定的方法,将结果写进另一个Parcel对象,
        * 
        * onTransact运行在服务端中的Binder线程池中 客户端发起跨进程请求时,远程请求会通过系统底层封装后交给此方法来处理,
        * 服务端接收到请求并且通过客户端携带的参数,执行完服务端的方法,返回结果
		*	如果此方法返回false,那么客户端的请求就会失败
		*
		* code :确定客户端请求的目标方法是什么
		* data : 如果目标方法有参数的话,就从data取出目标方法所需的参数
		* reply : 当目标方法执行完毕后,如果目标方法有返回值,就向reply中写入返回值
		* 
         */
        @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;//add方法第一个参数
                    _arg0 = data.readInt();
                    int _arg1;//add方法第二个参数
                    _arg1 = data.readInt();
                    //执行接口定义的方法,传入两个参数,执行完毕获取返回值,具体由Server实现具体逻辑
                    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 {

			//Binder代理对象,即Client里面的Binder对象,客户端绑定服务后传入的
            private android.os.IBinder mRemote;
            
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            //返回Binder代理对象
            @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);
                    //transact方法运行在客户端,通过该方法发起远程过程调用(RPC)请求,同时当前线程挂起;
                    //然后服务端的onTransact方法会被调用,直到RPC过程返回后,
                    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
                    //当前线程继续执行,并从_reply中取出RPC过程的返回结果,也就是返回_reply中的数据
                    _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);
    }
}
    
    

你在AS里看到的可能比较乱,我这是整理后的

这个文件就是进程通信的关键了,注释写的非常清楚了,大概总结下:

  • IBinder是一个接口,代表了跨进程传输能力,负责数据传输,只要实现了这个接口,就能将这个类型的对象进行跨进程传递;这是驱动底层支持的;在跨进程数据流经驱动的时候,驱动会识别IBinder类型的数据,从而自动完成不同进程Binder本地对象以及Binder代理对象的转换,有两个实现类Binder和BinderProxy
  • IInterface代表的就是远程server对象具有什么能力,具体来说,就是aidl里面的接口
  • Java层的Binder类代表的就是Binder本地对象,BinderProxy类是Binder类的一个内部类,它内部持有Binder引用,代表Binder代理对象;两个类都具备跨进程传输能力,在跨越进程的时候,Binder驱动会自动完成这两个对象的转换
  • AIDL通过Stub类用来接收、处理、返回数据,Proxy代理类供客户端调用发送数据及接收返回值

Stub类里有一个很重要的方法就是asInterface,我们通常在onServiceConnecttion的回调里面,把它的参数传递给这个方法拿到一个远程的service的,它的参数是IBinder,这是驱动给我们的。

如果这个IBinder是Binder类型,说明是Binder本地对象,处于同一个进程,asInterface方法就返回Stub对象,Client直接用其访问Server;如果IBinder是BinderProxy类型,说明是Binder代理对象,处于不同进程,asInterface方法就通过IBinder实例化Proxy对象,Client在这个类里用BinderProxy访问Server

通常我们这是跨进程调用,在onServiceConnecttion的回调里面,你实际拿到的是Proxy对象,Proxy对象持有Binder代理对象,也就是这里面的mRemote,mRemote实际是BinderProxy;Client在调用远程服务提供的add方法,其实是调用Proxy类的add方法,这个方法将Client请求进行封装后通过BinderProxy类的transact方法发送到远程Server

说是发送到Server了,但是远不止这么简单,先看下BinderProxy类的transact方法

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");
        if (Binder.isTracingEnabled()) { Binder.getTransactionTracker().addTrace(); }
        return transactNative(code, data, reply, flags);
}
public native boolean transactNative(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;

transactNative是一个本地方法,这需要调用到native层了,经过一系列调用后,最终会调用到Binder驱动,然后Binder驱动会通知Server,调用Server进程的本地对象的onTransact方法,这就会走到Binder本地对象的onTransact方法,也就是Stub类的onTransact方法

这个方法里根据调用号(每个AIDL函数都有一个编号,在跨进程的时候,不会传递函数,而是传递编号指明调用哪个函数)调用相关函数,这里就调用add方法,然后将结果返回给驱动,驱动再将结果传递给Client

创建服务

新建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:

1.如果属性值是以一个冒号 : 开头的,则这个新的进程对于这个应用来说是私有的,只有该APP可以使用其它应用无法访问,进程名称为:App-packageName:remote。

2.如果属性值不是以冒号 : 开头的,则这个服务将运行在一个以这个名字命名的全局的进程中,当然前提是它有相应的权限。这将允许在不同应用中的各种组件可以共享这个进程,从而减少资源的占用;但是属性值至少要包含一个点号 . 

这点从源码PackageParser.java解析AndroidManiefst.xml过程就明白进程名的命名要求

public class PackageParser {
    ...
    private static String buildCompoundName(String pkg,
       CharSequence procSeq, String type, String[] outError) {
        String proc = procSeq.toString();
        char c = proc.charAt(0);
        if (pkg != null && c == ':') {
           if (proc.length() < 2) {
               //进程名至少要有2个字符
               return null;
           }
           String subName = proc.substring(1);
           //此时并不要求强求 字符'.'作为分割符号
           String nameError = validateName(subName, false, false);
           if (nameError != null) {
               return null;
           }
           return (pkg + proc).intern();
        }
        //此时必须字符'.'作为分割符号
        String nameError = validateName(proc, true, false);
        if (nameError != null && !"system".equals(proc)) {
           return null;
        }
        return proc.intern();
    }

    private static String validateName(String name, boolean requireSeparator,
    boolean requireFilename) {
        final int N = name.length();
        boolean hasSep = false;
        boolean front = true;
        for (int i=0; i<N; i++) {
            final char c = name.charAt(i);
            if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
                front = false;
                continue;
            }
            if (!front) {
                if ((c >= '0' && c <= '9') || c == '_') {
                    continue;
                }
            }
            //字符'.'作为分割符号
            if (c == '.') {
                hasSep = true;
                front = true;
                continue;
            }
            return "bad character '" + c + "'";
        }
        if (requireFilename && !FileUtils.isValidExtFilename(name)) {
            return "Invalid filename";
        }
        return hasSep || !requireSeparator
                ? null : "must have at least one '.' separator";
    }
}

我还加了一个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进行进程间复杂类型数据交换)。

使用AIDL实现远程服务调用总结起来就是:

Server端为了提供服务,就得提供一套接口,里面包含了各种功能的方法;为了让Client端能通过远程访问调用Server端方法,就通过Proxy模式(代理模式),将接口方法定义在抽象类,然后Server和Client继承抽象类,实现所有接口方法;只不过Server端的这些方法的有真正功能实现,Client端的这些方法只是对请求进行封装,然后通过Binder驱动发送给Server端

如果你看过一些系统源码的实现,比如ActivityManagerServer的源码,PackageManagerService源码等,你会发现实现AIDL有一个固定模式:肯定有个接口实现了IInterface,定义了一些方法表明Server具有哪些能力;一个跨进程传递对象必然实现了IBinder接口,如果是Binder本地对象,就肯定继承Binder,实现了IInterface接口;如果是Binder代理对象,必然实现了IInterface接口,持有BinderProxy引用(Binder和BinderProxy都实现了IBinder接口)

 

参考文章http://weishu.me/2016/01/12/binder-index-for-newer/

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值