跨进程通信和Binder机制

Android中进程和线程的关系和区别

  • 线程是CPU调度的最小单元,同时线程是一种有限的系统资源;而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。
  • 进程有自己独立的地址空间,而进程中的线程共享此地址空间,都可以并发执行。
  • 一般来说,一个App程序至少有一个进程,一个进程至少有一个线程(包含与被包含的关系),通俗来讲就是,在App这个工厂里面有一个进程,线程就是里面的生产线,但主线程(即主生产线)只有一条,而子线程(即副生产线)可以有多个。

为何需要进程间通信(IPC)?多进程通信可能会出现的问题?

因为所有运行在不同进程的四大组件(Activity、Service、Receiver、ContentProvider)共享数据都会失败,这是由于==Android为每个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这会导致在不同的虚拟机中访问同一个类的对象会产生多份副本==。比如常用例子(通过开启多进程获取更大内存空间、两个或者多个应用之间共享数据、微信全家桶)。为了解决数据共享问题需要进行IPC。

一般来说,使用多进程通信会造成如下几方面的问题:

  • 静态成员和单例模式完全失效:独立的虚拟机造成。
  • 线程同步机制完全失效:独立的虚拟机造成。
  • SharedPreferences的可靠性下降:这是因为Sp不支持两个进程并发进行读写,有一定几率导致数据丢失。
  • Application会多次创建:Android系统在创建新的进程时会分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,自然也会创建新的Application。

Android中IPC方式、各种方式优缺点?

跨进程通信和Binder机制_第1张图片
image.png

还有==广播==

Bundle传递对象为什么需要序列化?Serialzable和Parcelable的区别?

==因为bundle传递数据时只支持基本数据类型,所以在传递对象时需要序列化转换成可存储或可传输的本质状态(字节流)==。序列化后的对象可以在网络、IPC(比如启动另一个进程的Activity、Service和Reciver)之间进行传输,也可以存储到本地。
序列化实现的两种方式:实现Serializable/Parcelable接口。不同点如图:

跨进程通信和Binder机制_第2张图片
image.png

AIDL以及优化多模块使用AIDL的情况

AIDL(Android Interface Definition Language,Android接口定义语言):如果在一个进程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它客户端可以实现间接调用服务端对象的方法。
AIDL的本质是系统提供了一套可快速实现Binder的工具。关键类和方法:

  • AIDL接口:继承IInterface。
  • Stub类:Binder的实现类,服务端通过这个类来提供服务。
  • Proxy类:服务端的本地代理,客户端通过这个类调用服务端的方法。
  • asInterface():客户端调用,将服务端返回的Binder对象,转换成客户端所需要的AIDL接口类型的对象。如果客户端和服务端位于同一进程,则直接返回Stub对象本身,否则返回系统封装后的Stub.proxy对象。
  • asBinder():根据当前调用情况返回代理Proxy的Binder对象。
  • onTransact():运行在服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。
  • transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。

多模块使用AIDL优化

  • 当有多个业务模块都需要AIDL来进行IPC,此时需要为每个模块创建特定的aidl文件,那么相应的Service就会很多。必然会出现系统资源耗费严重、应用过度重量级的问题。解决办法是建立Binder连接池,即将每个业务模块的Binder请求统一转发到一个远程Service中去执行,从而避免重复创建Service。
  • 原理:每个业务模块创建自己的AIDL接口并实现此接口,然后向服务端提供自己的唯一标识和其对应的Binder对象。服务端只需要一个Service并提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对象,不同的业务模块拿到所需的Binder对象后就可以进行远程方法的调用了。
     private final BookController.Stub stub = new BookController.Stub() {

        @Override
        public List getBookList() throws RemoteException {
            return bookList;
        }

        @Override
        public void addBookInOut(Book book) throws RemoteException {
            if (book != null) {
                book.setName("服务器改了新书的名字 InOut");
                bookList.add(book);
            } else {
                Log.e(TAG, "接收到了一个空对象 InOut");
            }
        }

    };
    ...
    
    @Override
    public IBinder onBind(Intent intent) {
        String type = intent.getStringExtra("type")
        if("one".isEqual(type)){
            return stub;
        }else{
            return stub1;
        }
        ...
    }

