IPC 机制

Posted Flow_孙权

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IPC 机制相关的知识,希望对你有一定的参考价值。

2.1 android IPC 简介

IPC 意为进程间通信或者跨进程通信,线程是 CPU 调度的最小单元,是一种有限的系统资源。进程一般指一个执行单元。任何操作系统都需要相应的 IPC 机制。如 Windows 上可以通过剪切板 管道 和邮槽来进行;Linux 上可以通过命名管道 共享内容 信号量等来进行。在 Android 中最有特色的进程间通信方式就是 Binder 了,同时也支持 Socket 实现任意两个终端之间的通信。

2.2 Android 中的多进程模式

(1) 通过给四大组件指定 android:process 属性,可以开启多线程模式,默认进程的进程名字是包名。

android:process=":sunquan"
android:process="cn.sunquan.com.xx"

“:”指当前的进程名前面附加上当前的包名,简写的方式。其次进程名以“:”开头的进程属于当前应用的私有进程,其他应用组件不可以跟它在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他的应用可以通过 ShareUID 方式和它跑到同一个进程中。
(2) 系统为每一个应用分配一个唯一的 UID,具备相同 UID 的应用才能共享数据。两个应用通过 ShareUID 跑在同一个进程中需要有相同 ShareUID 的并且签名相同。在这种情况下它们可以相互访问对方的私有数据比如 data 目录、 组件信息等,不管它们是否跑在同一个进程中。当然如果它们跑在同一个进程中,那么除了能共享 data 目录、组件信息,还可以共享内存数据,或者说它们看起来像一个应用的两个部门。
(3) Android 为每一个应用分配了一个独立的虚拟机,或者说为每一个进程都分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同虚拟机上访问同一个类的对象会产生多份副本。所以运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这就是多进程所带来的主要影响。
(4) 多进程一般会造成如下几方面的问题:
1. 静态成员和单例模式完全失效:不在同一块内存
2. 线程同步机制完全失效:不管锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是一个对象。
3. SharePreference 的可靠性下降:SharePreference 底层是通过读/写 XML 文件来实现,并发写显然可能出问题。SharePreference 不支持两个进程同时执行写操作,否则会有一定几率的丢失。
4. Application 会多次被创建:当一个组件跑在一个新的进程中,系统会创建新的进程同时分配独立的虚拟机,应用重启一次,会创建新的 Application。运行在同一个进程中的组件属于同一个虚拟机和同一个 Application。不同进程的组件的确会拥有独立的虚拟机、Application 以及内存空间。同一应用的不同组件,运作在不同的进程中,那跟它们分别属于两个应用的部门没有本质区别。
(5) 虽然不能直接共享内存但是通过跨进程通信还是可以实现数据交互。实现跨进程的方式:通过 Intent 来传递数据;共享文件;SharePreference;基于 Binder 的 Messager 和 AIDL 以及 socket。

2.3 IPC 基础概念介绍

