Binder
昨天从源码角度看了Android9.0下Activity的启动流程,其中关于跨进程的调用,都使用到了Binder进行跨进程通信,那么今天来阅读下Android的FrameWork层怎么实现Binder的吧。
IPC
IPC是Inter Process Communication
的缩写,意思是进程间通信。在Android系统中,每个应用都运行在一条独立的进程上,具有自己的DVM实例,而且进程之间是相互隔离的,也就是说各进程之间是互相独立,不影响的,就算哪一个进程崩溃也,也不会影响别的进程。有利于提高系统的稳定性。
为什么要使用Binder
在Linux内核中,提供了几种跨进程IPC方式,管道pipe(在前面讲Android消息机制的时候,Looper与MessageQueue的底层就是用c++使用了管道机制)、消息队列、共享内存、信号量、信号、socket。
那么为什么Android要重新开发一个Binder,而不采用现有的几种方式来进行进程间IPC呢?
首先看一下这几种IPC各自的特点
通信方式 | 特点 |
---|---|
管道pipe | 在创建时分配一个page大小内存,缓存区大小受限 |
消息队列 | 信息复制两次,额外的CPU消耗,不合适频繁或信息量大的通信 |
共享内存 | 无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快,但进程的同步问题,操作系统无法实现,必须各进程间使用同步工具来解决 |
信号量 | 常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。主要作为进程间以及同一进程内不同线程之间的同步手段 |
信号 | 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死进程等 |
套接字Socket | 作为更通用接口,传输效率低,主要用于不同机器或跨网络的通信 |
那么使用Binder的优点在哪?
- 性能:Binder传输只需要一次copy,管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存,效率成倍增长。
- 安全性:Binder机制对于通信双方的身份是内核进行机校检支持,而socket只需要知道地址都可以连接,安全机制需要上层协议来处理。
- 易用性:共享内存不需要copy,性能高,可是使用太复杂,管道与消息队列还要进行包装,另外,Binder使用面向对象的设计,基于C/S架构,进行一次远程调用和直接调用本地调用一样,使用方便
- 需要管理跨进程传递的代理对象的生命周期,别的机制都无法完成,Binder驱动通过引用计数技术解决了此问题。
每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间,对应一个4GB的虚拟空间,其中3GB是用户空间,1GB是内核空间,当然是可以通过参数来调整的。对于用户空间,不同进程之间是隔离的,不能共享,而内核空间是可以共享的。当Client进程身Server进程通信时,可以利用进程间共享内核空间来完成底层通信工作。
Binder在Android中的运用
Binder在Android中使用非常广,可以说没有Binder,Android系统都将不存在,无论是四大组件的生命周期,还是view的工作机制,都使用了Binder。
那么,我们先尝试在程序开发中,怎么使用Binder来进行进程间通信。
在Android中使用Binder,主要是AIDL,那么先说下AIDL传递的参数类型
-
基本数据类型(除short类型外)
-
String、charSequence
-
List,map(List与Map承载的数据必须是AIDL支持的类型,或其他声明的AIDL对象)
-
实现了Parcelable接口的数据类型
使用示例
// Book.aidl
package com.apkcore.studdy;
parcelable Book;
//IBookController.aidl
package com.apkcore.studdy;
import com.apkcore.studdy.Book;
interface IBookController{
List<Book> getBookList();
void addBook(inout Book book);
void addBookIn(in Book book);
void addBookOut(out Book book);
}
复制代码
在代码中的目录结构是上面这样的。
然后先clean一次项目,在如图的目录下,可以看到AS自动生成了IBookController类,这个类的结构过会再看。
然后新建一个同一个包名下在实体类Book
package com.apkcore.studdy;
import android.os.Parcel;
import android.os.Parcelable;
public class Book implements Parcelable {
private String name;
public Book(String name) {
this.name = name;
}
//1 为aidl使用Out类型时,会使得客户端传送一个不包含任何数据的对象给服务端,但该对象不是直接为null,所以还是需要实例化Book,就需要一个无参构造函数
public Book() {
}
public String getName() {
return name == null ? "" : name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int describeContents() {
return 0;
}
@Override
public String toString() {
return "book name:" + name;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
}
//2 使用插件自动生成Parcelable只包含了writeToParcel,当使用InOut时会出错,需要指定readFromParcel方法
public void readFromParcel(Parcel dest) {
name = dest.readString();
}
protected Book(Parcel in) {
this.name = 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使用Out类型时,会使得客户端传送一个不包含任何数据的对象给服务端,但该对象不是直接为null,所以还是需要实例化Book,就需要一个无参构造函数
- 默认生成的模板类的对象只支持为 in 的定向 tag 。为什么呢?因为默认生成的类里面只有 writeToParcel() 方法,而如果要支持为 out 或者 inout 的定向 tag 的话,还需要实现 readFromParcel() 方法——而这个方法其实并没有在 Parcelable 接口里面,所以需要我们从头写,指定readFromParcel方法。具体为什么大家可以去看看:你真的理解AIDL中的in,out,inout么?
AIDLService的源码如下:
public class AIDLService extends Service {
private static final String TAG = "AIDLService";
private List<Book> mBookList;
public AIDLService() {
}
@Override
public void onCreate() {
super.onCreate();
mBookList = new ArrayList<>();
initData();
}
private void initData() {
Book book1 = new Book("aaa");
Book book2 = new Book("bbb");
Book book3 = new Book("cc");
Book book4 = new Book("ddddddd");
Book book5 = new Book("eee");
mBookList.add(book1);
mBookList.add(book2);
mBookList.add(book3);
mBookList.add(book4);
mBookList.add(book5);
}
private final IBookController.Stub mStub = new IBookController.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
if (book!=null){
book.setName("服务器更改了书的名字 Inout");
mBookList.add(book);
}else {
Log.d(TAG, "addBook 接收到了一个空对象 Inout");
}
}
@Override
public void addBookIn(Book book) throws RemoteException {
if (book!=null){
book.setName("服务器更改了书的名字 In");
mBookList.add(book);
}else {
Log.d(TAG, "addBook 接收到了一个空对象 In");
}
}
@Override
public void addBookOut(Book book) throws RemoteException {
if (book!=null){
book.setName("服务器更改了书的名字 Out");
mBookList.add(book);
}else {
Log.d(TAG, "addBook 接收到了一个空对象 Out");
}
}
};
@Override
public IBinder onBind(Intent intent) {
return mStub;
}
}
复制代码
给Service添加权限
<service
android:name=".AIDLService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.apkcore.aidl.action"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
复制代码
再新建一个客户端应用,把aidl文件复制过去,同样还是Book,必须保证他们的包名是一样的,目录结构如下
在MainActivity
public class MainActivity extends AppCompatActivity {
private static final String TAG = "Client";
private IBookController mIBookController;
private boolean connected;
private List<Book> mBookList;
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mIBookController = IBookController.Stub.asInterface(service);
connected = true;
}
@Override
public void onServiceDisconnected(ComponentName name) {
connected = false;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent();
intent.setPackage("com.apkcore.studdy");
intent.setAction("com.apkcore.aidl.action");
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
}
public void bt1(View view) {
if (connected) {
try {
mBookList = mIBookController.getBookList();
if (mBookList != null && mBookList.size() > 0) {
for (Book book : mBookList) {
Log.d(TAG, "现在服务端有的书: " + book);
}
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
public void bt2(View view) {
if (connected) {
Book book = new Book("newInOut");
try {
mIBookController.addBook(book);
Log.d(TAG, "bt2 向service发送了一本新书:" + book.getName());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
public void bt3(View view) {
if (connected) {
Book book = new Book("newIn");
try {
mIBookController.addBookIn(book);
Log.d(TAG, "bt3 向service发送了一本新书:" + book.getName());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
public void bt4(View view) {
if (connected) {
Book book = new Book("newOut");
try {
mIBookController.addBookOut(book);
Log.d(TAG, "bt4 向service发送了一本新书:" + book.getName());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
...
}
复制代码
分别点击调用inout,in,out按钮,可以得出结论:
AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。其中,数据流向是针对在客户端中的那个传入方法的对象而言的。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的参数为空的对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。
Binder运行机制分析
我们接下来看as给我们自动生成的IBookController类
/*
* This file is auto-generated. DO NOT MODIFY.
* Original file: G:\\GitHub\\Studdy\\app\\src\\main\\aidl\\com\\apkcore\\studdy\\IBookController.aidl
*/
package com.apkcore.studdy;
// Declare any non-default types here with import statements
//import com.apkcore.studdy.Book;
public interface IBookController extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
//内部类Stub,继承自Binder对象,这个内部类是需要在服务端手动实现的,并会通过onBind方法返回客户端
public static abstract class Stub extends android.os.Binder implements IBookController
{
//Binder的唯一标识,一般用当前Binder的类名表示
private static final String DESCRIPTOR = "com.apkcore.studdy.IBookController";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.apkcore.studdy.IBookController interface,
* generating a proxy if needed.
* 将服务端的Binder对象转换为客户端的所需的AIDL接口类型的对象,客户端拿到这个对象就可以远程访问服务端的方法
* 如果客户端和服务端位于同一进程,那么返回的就是服务端的Stub对象本身,否则就是系统封闭后的Stub.proxy 对象
*/
public static IBookController asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof IBookController))) {
return ((IBookController)iin);
}
return new Proxy(obj);
}
//用于返回当前Binder对象
@Override public android.os.IBinder asBinder()
{
return this;
}
/**
* 运行在服务端进程的Binder线程池中,当客户端进程发起远程请求时,远程请求会要求系统底层执行回调方法
* @param code 客户端进程请求访求标识符,服务端进程会根据该标识确定所请求的目标方法
* @param data 目标就去的参数,它是客户端进程传进来的,当我们调用addBook(Book book)时,这个Book就是
* @param reply 目标方法执行后的结果,将返回给客户端
*/
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
String descriptor = DESCRIPTOR;
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(descriptor);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(descriptor);
java.util.List<Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(descriptor);
Book _arg0;
//结合了下面in和out两个的细节
if ((0!=data.readInt())) {
_arg0 = Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
if ((_arg0!=null)) {
reply.writeInt(1);
_arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_addBookIn:
{
data.enforceInterface(descriptor);
Book _arg0;
//从输入的data流中读取book数据,并赋值给_arg0
if ((0!=data.readInt())) {
_arg0 = Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
//在这里才是真正的开始执行实际的逻辑,调用服务端写好的实现
this.addBookIn(_arg0);
//执行完后就结束了,没有reply流的操作
reply.writeNoException();
return true;
}
case TRANSACTION_addBookOut:
{
data.enforceInterface(descriptor);
Book _arg0;
//可以看到,out作为tag时,根本不从data读取数据,而是直接new
//这也是我们为什么在book里一定要写一个无参构造的原因
//这样,服务端当然也就不可能收到客户羰的数据了
_arg0 = new Book();
this.addBookOut(_arg0);
reply.writeNoException();
//在这里,_arg0是方法的传入参数,故服务端的实现里对传参做出了任何修改
//都会在_arg0体现出来,将它写入reply流,这样客户端就能收到了
if ((_arg0!=null)) {
reply.writeInt(1);
_arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
}
/**
* 内部代理类,实现了IBookController接口,
* 这个代理类就是服务端返回给客户端的AIDL接口对象,客户端可以通过这个代理类访问服务端的方法
*/
private static class Proxy implements IBookController
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public java.util.List<Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
//这是inout方法
@Override public void addBook(Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
//定向tag为InOut组合,结合了in和out的两个方法的操作
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
book.readFromParcel(_reply);
}
}
finally {
_reply.recycle();
_data.recycle();
}
}
@Override public void addBookIn(Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
//如果book不为空,data写入int值1,将book写入_data中
//如果为空,写入int值0
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
//之后调用transact方法,将方法编码
//_data(包含从客户端向服务端的book流)
//_reply(包含从服务端流向客户端的数据流)
mRemote.transact(Stub.TRANSACTION_addBookIn, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
@Override public void addBookOut(Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
//tag为out时,并没有将book对象写入_data流
mRemote.transact(Stub.TRANSACTION_addBookOut, _data, _reply, 0);
_reply.readException();
//与tag为in的方法里不同的是,在执行transact方法后,还有针对reply的操作,把book赋值给reply流中的数据
if ((0!=_reply.readInt())) {
book.readFromParcel(_reply);
}
}
finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_addBookIn = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_addBookOut = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
}
public java.util.List<Book> getBookList() throws android.os.RemoteException;
public void addBook(Book book) throws android.os.RemoteException;
public void addBookIn(Book book) throws android.os.RemoteException;
public void addBookOut(Book book) throws android.os.RemoteException;
}
复制代码
注释里很明白的分析了AIDL三个不同的tag时,为什么会有不同的结果。
上面的例子中,
- AIDLService:服务提供者
- Activity:服务调用者
- IBookController:负责管理服务,内部通过map集合来存储service与binder的映射关系。
- Binder驱动:这是IBookController连接各种Service的桥梁,同时也是客户端与服务端交流的桥梁
总结一下就是应用程序(Activity)首先向IBookController发送AIDLService的请求,IBookController查看已经注册在里面的服务的列表,找到相应的服务后,通过Binder驱动将其中的Binder对象返回给客户端,从而完成对服务的请求
IBookController是编译器通过我们自己写的AIDL文件,自动生成的,当然我们也可以手写,那样就可以不用写AIDL文件了,有兴趣的话可以手写一波。可以参考Android多进程之手动编写Binder类或者《Android开发艺术探索》
Binder连接池
如果项目变得很大了,现在拥有10个不同的业务模块需要使用AIDL,如果按照上面的做法,那么需要写10个Service,如果更多呢?Service是一种资源,我们需要将其进一步的优化。
在《Android开发艺术探索》中有提出一个很好的解决多AIDL方法,就是使用Binder连接池,受篇幅影响,这里只说下使用的流程:
- 为每个业务模块创建AIDL接口,以及实现其接口的方法
- 创建IBinderPool.aidl文件,定义queryBinder(in BinderCode)方法,客户端通过调用这个方法来返回特定的Binder对象
- 创建BinderPoolService服务端,在onBind方法返回实例化的BinderPool.IBinderPoolImpl对象
- 创建BinderPool类,在该类实现客户端与服务端的连接,解决线程同步问题,设置Binder的死亡代理等。在onServiceConnected()方法内,获取到IBinderPool的代理对象。此外,IBinderPool的实现类:IBinderPoolImpl是BinderPool的内部类,实现了IBinderPool.aidl的方法:queryBinder()。
具体实现可以参考:实现AIDL接口的Binder连接池
参考
为什么 Android 要采用 Binder 作为 IPC 机制?
《Android开发艺术探索》
下面是我的公众号,欢迎大家关注我