AIDL使用

服务端

  • 创建实体类.aidl和对应的实体类.java文件。

Book.aidl

package com.wu.test.aidl;
parcelable Book;

Book.java

public class Book implements Parcelable {
//实体字段、实现序列化方法
}
  • 定义接口
interface BookController {

    List getBookList();

    void addBookInOut(inout Book book);

}

创建或修改过AIDL文件后需要clean下工程,使系统及时生成我们需要的文件

  • 创建Service供客户端调用
public class AIDLService extends Service {
...
private final BookController.Stub stub = new BookController.Stub() {

        @Override
        public List getBookList() throws RemoteException {
            return bookList;
        }

        @Override
        public void addBookInOut(Book book) throws RemoteException {
            if (book != null) {
                book.setName("服务器改了新书的名字 InOut");
                bookList.add(book);
            } else {
                Log.e(TAG, "接收到了一个空对象 InOut");
            }
        }

    };

    @Override
    public IBinder onBind(Intent intent) {
        return stub; //返回binder对象Stub
    }
}

客户端

  • 把服务端的AIDL文件以及Book类复制过来,将 aidl 文件夹整个复制到和Java文件夹同个层级下,不需要改动任何代码。创建和服务端Book类所在的相同包名来存放 Book类。
  • 绑定服务,获取服务端的binder接口对象。
...
private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            bookController = BookController.Stub.asInterface(service); //绑定服务后,拿到接口对象
            connected = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            connected = false;
        }
    };
    
    //绑定服务
    private void bindService() {
        Intent intent = new Intent();
        intent.setPackage("com.wu.test");
        intent.setAction("com.wu.test.action");
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

aidl文件只是用来定义C/S交互的接口,Android在编译时会自动生成相应的Java类,生成的类中包含了Stub和Proxy静态内部类,用来封装数据转换的过程,实际使用时只关心具体的Java接口类即可。为什么Stub和Proxy是静态内部类呢?这其实只是为了将三个类放在一个文件中,提高代码的聚合性。

AIDL的使用

为何选择Binder方式

在讨论这个问题之前,我们知道Android也是基于Linux内核,Linux现有的进程通信手段有以下几种:

  • 管道:在创建时分配一个page大小的内存,缓存区大小比较有限;
  • 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
  • 共享内存:无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
  • 套接字:作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信;
  • 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;

既然有现有的IPC方式,==为什么重新设计一套Binder机制呢==。主要是出于三个方面的考量:

  • 效率:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。从Android进程架构角度分析:对于消息队列、Socket和管道来说,数据先从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区,一共两次拷贝。
    对于Binder来说,数据从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,节省了一次数据拷贝的过程,如图:

  • 稳定性:共享内存不需要拷贝,Binder的性能仅次于共享内存。共享内存的性能优于Binder,那为什么不采用共享内存呢,因为共享内存需要处理并发同步问题,容易出现死锁和资源竞争,稳定性较差。Socket虽然是基于C/S架构的,但是它主要是用于网络间的通信且传输效率较低。Binder基于C/S架构 ,Server端与Client端相对独立,稳定性较好。

  • 安全性:传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Binder机制为每个进程分配了UID/PID,且在Binder通信时会根据UID/PID进行有效性检测。

Binder机制的作用和原理

Linux系统将一个进程分为用户空间和内核空间。对于进程之间来说,用户空间的数据不可共享,内核空间的数据可共享,为了保证安全性和独立性,一个进程不能直接操作或者访问另一个进程,即Android的进程是相互独立、隔离的,这就需要跨进程之间的数据通信方式。普通的跨进程通信方式一般需要2次内存拷贝,如下图所示

跨进程通信和Binder机制_第3张图片
image.png

==一次完整的 Binder IPC 通信过程==通常是这样:

  • 首先 Binder 驱动在内核空间创建一个数据接收缓存区。
  • 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系。(Binder IPC 机制中涉及到的==内存映射通过 mmap() 来实现==,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。)
  • 发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
跨进程通信和Binder机制_第4张图片
image.png
数据接收缓冲区映射如何实现?

==Binder驱动实现了mmap()系统调用==,这对字符设备是比较特殊的,因为mmap()通常用在有物理存储介质的文件系统上,而象Binder这样没有物理介质,纯粹用来通信的字符设备没必要支持mmap()。Binder驱动当然不是为了在物理介质和用户空间做映射,而是用来创建数据接收的缓存空间。先看mmap()是如何使用的:

fd = open("/dev/binder", O_RDWR);

mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);

