笔记 - 安卓中的Binder
引言
学习记录,其实是反复看了好几次Binder了,做个笔记记录一下,学习的内容是在《Android 开发艺术探索》中的
梗概
Binder是安卓的一种跨进程通信(IPC)的方式。不管是在安卓系统中,还是在日常开发中都可以存在着Binder。试想一下,两个人在不同的地方要进行通信,那么他们就可以用打电话的方式。那两个进程需要通信,那么它们就可以使用Binder来进行。
-
系统中的Binder
ServiceManager与ActivityManager、ServiceManager与WindowManager等等 -
应用中的Binder
客户端和服务端进行通信,这里的客户端与服务端是相对的,谁都可以是客户端,谁都可以是服务端。一端既可以是客户端也可以是另一个客户端的服务端。客户端目的是获取服务端的数据或者其提供的服务。
AIDL
aidl是什么?aidl与binder又有什么关系。aidl是一种文件,通过编写aidl文件,IDE会自动为我们生成Binder的代码。这就类似于我们创建menu,我们只是在menu文件夹下敲一敲xml格式的文件,然后IDE就会给我们生成响应的Java代码。
通过aidl来创建binder
-
第一步–创建Book.java
这是一个图书信息的类,并且实现Parcelable接口来进行序列化。
package com.example.aidltest;
import android.os.Parcel;
import android.os.Parcelable;
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(bookId);
out.writeString(bookName);
}
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>(){
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
private Book(Parcel in){
bookId = in.readInt();
bookName = in.readString();
}
}
- 创建aidl文件
-
Book.aidl
这里声明来Book.java这个类
package com.example.aidltest;
parcelable Book;
-
IBookManager.aidl
这里有两个方法,一个是getBookList,用于远程从服务端获取图书列表;另一个是addBook,用于远程向列表中添加书本
package com.example.aidltest;
import com.example.aidltest.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
整个项目结构
我使用的是Android Studio的开发环境
自动生成的Binder代码
生成的Binder类位置在根目录下,位置如下所示
分析IBookManager类的各部分实现
直接从最外层来看有3个部分:Default类、Stub类、方法声明。关键代码其实都在Stub类中
方法声明
整个IBookManager是一个接口,同时也继承了IInterface这个接口,这里声明了两个我们在aidl文件中定义的方法getBookList和addBook,如下所示
Default类
自动生成的注释也有说明,这个类是接口IBookManger的默认实现,里面默认实现了我们在aidl中声明的两个方法getBookList和addBook,这里默认实现基本是空的,如下。我们就跳过
Stub类
最重要的类,这里分为两块,一块是Stub自己的逻辑,一块是Stub类中的内部类Proxy类。
Stub类本身
- DESCRIPTOR标识
它是用于唯一标识本IBookManager类的,相当于身份证,一般来说会默认使用当前这个Binder的类名,如下所示
- 整形ID标识
这两个整形是用来唯一标识我们接口中的方法的,通信时可以知道客户端请求的是哪一个方法,如下所示
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
- asInterface()方法
这个方法的注释是将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象。如果客户端与服务端在同一个进程,则获得的是服务端对象的Stub本身,若不在同一个进程,则会获得一个代理的Stub对象。
public static com.example.aidltest.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.example.aidltest.IBookManager))) {
return ((com.example.aidltest.IBookManager)iin);
}
return new com.example.aidltest.IBookManager.Stub.Proxy(obj);
}
从代码逻辑来看,伪代码如下
- 判空
- 尝试获取Binder对象在本地进程的实现
- 如果获取到的非空且为IBookManager的实现类,则将获取到的对象转化为IBookManager并返回
- 若本地没有实现,则返回其代理对象Proxy,就是下面的内部代理类Proxy
- asBinder()方法
用于返回当前的Binder对象,实现如下
@Override public android.os.IBinder asBinder()
{
return this;
}
- onTransact()方法
这个方法会运行在服务端的Binder线程池中,客户端发起请求时,服务端可以通过code来辨别服务端想要请求什么服务,然后再执行不同的逻辑。最后返回true代表客户端请求成功了,false代表客户端请求失败。如下
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
java.lang.String descriptor = DESCRIPTOR;
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(descriptor);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(descriptor);
java.util.List<com.example.aidltest.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(descriptor);
com.example.aidltest.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.example.aidltest.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
}
根据code执行不同的逻辑,这里的第2、3个代码框的code就是之前定义的整形ID标识
getBookList时会获取到图书列表_result然后写入reply中,再返回true代表获取了请求。
addBook时会创建一个Book实例,将参数的data转换后再调用addBook()方法添加图书,最后返回true代表获取了请求
内部代理类Proxy
上面我们看到,如果客户端与服务端不在一个进程,那么就会调用到Proxy,这里主要看两个方法一个是getBookList(),另一个是addBook(),就是我们声明的并且一直提到那两个方法。
- getBookList()方法
@Override public java.util.List<com.example.aidltest.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<com.example.aidltest.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
boolean _status = mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
if (!_status && getDefaultImpl() != null) {
return getDefaultImpl().getBookList();
}
_reply.readException();
_result = _reply.createTypedArrayList(com.example.aidltest.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
先创建Parcel类型的_data和_reply,再List类型的结果_result,如果有参数,则将参数写入_data,再调用mRemote的transact方法,传入请求code,_data信息,_reply。这样就发起了RPC(远程过程调用),然后当前线程挂起,服务端的onTransact就会被调用,直到放回了结果,这样当前线程就会继续执行,如果返回值为true,表示服务端接收了我们的请求,就从_reply中取出结果并构造到_result中,最后对Parcel对象进行回收并返回_result。
- addBook()方法
@Override public void addBook(com.example.aidltest.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);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
boolean _status = mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
if (!_status && getDefaultImpl() != null) {
getDefaultImpl().addBook(book);
return;
}
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
这个方法过程与getBookList()方法的过程类似,这里就不再展开
这里我打一个可能不太恰当的比喻
小时候你妈妈要炒菜,发现家里没有菜了,拿了个菜篮让你去隔壁邻居家借点菜回来炒。自己的家跟邻居的家就是两个进程,妈妈要炒菜,先在家里自己找,如果有,那就直接用,如果没有就有你这个小朋友做代理去邻居家要,并且拿个菜篮子让你把菜放里面包装好带回家里来。在作为小代理的你还没回来时,妈妈没有菜炒,只能一直在家等你回来,直到你回来了,妈妈才继续干接下来的事。
注意事项
- 因为在客户端发起请求后,会一直等到服务端返回消息,所以如果请求的远程方法很耗时,那么就不可以在UI线程中发起这种远程请求,因为会这样会造成ANR。
- 服务端的Binder方法已经是在Binder线程池中运行的,所以Binder方法应该使用同步的方式去实现(因为本来就在线程中运行)
我把相关的代码放在github上了,如果有需要的话,大家自取
下一篇是Binder在两个进程的使用
本文地址:https://blog.csdn.net/weixin_42530254/article/details/107409520