通过引入相关问题来进行记录思考:
1.简述 IPC?
IPC 就是指进程之间的通信机制,在 Android 系统中启动 Activity/Service 等都涉及跨进程调用的过程。
2.Android中的IPC 方式有哪些:
Bundle,文件共享,Messenger,AIDL,ContentProvider,Socket等
3.如何开启Android中的多进程:
- (常规)在
AndroidMenifest
中给四大组件指定属性android:process
。- (不常规)通过
JNI
在native层fork一个新的进程。进程名的默认规则:
默认进程:
- 没有指定该属性则运行在默认进程,其进程名就是包名。
以“:”开头的进程:
- 省略包名,如
android:process=":remote"
,表示进程名为com.example.myapplication:remote
。- 属于当前应用的私有进程,其他进程的组件不能跟它跑在同一进程中。
完整命名的进程:
- 如
android:process="com.example.myapplication.remote"
。- 属于全局进程,其他应用可以通过
**ShareUID**
方式和它跑在用一个进程中。
UID
: Android系统会为每个应用分配一个唯一的UID
,具有相同的UID
才能共享数据。两个应用通过
ShareUID
跑在同一个进程中的条件:具有相同的ShareUID
和签名。- 满足上述条件的两个应用,无论是否跑在同一进程,它们可共享data目录,组件信息。
- 若跑在同一进程,它们除了可共享data目录、组件信息,还可共享内存数据。它们就像是一个应用的两个部分。
4.多进程的运行机制会导致哪些问题?
Android为每一个进程分配了一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,这也导致了不同虚拟机中访问同一个对象会产生多份副本。
一般来说使用多进程会带来以下四个方面的问题:
静态变量和单例模式失效 原因:不同虚拟机没有存储在同一个空间上;
线程同步机制失效 原因:线程处于不同的进程,无法同步
SharedPreference
的可靠性下降 原因:底层是通过读写XML文件实现的,发生并发问题。Application多次创建- 原因:Android系统会为新的进程分配独立虚拟机,相当于应用重新启动了一次。
5.为什么要设计 Binder?
在Linux中,进程通信的方式肯定不止Binder这一种,还有以下这些:
管道(Pipe)
信号(Signal)
消息队列(Message)
共享内存(Share Memory)
套接字(Socket)Binder在这之后主要有以下优点:
效率上 :Socket 作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。Binder 只需要一次数据拷贝,性能上仅次于共享内存。
稳定性:Binder 基于 C|S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,稳定性好。共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder 机制是优于内存共享的。
安全性:Binder 通过在内核层为客户端添加身份标志 UID|PID,来作为身份校验的标志,保障了通信的安全性。 传统 IPC 访问接入点是开放的,无法建立私有通道。比如,命名管道的名称,SystemV 的键值,Socket 的 ip 地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,无法阻止恶意程序通过猜测接收方地址获得连接。注:熟悉Zygote的朋友可能知道,在fork()进程的时候,也就是向Zygote进程发出创建进程的消息的时候,用到的进程间通信方式就不是Binder了,而换成了Socket,这主要是因为fork不允许存在多线程,Binder通讯偏偏就是多线程。
6.Binder通信过程和原理
首先要明确的是客户端进程是无法直接操作服务端中的类和方法的,因为不同进程之间是不共享资源的。所以客户端这边操作的只是服务端进程的一个代理对象,也就是一个服务端的类引用,也就是Binder引用。
总体通信流程就是:
客户端通过代理对象向服务器发送请求。
代理对象通过Binder驱动发送到服务器进程
服务器进程处理请求,并通过Binder驱动返回处理结果给代理对象
代理对象将结果返回给客户端。
再看看在我们应用中常常用到的工作模型,上图:
这就是在应用层面我们常用的工作模型,通过ServiceManager去获取各种系统进程服务。这里的通信过程如下:
服务端跨进程的类都要继承Binder类,所以也就是服务端对应的Binder实体。这个类并不是实际真实的远程Binder对象,而是一个Binder引用(即服务端的类引用),会在Binder驱动里还要做一次映射。
客户端要调用远程对象函数时,只需把数据写入到Parcel,在调用所持有的Binder引用的transact()函数
transact函数执行过程中会把参数、标识符(标记远程对象及其函数)等数据放入到Client的共享内存,Binder驱动从Client的共享内存中读取数据,根据这些数据找到对应的远程进程的共享内存。
然后把数据拷贝到远程进程的共享内存中,并通知远程进程执行onTransact()函数,这个函数也是属于Binder类。
远程进程Binder对象执行完成后,将得到的写入自己的共享内存中,Binder驱动再将远程进程的共享内存数据拷贝到客户端的共享内存,并唤醒客户端线程。
所以通信过程中比较重要的就是这个服务端的Binder引用,通过它来找到服务端并与之完成通信。
看到这里可能有的人疑惑了,图中线程池怎么没用到啊?
可以从第一张图中看出,Binder线程池位于服务端,它的主要作用就是将每个业务模块的Binder请求统一转发到远程Servie中去执行,从而避免了重复创建Service的过程。也就是服务端只有一个,但是可以处理多个不同客户端的Binder请求。
7.说下你对进程与线程的理解
一个进程就是一个执行单元,在 Android 中,一个应用默认只有一个进程,每个进程都有自己独立的资源和内存空间,其它进程不能任意访问当前进程的内存和资源,系统给每个进程分配的内存会有限制。实现的方式很简单就是在 Manifest 中注册 Activity 等的时候,使用
process
属性指定一个进程即可。process 分私有进程和全局进程,以:
号开头的属于私有进程,其他应用组件不可以和他跑在同一个进程中;不以:
号开头的属于全局进程,其他应用可以通过 ShareUID 的方式和他跑在同一个进程中Android 系统启动的时候会先启动 Zygote 进程,当我们需要创建应用程序进程的时候的会通过 Socket 与之通信,Zygote 通过 fork 自身来创建我们的应用程序的进程。
线程是 CPU 调度的最小单元,一个进程可包含多个线程。Java 线程的实现是基于一对一的线程模型,即通过语言级别层面程序去间接调用系统的内核线程。内核线程由操作系统内核支持,由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程。由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器。这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。