【Android】Binder 原理初探:理解 Android 进程通信机制

笔者刚开始学习的时候看的《安卓开发艺术探索》,看完之后感觉不太清楚难以理解。在此建议大家先去实战,去体验代码写的原因以及逻辑,然后再学习AIDL生成的binder代码感觉会好很多;

Binder的起因

  1. 程序都是由一些ActivityService组成的,那么这些组件可能在一个进程中,又可能不在一个进程中。
  2. 每一个进程都会有一个独立的虚拟机,每个虚拟机在内存分配上有不同的地址空间,所以当不同进程访问同一个类的对象时,这个类的对象会产生很多的副本,其他进程访问的是副本;
  3. 所以两个进程无法通信,此时binder应运而出;

Binder是什么

以下是《安卓开发艺术探索》中对于binder的介绍:

  1. Binder是Android中的一个类,继承了IBinder接口;

    IBinderAndroid中IPC的桥梁接口,是客户端和服务端通信的唯一桥梁对象;而Binder实现了这个接口;

  2. Binder可以理解为一种虚拟的物理设备,它的设备驱动是dev/binder

  3. Android FrameWork的角度说的话,Binder是连接ServiceManager和MS以及各种Manager的桥梁;

    1. Android FrameWork :是基于Linux以及应用层的系统框架层,包含了很多组件和服务,例如活动管理器,窗口管理器;
    2. ServiceManager:以startActivity()举例,系统启动时,它最早被启动。当AMS启动后,AMS会在ServiceManager注册服务名和b引用(binder),那么当startActivity()时,框架层找不到AMS,这个时候就会去ServiceManager查表,通过binder引用找到AMS,然后AMS会经过效验,去通过b引用去对应的进程中调用方法实现生命周期的管理,此外,AMS还会管理任务栈以及任务、应用进程等;
  4. 从应用层面来说,Binder是客户端和服务端通信的媒介,当binderService的时候,那么服务端会返回一个包含了服务端业务调用的binder对象,通过这个binder对象,客户端就可以访问到服务端提供的数据以及服务;(了解binder原理后,看这段话就可以理解了)

Binder的选择

Android继承于LinuxLinux本身提供了很多跨进程的机制,那么Android为什么会开发binder机制呢?

简要介绍Linux中的几种IPC机制:

共享内存

  1. 介绍:顾名思义,就是两个进程共享一片内存区域,当一个进程修改地址上的内容时,其他进程会察觉到更改;

  2. 安全和性能分析:

    1. 因为不需要数据的复制,直接访问的是同一片内存区域,所以是IPC中最快,性能最好的方式;
    2. 实现较为复杂,且安全性较低(因为任意的访问可能导致恶意修改或者隐私泄露);

管道

  1. 介绍:管道分为单向管道和双向管道,单向管道有固定的读端、写端,写进程通过写端向管道文件写数据,读进程通过读端从管道文件读数据;

  2. 安全和性能分析:

    1. 关于性能:数据复制了两遍;
    2. 管道分为阻塞和非阻塞,一般简单同步程序阻塞,这时如果读写操作不当,就会出现阻塞的情况;(比如管道里面没数据,读的进程要读,那么进程就会被挂起,也就是发生阻塞)
    3. 管道只能字节流;
    4. 管道有缓冲区,传输数据大小超过缓冲区了也会发生阻塞;

消息队列

  1. 介绍:存放在内核中的消息链表,消息队列由消息标识符表示,允许多个进程同时读和写,发送方和接收方要约定好消息体的大小和类型;
  2. 安全和性能分析:数据也是会经过两次复制;

Socket

  1. 介绍:原本是为了网络设计的,但是也可以进行进程间通信;

  2. 安全和性能分析:

    1. Socket有两个缓冲区,一读一写,那么进程A->写缓冲区复制一次,写缓冲区->读缓冲区是物理传输,读缓冲区->读进程B复制一次,一共复制两次;

