一、如何开启多进程模式
我们可以给四大组件在 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 接口的比较:
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、管道、消息队列来说,数据先从发送方缓存区拷贝到内核缓存区中,然后再从内核缓存区拷贝到接收方缓存区,一共两次拷贝,如图:
而对于 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 是串行的,不适合高并发的场景
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 接口本身
注意:
- 自定义的 Parcelable 对象和 AIDL 对象需要显式 import 到当前的 AIDL 文件中
- 如果 AIDL 文件用到了 Parcelable 对象,那么需要新建一个同名的 AIDL 文件并在其中声明它为 Parcelable 类型
- AIDL 中除了基本数据类型,其他参数必须加上方向:in、out 或者 inout
- 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 对象进行远程方法调用
网友评论