这样Binder的接收方就有了一片大小为MAP_SIZE的接收缓存区。==mmap()的返回值是内存映射在用户空间的地址==,不过这段空间是由驱动管理,用户不必也不能直接访问(映射类型为PROT_READ,只读映射)。
==mmap()分配的内存除了映射进了接收方进程里,还映射进了内核空间==。所以调用copy_from_user()将数据拷贝进内核空间也相当于拷贝进了接收方的用户空间,这就是Binder只需一次拷贝的‘秘密’。

Binder框架中ServiceManager的作用

Binder框架 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder驱动,其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。如下图所示:

跨进程通信和Binder机制_第5张图片
image.png
  • Server&Client:服务器&客户端。在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。
  • ServiceManager(如同DNS域名服务器)服务的管理者,==将Binder名字转换为Client中对该Binder的引用,使得Client可以通过Binder名字获得Server中Binder实体的引用==。
  • Binder驱动(如同路由器):负责进程之间binder通信的建立,计数管理以及数据的传递交互等底层支持。

Service Manager是Binder进程间通信的核心之一,它扮演着Binder进程间通信机制上下文管理者的角色,同时负责管理系统的Service组件,并且向Client组件提供获取Service代理对象的服务。
==Service Manager运行在一个独立进程中,因此和Client和Service通信也是运用Binder进程间通信机制来交互的==。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信。

==Service Manager主要是负责如下==:

  1. 打开和映射Binder设备文件;
  2. 注册为Binder上下文管理者;
  3. 循环等待Client进程请求;
  4. 管理ServiceManager代理对象;
  5. Service组件创建,同时需要在注册ServiceManager;
  6. 管理Service代理对象;

总结图

跨进程通信和Binder机制_第6张图片
image.png
Binder进程间通信模型
跨进程通信和Binder机制_第7张图片
image.png

Client进程组件、Service进程组件和ServiceManager运行在用户空间,而Binder驱动程序运行在Linux系统内核空间,其中Client进程组件、Service进程组件和Service Manager均是通过调用Open、mmap和ioctl来访问虚拟设备文件/dev/binder,从而实现与Binder驱动程序的交互,进而能够间接地执行进程通信
Binder就是一种把这四个组件粘合在一起的粘结剂了,其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。

==Binder具体是如何通信的==?
跨进程通信和Binder机制_第8张图片
image.png

Binder实体对象、Binder引用对象、Binder本地对象和Binder代理对象交互过程:

  1. 运行在Client中Binder代理对象通过Binder驱动程序向运行在Server中的Binder本地对象发出一个进程间通信请求,Binder驱动程序接着根据Client传递过来的Binder代理对象的句柄值来找到对应的Binder引用对象。
  2. Binder驱动程序根据前面找到的Binder引用对象找到对应的Binder实体对象,并且创建一个事务来描述该次进程间通信过程。
  3. Binder驱动程序根据前面找到的Binder实体对象来找到运行在server中的Binder本地对象,并且将Client传递过来的通信数据发送给它处理。
  4. Binder本地对象处理完成Client进程的通信请求之后,就将通信结果返回给Binder驱动程序,Binder驱动程序接着找到前面创建的一个事务。
  5. Binder驱动程序根据前面找到的事务的相关属性来找到发出通信请求的Client,并且通知Client将通信结果返回给对应的Binder代理对象处理。
Binder 通信中的代理模式

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

当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。

跨进程通信和Binder机制_第9张图片
image.png

Binder 的完整定义

  • 从进程间通信的角度看,Binder 是一种进程间通信的机制;
  • 从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
  • 从 Client 进程的角度看,Binder 指的是 Binder 代理对象,是 Binder 实体对象的一个远程代理;
  • 从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。

https://blog.csdn.net/AndroidStudyDay/article/details/93749470

你可能感兴趣的:(跨进程通信和Binder机制)