Binder的优势

  1. 从性能角度:Binder的数据拷贝只需要一次,而管道,消息队列、Socket都需要2次,共享内存方式实现方式复杂;
  2. 从安全角度:传统的IPC机制对于通信双方没有严格的验证,Binder协议本身就支持双方做身份效验,提升了安全性;

Binder的实战

Binder的工作流程:

上面的图方便大家理解;;;

流程大致如下:

  1. 服务端:会创建Binder的实例,此时会开启隐藏的Binder线程(来接收客户端的请求),Binder驱动会创建mRemote对象;
  2. 客户端:调用bindService()方法后,Binder驱动会返回mRemote对象;那么此时客户端可以调用transact()方法通过Binder驱动向服务端发送请求,同时挂起线程;
  3. 服务端:收到数据后调用ontransact()方法,通过Binder驱动把结果返回给客户端;
  4. 客户端:收到数据之后,重新拉起线程;

通过Binder实现跨进程通信

功能就是通过名字查询名字对应的信息,重点在于实现的逻辑;

接口

java 复制代码
public interface Queuemes {
    
    String getMesName(String name);
}

接口里面写了我们要实现的功能;

服务端

java 复制代码
public class MyService extends Service {
  Binder binder;
  public MyService() {
      binder = new MyServiceNative();
  }

  @Override
  public IBinder onBind(Intent intent) {
      return binder;
  }
}

创建Serviece,在服务端里创建Binder实例;

onBind()return的是客户端接收的Binder对象;

java 复制代码
class MyServiceNative extends Binder implements Queuemes{

      @Override
      protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
          if (code == 1) {
              String name;
              name = data.readString();
              String res = getMesName(name);
              reply.writeString(res);
              return true;
          }
          return super.onTransact(code, data, reply, flags);
      }
      public String getMesName(String name) {
      String mes;
      switch (name){
          case "徐照茹":
              mes = "万事都灵";
              break;
          case "高远":
              mes = "我要进大厂(怒吼)";
              break;
          default:
              mes = "yws";
              break;
      }
      return mes;
  }

}

我们自定义了一个BinderMyServiceNative,它继承了Binder,实现了Queuemes接口;重写了 onTransact方法和实现的功能;

  1. 为什么要实现Queuemes接口:

    原因:如果客户端和服务端是同一个进程,不会走跨进程的过程,Binder内驱返回的是服务端中Binder的对象;但如果不是一个进程,那么会走跨进程的过程, Binder内驱返回的是Binder的代理(稍后会介绍到);

    结果:如果是同一个进程,那么客户端需要用到服务端的资源(这里指方法),所以实现我们想要的借口,重写方法,这样客服端拿到Binder对象之后才可以使用方法;

  2. onTransact()

    1. 参数:int code, Parcel data, Parcel reply, int flags

      code:用来标识请求的是哪一个方法,data:目标方法的参数,reply:目标方法的返回值,flags:0为同步,IBinder.FLAG_ONEWAY为异步;

    2. 介绍:运行在服务端Binder线程池中,如果此方法的返回值为false,表示客户端请求失败,以此可以做一些权限验证,提高了安全性;

    3. 过程:判断请求方法,通过data取得参数,处理后把结果写入reply,返回ture;

java 复制代码
<service
   android:name=".MyService"
   android:enabled="true"
   android:exported="true"
   android:process=":ljx"
   >
   <intent-filter>
       <action
           android:name="com.example.myselfbinder.action.BIND_MYSERVICE">
       </action>
   </intent-filter>
</service>

同时我们需要把服务端运行在不同的进程中: android:process=":ljx"

并且指定action

客户端

java 复制代码
binding.bt1.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View view) {
       String action = "com.example.myselfbinder.action.BIND_MYSERVICE";
       Intent intent = new Intent(action);
       intent.setPackage("com.example.myselfbinder");
       bindService(intent,serviceConnection,BIND_AUTO_CREATE);
   }
});