(1) Serializable 是 Java 所提供的一个序列化接口,为对象提供标准的序列化和反序列化操作。Parceable 接口是 Android 提供的序列化方式。
(2) 实现 Serializable 接口,并声明一个 serialVersionUID 即可让一个对象实现序列化。serialVersionUID 是一串long型的数字,用来辅助序列化和反序列化过程。原则上序列化后的数字中的 serialVersionUID 只有和当前累的 serialVersionUID 相同才能够正常的被反序列化。serialVersionUID 的详细工作机制:序列化得时候系统会把当前类的 serialVersionUID 写入序列化得文件中(也可以是其他中介),当反序列化得时候系统会检测文件中的 serialVersionUID,看它是否和当前类的 serialVersionUID 一致,如一致说明序列化的类的版本和当前类的版本相同,这个时候反序列化可以成功,否则说明当前类和序列化的类相比发生了某些变换,一般来说我们应该指定 serialVersionUID 的指。
注意:1.静态成员变量属于类不属于对象,不参与序列化过程;2.用transient 关键字标记的成员变量不参与序列化过程。
(3) 实现 Parceable 接口,一个类的对象可以通过实现序列化并可以通过 Intent 和 Binder 传递。Pacel 内部包装了可序列化的数据,可以在 Binder 中自由传输,可以直接序列化得有 Intent、Bundle、Bitmap、List、Map等,前提是它们里面的每一个元素都是可序列化的。
(4) Serializable 和 Parceable 的区别:Serializable 是 Java 中的序列化接口,其使用起来简单但是因为大量 I/O 操作而开销大。Parceable 是 Android 中的序列化方式,更适用在 Android 平台上,缺点是使用起来麻烦,但是效率高。Parceable 主要用在内存序列上,而序列化到存储设备上或者序列化后通过网络传输则建立适用 Serializable。
(5) Binder 是 Android 中的一个类,它实现了 Binder 接口。从 IPC 角度来说,Binder 是 Android 中一种跨进程通信方式,Binder 还可以理解为一种虚拟的物理设备,设备驱动是 /dev/binder,该通信方式在 Linux 中没有;从 Android Framework 角度来说,Binder 是ServiceManager 连接各种 Manager 和相应 ManagerService 的桥梁;从 Android 应用层来说,Binder 是客户端和服务端进行通信的媒介,当 bindService 的时候,服务端会返回一个包含了服务端业务调用的 Binder 对象,通过这个 Binder 对象,客户端就可以获取服务端提供的服务或数据,这里的服务包括普通服务和基于 AIDL 的服务。
Android 开发中,Binder 主要用在 Service 中,包括 AIDL 和 Messager,其中普通 Service 中的 Binder 不涉及进程间的通信,较为简单。而 Messager 的底层其实也是 AIDL。
(6) aidl工具根据 aide文件自动生成 Java 接口的解析:声明了几个接口的方法,同时声明几个整型 id 来标识这几个接口,id 用来标识在 transact 过程中客户端请求的是哪个方法。接着会声明一个 内部类 Stub ,这个 Stub 就是一个 Binder 类, 当客户端和服务器位于同一个进程中,则不会走 跨进程的 transact 过程,如果不在同一个进程,方法调用需要走 transact 过程,这个逻辑有 Stub 内部代理 Proxy 来完成。
其核心实现就是它的内部类 Stub 和 Stub 内部代理 Proxy。其几个方法分析如下:
1. asInterface (android.os.Binder obj):用于将服务端的 Binder 对象转换成客户端所需的 AIDL 接口类型的对象。如果客户端和服务端位于同一进程,那么此方法返回服务端的 Stub 对象本身,否则返回系统封装后的 Stub.proxy 对象。
2. asBinder:用于放回当前 Binder 对象
3. onTransact :运行在服务端中的 Binder 线程中,当客户端发起跨进程请求中,远程请求会通过系统底层封装后由此方法来处理。该方法的原型为 public Boolean onTranact (int code, android.os.Parcel data, android.os.Parcel reply, int flags)。服务端通过 code 可以确定客户端所请求的目标方法是什么,接着从 data 中取出目标方法所需的参数(如果目标方法有参数的话),然后执行目标方法。当目标方法执行完毕后,就向 reply 中写入返回值(如果目标方法有返回值的话)。如果此方法返回 false,那么客户端的请求会失败,可以通过这个特性做权限验证。
4. Proxy#[method] :运行在客户端,当客户端调用此方法时,首先创建该方法所需的输入型 Parcel 对象 _data、输出型 Parcel 对象 _repley 和返回值对象,把该方法的参数信息写入 _data 中(如果有参数),然后调用 transact 方法发起 RPC(远程过程调用)请求,同时当前线程挂起,然后服务端的 onTransact 方法会被调用,直到 RPC 过程返回后,当前线程继续执行,并从 _reply 中取出 RPC 过程的返回结果。最后返回 _reply 中的数据。
注意:1.当客户端发起请求时,由于当前线程会被挂机直到服务端进程返回数据,如果远程方法耗时,那么不能在 UI 线程中发起此远程请求。2.服务端的 Binder 方法运行在 Binder 的线程池中,不管 Binder 方法是否耗时都应采用同步的方式去实现,因为运行在一个线程中。
5. AIDL 文件的本质就是系统提供一种快速实现 Binder 的工具,我们可以自己手动写,也可以通过 AIDL 文件让系统自动生成。
6. Binder 有两个很重要的方法:linkToDeath 和 unlinkToDeath,Binder 运行在服务端,服务端进程由于某些原因异常终止了,服务端的 Binder 连接断裂,导致客户端远程调用失败。通过 linkToDeath 可以给 Binder 设置一个死亡代理,当 Binder 死亡时,会收到通知,这时可以重新发起连接请求从而恢复连接。
设置 Binder 死亡代理如下:
首先声明一个 DeathRecipient 对象,DeathRecipient 是一个接口,内部只有一个方法 binderDied,当 Binder 死亡时候,系统回调此方法,我们可以在移除之前绑定的 binder 代理并重新绑定远程服务。

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() 
    @Override
    public void binderDied() 
        if (mRemoteBookManager == null) return;        mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mRemoteBookManager = null;
        // TODO:这里重新绑定远程Service
    
