开发艺术之IPC

作者: 请叫我林锋 | 来源:发表于2019-12-17 22:48 被阅读0次
    一、如何开启多进程模式

    我们可以给四大组件在 AndroidManifest 中指定 android:process 属性来开启多进程,默认进程的进程名就是包名,假设现在包名为 "com.example.myapplication"

    • 以": "开头的进程:
      • android:process=":remote",表示进程名"com.example.myapplication:remote"
      • 属于私有进程,其他进程的组件不可以和它跑在同一个进程
    • 完整命名的进程:
      • android:process="com.wlf.test.xxx"
      • 属于全局进程,其他应用可通用 ShareUID 和它跑在一个进程

    通过 adb shell ps 或者 adb shell ps | grep 包名 来查看进程信息


    二、多进程带来的问题以及问题出现的原因

    每一个进程都拥有一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,不同虚拟机访问一个类的对象就会产生多份副本。这位造成下面几个问题:

    • 静态成员和单例模式完全失效(获取的不是同一个对象)
    • 线程同步机制完全失效(不同进程锁的不是同一个对象)
    • SharedPreferences 可靠性下降(并发的读/写可能会出问题)
    • Application 多次创建(不同进程的组件属于不同的虚拟机和 Application)

    三、序列化的使用

    序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。可以通过实现 Serializable 接口或者 Parcelable 接口来实现序列化。

    Serializable 接口和 Parcelable 接口的比较:

    序列化接口比较.png
    serialVersionUID
    • 作用:是 Serializable 接口中用来辅助反序列化过程,可以很大程度避免反序列化的过程的失败。建议手动指定 serialVersionUID 的值。

    有两种变量不参与序列化

    • 静态成员变量(属于类不属于对象)

    • 用 transient 关键字标记的成员变量

    推荐阅读:Serializable 这么牛逼,Parcelable 要你何用?


    四、Binder 通信机制

    a. 概念:

    • API, 是 Android 中的一个类,它实现了 IBinder 接口

    • IPC,是Android 中的一种跨进程通信方式

    • Android Framework,是 ServiceManager 连接各种 Manager 和相应 ManagerService 的桥梁

    • Android 应用层,是客户端和服务端进行通信的媒介

    b. Android 是基于 Linux 内核基础上设计的,Linux 已经拥有了 socket、管道、消息队列、共享内存,为什么还需要 Binder?

    • 传输性能高,易于控制:
    IPC方式 数据拷贝次数
    共享内存 0
    Binder 1
    Socket/管道/消息队列 2

    对于 Socket、管道、消息队列来说,数据先从发送方缓存区拷贝到内核缓存区中,然后再从内核缓存区拷贝到接收方缓存区,一共两次拷贝,如图:

    传统 IPC 通信原理

    而对于 Binder 来说,数据从发送方缓存区拷贝到内核开辟的缓存区中,内核缓存区内核中数据接收缓存区之间的映射关系,节省了一次数据拷贝,如图:

    Binder 通信原理

    共享内存虽然无需拷贝,但控制复杂,难以使用,综合来看 Binder 最具优势。

    • 安全性高:

      • 传统 IPC 只能由用户在数据包里填入UID/PID,容易被恶意程序利用;而Binder机制为每个进程分配了UID/PID 且在 Binder 通信时会根据 UID/PID 进行有效性检测。

      • 传统 IPC 访问接入点是开放的,无法建立私有通道,只要知道这些接入点的程序都可以和对端建立连接,我们无法阻止恶意程序通过猜测接收方地址获得连接;而 Binder 机制可以看成 Server 提供的实现某个特定服务的访问接入点。

    • 实现 C/S 架构方便:Linux 的众 IPC 方式除了 Socket 以外都不是基于 C/S 架构,而 Socket 主要用于网络间的通信且传输效率较低。Binder基于C/S 架构 ,Server 端与 Client 端相对独立,稳定性较好。

    c. Binder 通信模型

    Binder 框架定义了四个角色:Server,Client,ServiceManager以及Binder驱动。

    其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间;其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。关系如图:


    Binder 通信模型关系
    • Binder 驱动:

      • 它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作
      • 负责进程之间 Binder 通信的建立,数据包在进程之间的传递和交互等一系列底层支持
      • 驱动和应用程序之间定义了一套接口协议,主要功能由 ioctl() 接口实现,不提供 read(),write() 接口,因为 ioctl() 灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用 write() 和 read()
      • Binder驱动的代码位于linux目录的drivers/misc/binder.c中
    • ServiceManager:作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 名字获得对 Server 中 Binder 实体的引用

    • Server&Client:服务器&客户端。在 Binder 驱动和 Service Manager 提供的基础设施上,进行 Client-Server 之间的通信。

    Binder 通信过程如下:


    Binder 通信过程

    d. 代理模式Proxy

    在数据流经 Binder 驱动的时候驱动会对数据做一层转换,当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把把请求参数交给驱动即可。如图:

    image.png

    参考文章:写给 Android 应用工程师的 Binder 原理剖析Android Bander设计与实现 - 设计篇


    五、Android 中的 IPC 方式

    a. Bundle:

    由于 Bundle 实现了 Parcelabe 接口,所以它可以在不同进程间传输。可以在 Bundle 中附加我们要传输的数据,然后通过 Intent 发送出去。

    我们传输的数据必须能够被序列化,比如基本类型、实现了 Parcelabe 接口或者 Serializable 接口的对象。

    思考一个特殊场景,若 A 进程需要完成一个计算,并在结束后启动 B 进程的一个组件并传递结果给 B 进程,但这个结果不能放入 Bundle。此时可以通过 Intent 启动 B 进程的 Service,让它在后台计算,从而避免进程间通信。


    b. 文件共享:

    通过文件共享,可以交换一些文本信息,还可以序列化一个对象到文件系统,同时从另一个进程中恢复这个对象。

    缺点:多进程并发读/写,读出的内容可能不是最新的;并发写更可能会产生冲突

    适用场景:对数据同步要求不高的进程之间通信,并妥善处理并发读/写的问题

    注意 SharedPreferences 是个特例,虽然它的本质也是文件的一种,但系统对它的读/写有一定的缓存策略,即内存中会有一份 SharedPreferences 的缓存,所以在多进程模式下,它的读/写就会变得不可靠。因此,不要在多进程中使用 SharedPreferences。


    c. Messenger

    概念:

    • Messenger 可译为信使,通过它能在不同进程传递 Message 对象,Message 可传递以下类型:
      • what、arg1、arg2:int 类型
      • 实现了 Parcelable 接口的对象
      • Bundle 对象
      • replyTo:Messenger 类型
    • 它是一种轻量级 IPC 方案,底层实现是 AIDL
    • 有两个构造函数,分别接受 Handler 对象和 Binder 对象

    实现方法:

    • 服务端进程

      • 创建一个 Service 来提供服务
      • 创建一个 Handler 来处理客户端发送的数据
      • 利用 Handler 来创建一个 Messenger
      • 在 onBind 中返回这个 Messenger 对应的底层 Bidner
    • 客户端进程:

      • 绑定服务端的 Service
      • 绑定成功后利用服务端返回的 IBinder 对象创建一个 Messenger,利用这个 Messenger 向服务端发送消息(至此仅能够完成单向通信)
      • 创建一个 Handler,并利用这个 Handler 创建 Messenger,然后把这个 Messenger 对象通过 Message 的 replyTo 参数传递给服务端,服务端就可以通过这个 replyTo 参数回应客户端了(完成了双向通信)
    Messenger 流程图

    缺点:**

    • 只能够传递消息,客户端无法调用服务端的方法
    • Messenger 是串行的,不适合高并发的场景

    d. AIDL

    基本使用:

    • 服务端:
      • 创建 Service 来监听客户端的连接请求
      • 创建 AIDL 文件,将暴露给客户端的接口在这个文件中声明
      • 在 Service 中实现这个 AIDL
    • 客户端:
      • 绑定服务端的 Service
      • 绑定成功后,将服务端返回的 Binder 对象转成 AIDL 接口所属的类,然后就可以使用 AIDL 中的方法

    使用观察者模式,客户端监听服务端数据变化:

    • 服务端:
      • 创建 AIDL 接口(因为 AIDL 中无法使用普通接口)
      • 在 AIDL 文件中增加注册与解注册的方法
      • 当数据改变时,在 Service 中通过这个接口通知客户端
    • 客户端:
      • 注册与解注册监听
      • 处理服务端的通知

    注意:因为 Binder 传输对象是通过序列化和反序列,所以客户端在注册和解注册传到服务端的 listener 不是同一个对象,导致服务端找不到注册时的 listener 最终解注册失败。

    解决办法:使用 RemoteCallbackList 来删除跨进程的 listener 接口。虽然传到服务端的 listener 不是同一个,但是它们的底层 Binder 对象是同一个,RemoteCallbackList 能够遍历 Map 通过 Binder 对象找到注册时的那个 listener 从而解注册。而且它还能够在客户端进程终止后,自动移除客户端注册的 listener。

    AIDL 文件支持的数据类型:

    • 基本数据类型
    • String 和 CharSequence
    • List:只支持 ArrayList,里面每个元素都必须能被 AIDL 支持
    • Map:只支持 Map,里面每个元素都必须能被 AIDL 支持,包括 key 和 value
    • 所有实现 Parceable 接口的对象
    • 所有 AIDL 接口本身

    注意:

    1. 自定义的 Parcelable 对象和 AIDL 对象需要显式 import 到当前的 AIDL 文件中
    2. 如果 AIDL 文件用到了 Parcelable 对象,那么需要新建一个同名的 AIDL 文件并在其中声明它为 Parcelable 类型
    3. AIDL 中除了基本数据类型,其他参数必须加上方向:in、out 或者 inout
    4. CopyOnWriteArrayList,支持并发读/写,会在 Binder 中形成 ArrayList 传递给客户端
    可能产生 ANR 的场景:
    • 对于客户端:

      • 客户端调用远程服务的方法,被调用的方法运行在服务端的 Binder 线程中,同时客户端会被挂起,如果此时服务端的方法比较耗时,就会导致客户端线程长时间阻塞
      • 客户端的 onServiceConnected 和 onServiceDisconnected 方法都运行在 UI 线程中,所以也不可以在它们里面直接调用服务端的耗时方法
    • 对于服务端:

      • 服务端的 AIDL 方法本身就运行在服务端的 Binder 线程,可在其中执行耗时操作,无需再开启子线程

      • 远程服务端调用客户端的 listener 中的方法时,被调用的方法运行在客户端的 Binder 线程池中,所以同样不可以在服务端中去调用客户端的耗时方法

    总结:不要在主线程调用另一端的耗时方法


    e. ContentProvider

    ContentProvider 是 Android 中提供的专门用于不同应用进行数据共享的方式,和 Messenger 一样,ContentProvider 的底层实现同样也是 Binder。

    注意:

    • ContentProvider 的 onCreate() 方法运行在主线程,所以不能做耗时操作。query()、delete()、insert()、update()、getType() 方法都运行在 Binder 线程池中

    • query()、delete()、insert()、update() 四大方法存在多线程并发访问,所以需要做好线程同步

    • 一个 SQLiteDatabase 内部对数据库的操作有同步处理,但是多个 SQLiteDatabase 对象来操作数

      据库就无法保证线程同步了


    f. Socket

    Socket 也称为“套接字”,不仅可以实现进程间通信,还可以实现设备间通信,两种形式:

    • 流式套接字,对应 TCP 协议,提供稳定的双向通信功能,TCP 连接的建立需要经过“三次握手”
    • 用户数据报套接字,对应 UDP 协议,提供不稳定的单向通信功能

    使用 Socket 来进行通信,要注意两点:

    • 需要声明权限
    <uses-permission android:name="android.permission.INTERNET" />  
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 
    
    • 不能在主线程中访问网络,因为网络操作可能是耗时的

    六、Binder 连接池的概念

    a. 背景:按照 AIDL 的流程,如果有 100 个业务模块使用 AIDL 来进行进程间通信,就需要创建 100 个 Service,然后在 100 个 Service 的 onBind 方法返回对应的 binder 对象。

    Service 是四大组件之一,这么多的 Service 会浪费大量的系统资源,所以需要减少 Service 的数量,将所有的 AIDL 放在同一个 Service 中去管理。

    b. 作用:将每个业务模块的 Binder 请求统一转发到远程 Service 中去执行,从而避免了重复创建 Service 的过程,工作原理如图:


    Binder 连接池工作原理

    c. 实现:

    • 每个业务模块创建自己的 AIDL 接口并实现此接口
    • 为连接池创建 AIDL 接口,提供并实现 queryBinder() 方法,根据不同业务返回 Binder 对象
    • 远程服务在 onBind() 处返回连接池的的 binder 对象
    • 客户端拿到所需的 Binder 对象进行远程方法调用

    七、选用合适的 IPC 方式
    选用合适的 IPC 方式

    相关文章

      网友评论

        本文标题:开发艺术之IPC

        本文链接:https://www.haomeiwen.com/subject/mgaenctx.html