点击按钮,我们绑定服务,注意的是我们需要指定启动服务所在的包名,通过bindService进行绑定;

bindService:第二个参数是ServiceConnection的对象,这个方法接收绑定成功或失败的回调;

java 复制代码
ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
         binder = asInterface(iBinder);
        Toast.makeText(getBaseContext(),"连接成功了",Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {

    }
};

创建ServiceConnection的对象,在成功链接服务的回调里: binder = asInterface(iBinder):左边是我们定义方法接口的对象;右边调用了asInterface(),这里的ibinder其实是Binder驱动返回的mRemote对象;

  1. asInterface()

    因为前文提到过,不同的进程,Binder驱动返回值是不一样的,比如我们这里属于跨进程通信,返回的是Binder的代理,那如果同一个进程,返回的是Binder本体;

    那么返回的Binder不一样,其实走的就是两条路了,所以我们应该写个方法判断一下,是否是同一个进程,这个方法就是asInterface()

  2. 左边为什么是我们定义接口的对象:

    代理也需要实现 Queuemes我们统一定义的接口,这样我们不管是不是代理,都只需要调用mRemote对象中我们需要的方法,就可以实现通信,为了统一把;

java 复制代码
private static  Queuemes asInterface(android.os.IBinder os){
   if(os == null){
       return null;
   }
   else if(os instanceof Binder){
       return (Queuemes) os;
   }
   else{
       return new MesBinderProxy(os);
   }
}

实现Binder返回值的判断,我们可以看到,如果是Binder,那么强转为Queuemes,如果不是,则为Binder的代理,那我们就创建实例,接下来看看代理类怎么写;

java 复制代码
private static class MesBinderProxy implements Queuemes{
    private IBinder mRemote;

    public MesBinderProxy(IBinder mRemote) {
        this.mRemote = mRemote;
    }

    @Override
    public String getMesName(String name) {
        Parcel _data = Parcel.obtain();
        Parcel _reply = Parcel.obtain();
        String mes;
        try {
            _data.writeString(name);
            mRemote.transact(1,_data,_reply,0);
            mes = _reply.readString();
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
        finally {

            _data.recycle();
            _reply.recycle();
        }
     return mes;
    }
}

方法内很简单,构造方法中我们得到了Binder驱动返回的对象;主要方法介绍:

  1. Parcel.obtain():申请一个空的parcel对象;

    为什么不new一个?parcel不是普通的java类,它的内容储存在native层共享内存,obtain()是从对象池去一个现成的parcel,减少内存分配的开销;

  2. transact:底层会通过Binder的驱动,将_data数据发送给服务端,服务端会把最后的数据写到 _reply中;

  3. _data.recycle():回收parcel对象,释放底层空间;

大致逻辑就是,得到两个parcel对象,然后把数据写入_data,通过transact和服务端通信,将线程挂起,当服务端返回时,线程继续,我们从服务端返回的 _reply对象中读出返回的值,最后回收parcel对象;

java 复制代码
binding.bt2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        String s = binder.getMesName("徐照茹");
        Toast.makeText(getBaseContext(),s,Toast.LENGTH_SHORT).show();
    }
});

最后通过按钮的点击事件,我们得到服务端返回的数据,通过Toast提示出来;

至于为什么这么写,大家可以根据上面的Binder工作流程结合进行理解;

Binder的高效实现

为了帮助开发者实现BinderAIDL自动生成了跨进程通信的模版,避免写一大堆统一的代码,有了AIDL,我们在服务端关注方法的实现就行,客户端不需要写额外的代码,因为全部自动生成了;

接下来我们对生成的代码进行解析比对,这下理解起来就比较容易了;

操作很简单,写两个aidl文件以及一个java文件:

java 复制代码
// Book.aidl
package com.example.binder.aidl;

// Declare any non-default types here with import statements

parcelable Book;

aidl用到了实现序列化的Book类,这里声明一下;