;

其次在客户端绑定远程服务成功后,给 Binder 设置死亡代理:

mservice= IMessageBoxManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient, 0)

其中 linkToDeath 的第二个参数是个标记位,直接设置0即可。此外通过 Binder 的方法 isBinderAlive 也可以判断 Binder 是否死亡。

2.4 Android 中的 IPC 方式

(1) 使用 Bunble:Bunble 实现了 Parcelable 接口,可以在不同的进程间传输。Bunble 不支持的类型无法通过它在进程间传输。
(2) 使用文件共享:由于 Android 系统基于 Linux,并发读/写文件可以没有限制的进行,两个线程对同一个文件进行写操作都是运行,但可能出问题。文件共享方式适合对数据同步要求不高的进程间进行通信,并要妥善处理并发读/写的问题。SharedPreference 是一个特例,属于文件的一种,但系统对其的读/写有一定的缓存策略,即在内存中会有一份 SharedPreference 文件的缓存,因此在多进程下,系统对它的读/写变得不可靠,面对高并发的读/写访问,SharedPreference 有很大几率丢失数据,所以不建议在进程间通信中使用 SharedPreference。
(3) 使用 Messenger:Messenger 是一种轻量级的 IPC 方案,其底层实现是 AIDL,以串行方式处理客户端发来的消息,其服务端一次处理一个请求,不存在并发执行的情形(对于有大量并发请求,Messenger 就不合适适用)。
(4) 使用 AIDL :首先服务端创建一个 Service 用来监听客户端的连接请求,创建一个 AIDL 文件,将暴露给客户端的接口在这个 AIDL 文件中声明,最后 Service 中实现这个 AIDL接口。客户端绑定服务端 Service,建立连接就可访问远程服务端的方法。
1. AIDL 支持的数据类型:基本数据类型(int、long、chat、boolean、double 等);String 和 CharSequence;List(只支持 ArrayList,里面的子元素都必须能够被 AIDL 支持);Map(只支持 HashMap,里面每个元素都必须被 AIDL 支持,包括 key 和 value);Parcelable(所以实现了 Parcelable 接口的对象);AIDL(所有的 AIDL 接口也可以在 AIDL 文件中使用)。
2. 自定义的 Parcelable 对象和 AIDL 文件就算和当前 AIDL 文件位于同一包内也要显式 import。
3. 如果 AIDL 文件中用到自定义 Parcelable 对象,必须新建一个和它同名的 AIDL 文件,并在其中声明它为 Parcelable 类型。
4. AIDL 中除了基本数据类型,其他类型的参数必须标上方向:in、out 或者 inout,in 表示输入类型参数,out 表示输出型参数。
5. AIDL 接口中只支持方法,不支持声明静态常量。区别于传统的接口。
6. 为方便 AIDL 的开发,建议把所有和 AIDL 相关的类和文件全部放入同一个包中,当客户端是另一个应用时,可以直接把整个包复制到客户端工程中。
7. RemoteCallbackList 是系统专门提供用于删除进程 listener 的接口,RemoteCallbackList 是一个泛型,支持管理任意的 AIDL 接口,所有的 AIDL 接口都继承自 Interface 接口,在它的内部有一个 Map 结构专门用来保存所有的 AIDL 回调,其 Map 的 key 是 Binder 类型,value 是 Callback 类型。当客户端进程终止后,它能够自动移除客户端所注册的 listener。另外 RemoteCallbackList 内部自动实现了线程同步的功能,所以使用它进行注册和解注册时,不需要额外的线程同步工作。使用 RemoteCallbackList 需要注意是:它不是一个 List。遍历 RemoteCallbackList 需要按照以下方式进行,其中 beginBroadcast 和 finishBroadcast 必须配对使用,哪怕仅仅是获取 RemoteCallbackList 中的元素个数:

