在Android的开发过程中,涉及到进程间通信的逻辑,通常都会使用Binder来实现,在学习Linux系统时,我们接触过pipe、socket、共享内存、消息队列等方式,那么为什么Android不使用这些原有的技术,而要开发Binder这种新的进程间通信机制呢?
为什么用Binder
1.历史原因
Binder最早是为一个名叫Palm Cobaltw的内核操作系统设计的,后来,Palm Cobaltw移植到了Linux系统上。Google在组建Android开发团队的时候,聘请了一位叫做Dianne Hackborn的工程师,她之前是Palm Cobaltw的核心成员,在做android进程间通信的时候,发现Binder非常合适,于是就理所当然得用起了Binder
2.性能原因
Android是移动设备,相对于PC的性能会有差异,Socket是一个通用接口,传输效率低,开销大;共享内存,虽然传输时不需要拷贝数据,但是控制机制复杂;管道和消息队列,采用存储转发方式,至少需要拷贝2次数据,效率低;而Binder中复杂数据类型传递可以复用内存,只需要拷贝一次数据。
3.安全原因
Binder支持通信双方进行身份校验,这极大得保障了安全性;Android为每个安装好的应用程序分配了自己的UID,进程的UID是鉴别进程身份的重要标志。传统的IPC要发送类似的UID只能放在数据包里,容易被拦截,遭到恶意进攻,socket则需要暴露自己的ip和端口,知道这些,恶意程序就可以肆意接入。
Binder跨进程通信原理
Android进程间通信的方式有很多种,比如Messenger、SharePreference、AIDL、Socket和Content Provider等,其中Messenger、AIDL和Content Provider的底层都是依赖Binder机制去实现的。从字面上看,Binder是胶水的意思,Binder的职责是在不同的进程间扮演一个桥梁的角色,让他们可以互相通信。
Linux IPC原理
在分析Binder的实现原理之前,先来看一下Linux中跨进程通信是如何实现的:
进程隔离是为保护操作系统中进程互不干扰而设计的一组不同硬件和软件的技术。这个技术是为了避免进程 A 写入进程 B 的情况发生。 进程的隔离实现,使用了虚拟地址空间。进程 A 的虚拟地址和进程B的虚拟地址不同,这样就防止进程 A 将数据信息写入进程 B。
Linux在逻辑上将虚拟内存分为用户空间(User Space)和内核空间(Kernel Space),普通应用程序运行在用户空间,系统内核运行在内核空间,为了控制应用程序的访问范围、保证系统安全,用户空间只能通过系统调用的方式去访问内核空间。当进程执行系统调用而陷入内核代码的时候,该进程则进入了内核态,相比之下,进程在用户空间执行自己的代码的时候,则是处于用户态。
系统调用主要通过两个函数来实现:
copy_from_user() //将数据从用户空间拷贝到内核空间
copy_to_user() //将数据从内核空间拷贝到用户空间
传统IPC的做法:
数据发送方进程将数据放在内存缓存区,通过系统调用进入内核态
内核程序在内核空间开辟一块内核缓存区,通过 copy_from_user 函数将数据从数据发送方用户空间的内存缓存区拷贝到内核空间的内核缓存区中
数据接收方进程在自己的用户空间开辟一块内存缓存区
内核程序将内核缓存区中通过 copy_to_user 函数将数据拷贝到数据接收方进程的内存缓存区
这种进程间通信有两个问题:
1、性能低下,一次数据传输需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝,浪费时间;
2、事先不知道需要开辟多大的内存用于存放数据,因此需要开辟尽可能大的空间或者事先调用 API 来解决这个问题,浪费空间。
Binder IPC原理
进程间的通讯少不了Linux内核的支持,Binder并不属于内核的一部分,好在Linux有LKM(Loadable Kernel Module)机制:模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。Binder作为模块存在于内核中,被称为Binder驱动。
Linux中有个概念:内存映射
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间,映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反映到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。
Binder IPC的做法:
1、Binder 驱动在内核空间创建一个数据接收缓存区;
2、在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
3、发送方进程通过系统调用 copy_from_user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
Binder 通信模型
Binder通信模型是基于C/S架构的,通信调用方进程称为 Client 进程,被调用方称为 Server 进程,除此之外还需要 ServiceManager 和 Binder 驱动的参与,它们都是通过 open/mmap/iotl 等系统调用来访问设备文件 dev/binder 来实现 IPC 过程的。
Binder驱动:就像路由器一样,是整个通信的核心;驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。
ServiceManager与实名Binder:ServiceManager和DNS类似,作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder的名字获得对Binder实体的引用。注册了名字的Binder叫实名Binder,就像网站一样除了有IP地址以外还有自己的网址。
ServiceManager也是一个进程,Service向ServiceManager注册Binder必然涉及进程间通信。ServiceManager有自己的Binder实体,这个Binder实体没有名字,不需要注册。当一个进程使用BINDER_SET_CONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体。
Client获取实名Binder的引用:Service向ServiceManager中注册Binder以后,Client就能通过名字获得Binder的引用了。
Binder通信过程
Binder通信过程如下:
一个进程使用 BINDER_SET_CONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
Binder的完整定义
从进程通信的角度看:Binder是一种进程间通信的机制
从Service进程的角度看:Binder是Server中的Binder实体对象
从Client进程的角度看:Binder是对Binder代理对象,是Binder实体对象的一个远程代理
从传输过程的角度看:Binder是一个可以跨进程传输的对象;Binder驱动会对这个跨进程边界的对象做一点点特殊处理,自动完成代理对象和本地对象的转换
ASDL的使用
AIDL的简介
AIDL (Android Interface Definition Language) 是一种接口定义语言,用于生成可以在Android设备上两个进程之间进行进程间通信(interprocess communication, IPC)的代码。如果在一个进程中(例如Activity)要调用另一个进程中(例如Service)对象的操作,就可以使用AIDL生成可序列化的参数,来完成进程间通信。
简单说来,AIDL能够实现进程间通信,其内部是通过Binder机制来实现的,具体介绍这里就略过了。
AIDL的具体使用
AIDL的实现一共分为三部分,一部分是客户端,调用远程服务。一部分是服务端,提供服务。最后一部分,也是最关键的是AIDL接口,用来传递的参数,提供进程间通信。
先在服务端创建AIDL部分代码。在Android Studio中新建AIDL文件
默认生成格式
interface IBookManager { /**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/ void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString);}
默认如下格式,由于本例要操作Book类,实现两个方法,添加书本和返回书本列表。
定义一个Book类,实现Parcelable接口。
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book() {
}
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
public int getBookId() {
return bookId;
}
public void setBookId(int bookId) {
this.bookId = bookId;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.bookId);
dest.writeString(this.bookName);
}
protected Book(Parcel in) {
this.bookId = in.readInt();
this.bookName = in.readString();
}
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
@Override
public Book createFromParcel(Parcel source) {
return new Book(source);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
}
由于AIDL只支持数据类型:基本类型(int,long,char,boolean等),String,CharSequence,List,Map,其他类型必须使用import导入,即使它们可能在同一个包里,比如上面的Book。
最终IBookManager.aidl 的实现
// Declare any non-default types here with import statements
import com.lvr.aidldemo.Book;
interface IBookManager {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
void addBook(in Book book);
List<Book> getBookList();
}
注意:如果自定义的Parcelable对象,必须创建一个和它同名的AIDL文件,并在其中声明它为parcelable类型。
** Book.aidl**
// Book.aidl
package com.lvr.aidldemo;
parcelable Book;
以上就是AIDL部分的实现,一共三个文件。
然后Make Project ,SDK为自动为我们生成对应的Binder类。在build文件夹下可以找到IBookManager.class文件
其中该接口中有个重要的内部类Stub ,继承了Binder 类,同时实现了IBookManager接口。 这个内部类是接下来的关键内容。
public static abstract class Stub extends android.os.Binder implements com.lvr.aidldemo.IBookManager{}
服务端
服务端首先要创建一个Service用来监听客户端的连接请求。然后在Service中实现Stub 类,并定义接口中方法的具体实现。
**//实现了AIDL的抽象函数**
private IBookManager.Stub mbinder = new IBookManager.Stub() {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
//什么也不做
}
@Override
public void addBook(Book book) throws RemoteException {
//添加书本
if(!mBookList.contains(book)){
mBookList.add(book);
}
}
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
};
当客户端连接服务端,服务端就会调用如下方法:
public IBinder onBind(Intent intent) {
return mbinder;
}
就会把Stub实现对象返回给客户端,该对象是个Binder对象,可以实现进程间通信。 本例就不真实模拟两个应用之间的通信,而是让Service另外开启一个进程来模拟进程间通信。
<service
android:name=".MyService"
android:process=":remote">
<intent-filter>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="com.lvr.aidldemo.MyService"/>
</intent-filter>
</service>
android:process=":remote"设置为另一个进程。<action android:name="com.lvr.aidldemo.MyService"/>是为了能让其他apk隐式bindService。通过隐式调用的方式来连接service,需要把category设为default,这是因为,隐式调用的时候,intent中的category默认会被设置为default。
Intent intentService = new Intent();
intentService.setAction("com.lvr.aidldemo.MyService");
intentService.setPackage(getPackageName());
intentService.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
MyClient.this.bindService(intentService, mServiceConnection, BIND_AUTO_CREATE);
Toast.makeText(getApplicationContext(),"绑定了服务",Toast.LENGTH_SHORT).show();
将服务端返回的Binder对象转换成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
if(mIBookManager!=null){
try {
mIBookManager.addBook(new Book(18,"新添加的书"));
Toast.makeText(getApplicationContext(),mIBookManager.getBookList().size()+"",Toast.LENGTH_SHORT).show();
} catch (RemoteException e) {
e.printStackTrace();
}
}
完整的代码放在GitHub上面,感兴趣的小伙伴可以看看。源码地址:
https://github.com/sherry1990smile/BinderDemo
总结
Binder使用Client-Server通信方式,安全性好,简单高效,再加上其面向对象的设计思想,独特的接收缓存管理和线程池管理方式,成为Android进程间通信的中流砥柱。