java 复制代码
// IBookManager.aidl
package com.example.binder.aidl;

// Declare any non-default types here with import statements
import com.example.binder.aidl.Book;
interface IBookManager {

  List<Book> getBookList();
  void addBook(in Book book);

}

aidl接口文件,里面声明了我们想实现的两个方法,一个得到书的列表,一个增加书;

需要注意的是import com.example.binder.aidl.Book;一定要导入book类,即使是在同一个包下,也要导入!!!

java 复制代码
 public class Book implements Parcelable {
        public int bookId;
        public  String bookName;

        public Book(int bookId, String bookName) {
            this.bookId = bookId;
            this.bookName = bookName;
        }

        protected Book(Parcel in) {
            bookId = in.readInt();
            bookName = in.readString();
        }

        public static final Creator<Book> CREATOR = new Creator<Book>() {
            @Override
            public Book createFromParcel(Parcel in) {
                return new Book(in);
            }

            @Override
            public Book[] newArray(int size) {
                return new Book[size];
            }
        };

        @Override
        public int describeContents() {
            return 0;
        }


        @Override
        public void writeToParcel(@NonNull Parcel parcel, int i) {
            parcel.writeInt(bookId);
            parcel.writeString(bookName);
        }


    }

实现序列化的Book类;

结构大概长这样,括弧,如果你放的路径有问题,也是无法正常生成的;

那么重新build,系统就会自动在gen目录下生成一个类IBookManager.java

现在我们来简单介绍一下这个类

生成类框架理解

java 复制代码
    package com.example.binder.aidl;
    public interface IBookManager extends android.os.IInterface
    {
      public static abstract class Stub extends android.os.Binder implements com.example.binder.aidl.IBookManager
      {
        @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
        {
          
        }
        private static class Proxy implements com.example.binder.aidl.IBookManager
        {
        }
        static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
      }
      //Binder的唯一标识
      public static final java.lang.String DESCRIPTOR =
      //声明的方法
      public java.util.List<com.example.binder.aidl.Book> getBookList() throws android.os.RemoteException;
      public void addBook(com.example.binder.aidl.Book book) throws android.os.RemoteException;
      }

这是它的大致框架:

  1. IBookManager 实现了IInterface接口,同时本身也是一个接口;

    所有在Binder中传输的接口都必须实现这个接口;

  2. 接口里面,声明了我们想调用的两个方法,以及DESCRIPTOR;

    DESCRIPTOR:Binder的唯一标识,一般用Binder的类名标识;

  3. Stub:就是一个Binder类,Stub内部还有一个Proxy是客户端的代理类;

  4. Stub里面定义了两个方法的整形Id,为了标识在跨进程通信中,客户端到底调用的是哪个方法;

生成类Stub

java 复制代码
    public static abstract class Stub extends android.os.Binder implements com.example.binder.aidl.IBookManager
    {
      public Stub()
      {
        this.attachInterface(this, DESCRIPTOR);
      }
      public static com.example.binder.aidl.IBookManager asInterface(android.os.IBinder obj)
      {
       
      }
      @Override public android.os.IBinder asBinder()
      {
        return this;
      }
      @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
      {
       
      }
      private static class Proxy implements com.example.binder.aidl.IBookManager
      {
      }
    }

我们可以看到BinderStub中,除了构造方法以外,还有asInterface onTransactasBinder()三个方法;

其实前两个方法在我们刚刚完成了实战中已经讲过了,,,这里先说下第三个方法,也很好理解;

asBinder():相当于一个get方法,用于返回当前的Binder对象;

onTransact:根据code来分发具体要执行的方法;

asInterface:没啥好说的,如果是不同进程,就会返回包装好的客户端的代理类对象Proxy;这个方法在客户端调用,用来获取一个IBookManager的对象,就是我们自己定义的接口对象;

生成类Proxy