int N =mListenerList.beginBroadcast();
   for(int i = 0; i < N; i++)
            //         
     
    mListenerList.finishBroadcast();
  1. 客户端的 onServiceConnected 和 onServiceDisconnected 方法都运行在 UI 线程中,所以不可以在它们里面直接调用服务端的耗时方法。服务端的方法本身就运行在服务端的 Binder 线程池中,所以服务端方法本身就可以执行大量耗时操作,这个时候切记不要在服务端方法中开线程去进行异步任务,除非明确干什么。
  2. Binder 可能意外死亡,需要重新连接服务,有两种方法:给 Binder 设置 DeathRecipient 监听,死亡时会收到 binderDied 方法回调,然后可以重连;一种是在 onServiceDisconnected 中重连。其二者区别:onServiceDisconnected 在客户端的 UI 线程中被回调,binderDied 在客户端的 Binder 线程池中被回调,不能访问 UI。
  3. AIDL 使用常用的权限验证方法: 一是在 onBind 中进行验证,验证不通过返回 null,客户端直接无法绑定服务。验证方式如 使用 permission 验证,在 AndroidMenifest 中声明所需权限(自定义 permission),此方式同样适用于 Messenger 中。二是在服务端 onTransact 方法中进行权限验证,如果验证失败返回 false,这样服务端就不会终止执行 AIDL 中的方法从而达到保护服务端的效果,验证方式也可以采用permission,还可以采用 Uid 和 Pid 来做验证,通过 getCallingUid 和 getCallingPid 拿到客户端所属应用的 Uid 和 Pid。这个两个参数可以用来做一些验证工作,比如验证包名。
    (5) 使用 ContentProvider:ContentProvider 是 Android 中提供的专门用于不同应用间进行数据共享方式,和 Messenger 一样,其底层实现是 Binder,当其实现过程比 AIDL 简单许多。主要以表格的形式来组织数据,可以包含多个表。还支持文件数据,比如图片和视频等。文件数据和表格数据的结构不同,因此处理此类数据可以在 ContentProvider 中返回文件的句柄给外界从而让文件来访问 ContentProvider,Android 系统提供的 MediaStore 功能就是文件类型的 ContentProvider。ContentProvider 的底层数据看起来像一个 SQLite 数据库,但是 ContentProvider 对底层的数据存储方式没有任何要求,可以使用 SQLite 数据库,也可以使用普通文件,甚至可以采用内存中的一个对象进行数据的存储。要观察一个 ContentProvider 中的数据改变情况,可以通过 ContentResolver 的 registerContentObserver 方法来注册观察者,可以通过 unregisterContentObserver 方法来解除观察者。
    (6) 使用 Socket : Socket 被称为套接字,分为流式套接字和用户数据报套接字两种。分别对应于网络的传输控制层中的 TCP 和 UDP 协议。

2.5 Binder 连接池

当项目到一定程度,无限制的增加 Service 是不对的,Service 是四大组件之一,也是一种系统资源。我们需要将所有的 AIDL 放在同一个 Service 中去管理。工作机制:每个业务模块创建自己的 AIDL 接口并实现此接口,不同的业务模块之间是不能有耦合的,所有实现细节都单独开来,然后向服务端提供自己的唯一标识和其相对应的 Binder 对象。对于服务端来说只需要一个 Service 就可以了,服务端提供一个 queryBinder 接口,这个接口能够根据业务模块的特征来返回相应的 Binder 对象给它们。不同的业务模块拿到所需的 Binder 对象后就可以进行远程方法调用,由此可见,Binder 连接池的主要作用就是将每个业务模块的 Binder 请求同一转发到远程 Service 中去执行,从而避免重复创建 Service。建议在开发中使用 BinderPool。具体源码查看:
https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_2/src/com/ryg/chapter_2/binderpool/BinderPool.java

2.6 选用合适的 IPC 方式

以上是关于IPC 机制的主要内容,如果未能解决你的问题,请参考以下文章

IPC机制

linux各种IPC机制(进程通信)

复习:IPC机制

不同IPC机制的使用

IPC机制

20155202 张旭 Linux下IPC机制