java 复制代码
    private static class Proxy implements com.example.binder.aidl.IBookManager
      {
        private android.os.IBinder mRemote;
        Proxy(android.os.IBinder remote)
        {
          mRemote = remote;
        }
        @Override public android.os.IBinder asBinder()
        {
          return mRemote;
        }
        public java.lang.String getInterfaceDescriptor()
        {
          return DESCRIPTOR;
        }

        @Override public java.util.List<com.example.binder.aidl.Book> getBookList() throws android.os.RemoteException
        {
          //得到Parcel对象
          android.os.Parcel _data = android.os.Parcel.obtain();
          android.os.Parcel _reply = android.os.Parcel.obtain();
          //返回值的对象
          java.util.List<com.example.binder.aidl.Book> _result;
          try {
          //把参数写入
            _data.writeInterfaceToken(DESCRIPTOR);
            //调用Binder或Binder代理的方法,请求服务端
            boolean _status = mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
            //获取服务端信息
            _reply.readException();
            //获取信息,构造返回的列表;
            _result = _reply.createTypedArrayList(com.example.binder.aidl.Book.CREATOR);
          }
          finally {
          //避免内存泄漏
            _reply.recycle();
            _data.recycle();
          }
          return _result;
        }
      }
    }

结构和我们自己写的很相似,这里不做过多介绍,也都写注释;

Binder的总结

Binder的线程管理

  1. 每个Binder的服务端会创建很多Binder的线程来处理Binder的请求,可以简单理解为Binder的线程池,但是它不是由Service端管理的,是由Binder的驱动来管理的;
  2. 一个进程中最大Binder线程数量为16,超出数量的请求会阻塞等待空闲的Binder线程;

Binder讲的到底是什么?

  1. 通常意义上来说,Binder指的是Android的IPC机制;
  2. 从服务端的进程来说,Binder指的是Binder的本地对象;从客户端来说,Binder指的是Binder的代理对象;
  3. 从传输过程中来说,Binder是可夸进程传输的对象;

Binder的通信原理

  1. Binder是基于内存映射mmap来实现的,直接操作映射的这一部分内存的数据,后面通过映射来得到数据,所以它只用复制一次,所以它的性能很好;

  2. 大致的流程:

    1. Binder驱动会在内核空间开辟出一个接收数据的缓冲区;
    2. 接着又会在内核空间开辟出一个内核缓冲区;
    3. 内核缓冲区会和接收数据缓冲区建立映射关系;
    4. 接收数据缓冲区又会和接收进程的空间地址建立映射关系;
    5. 发送数据的进程会将数据复制给内核缓冲区;
    6. 由于内核缓冲区和接收数据缓冲区存在映射关系,接收数据缓冲区又和接收进程的空间地址有映射关系,所以在接受进程中会直接获得这段数据;

这样便完成了一次Binder通信!

好啦,本次分享的内容到此结束,期待下次具体的IPC方式的文章和大家见面~

相关推荐
申阳6 小时前
Day 4:02. 基于Nuxt开发博客项目-整合 Inspira UI
前端·后端·程序员
SimonKing10 小时前
你的项目还在用MyBatis吗?或许这个框架更适合你:Easy-Query
java·后端·程序员
Qinana10 小时前
🚙微信小程序实战解析:打造高质感汽车展示页
前端·css·程序员
资源分享交流21 小时前
智能课堂课程系统源码 – 多端自适应_支持讲师课程
源码
京东云开发者1 天前
告别 “盲买”!京东 AI 试穿 Oxygen Tryon:让服饰购物从“想象”到“所见即所得”
程序员
前端_逍遥生1 天前
如何快速让自己放松下来,不再紧绷
程序员
申阳1 天前
Day 3:01. 基于Nuxt开发个人呢博客项目-初始化项目
前端·后端·程序员
不想说话的麋鹿1 天前
「项目前言」从配置程序员到动手造轮子:我用Vue3+NestJS复刻低代码平台的初衷
前端·程序员·全栈