Binder + AMS + AIDL大杂烩

参考文章:

Binder

强推博客:

按照参考文章里说的,我们最好先学习以下AIDL。

AIDL

参考文章:

文章思路:

  • 为什么要设计这门语言?
  • 它有什么语法?
  • 如何使用AIDL文件完成跨进程通信?

AIDL是什么?

AIDL 是 Android 接口定义语言。

AIDL 设计的初衷?

用来解决跨进程通信,其实跨进程通信还可以使用BroadcastReceiverMessenger,但是BroadcastReceiver占用的系统资源比较多,如果是频繁的跨进程通信是不可取的。Messenger进行跨进程通信时只能同步,不能异步。

AIDL 语法

  • 文件类型:xx.aidl
  • 支持类型:
    • Java中的八种基本数据类型,包括 byte,short,int,long,float,double,boolean,char。
    • String 类型。 CharSequence类型。
    • List类型:List中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是定义的parcelable(下文关于这个会有详解)。List可以使用泛型。
    • Map类型:Map中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是定义的parcelable。Map是不支持泛型的
  • 定向tag: 表示跨进程通信中数据的流向。
    • in: 数据只能从客户端流向服务端
    • out:数据只能从服务端流向客户端
    • inout: 表示可以在服务端和客户端之间双向流通。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。
  • 两种AIDL文件:
    • 用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类
    • 定义接口方法

下面是例子。

  1. 看一下总体的文件布局:
    aidl.png

上面的5个文件就是我们编写的文件了。 2. 先编写AIDL文件夹里的文件 直接右键,new 一个 AIDL文件即可。

AIDL2.png

Book.aidl

aidl3.png

BookManager.aidl

aidl4.png

这里要确保Book.java文件和Book.aidl文件在同一文件夹下,同时又能被编译器找到,所以我们要在build.gradle中加入:

sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}
  1. 编写服务端代码
public class AIDLService extends Service {

    public final String TAG = this.getClass().getSimpleName();

    //包含Book对象的list
    private List<Book> mBooks = new ArrayList<>();

    //由AIDL文件生成的BookManager
    private final BookManager.Stub mBookManager = new BookManager.Stub() {
        @Override
        public List<Book> getBooks() throws RemoteException {
            synchronized (this) {
                Log.e(TAG, "invoking getBooks() method , now the list is : " + mBooks.toString());
                if (mBooks != null) {
                    return mBooks;
                }
                return new ArrayList<>();
            }
        }


        @Override
        public void addBook(Book book) throws RemoteException {
            synchronized (this) {
                if (mBooks == null) {
                    mBooks = new ArrayList<>();
                }
                if (book == null) {
                    Log.e(TAG, "Book is null in In");
                    book = new Book();
                }
                //尝试修改book的参数,主要是为了观察其到客户端的反馈
                book.setPrice(2333);
                if (!mBooks.contains(book)) {
                    mBooks.add(book);
                }
                //打印mBooks列表,观察客户端传过来的值
                Log.e(TAG, "invoking addBooks() method , now the list is : " + mBooks.toString());
            }
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        Book book = new Book();
        book.setName("Android开发艺术探索");
        book.setPrice(28);
        mBooks.add(book);   
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString()));
        return mBookManager;
    }
}

在清单文件中进行配置:

<service
    android:name=".service.AIDLService"
    android:exported="true">
        <intent-filter>
            <action android:name="com.lypeer.aidl"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
</service>
  1. 回到java文件夹下,编写我们的客户端代码
public class AIDLActivity extends AppCompatActivity {

    //由AIDL文件生成的Java类
    private BookManager mBookManager = null;

    //标志当前与服务端连接状况的布尔值,false为未连接,true为连接中
    private boolean mBound = false;

    //包含Book对象的list
    private List<Book> mBooks;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_aidl);
    }

    /**
     * 按钮的点击事件,点击之后调用服务端的addBookIn方法
     *
     * @param view
     */
    public void addBook(View view) {
        //如果与服务端的连接处于未连接状态,则尝试连接
        if (!mBound) {
            attemptToBindService();
            Toast.makeText(this, "当前与服务端处于未连接状态,正在尝试重连,请稍后再试", Toast.LENGTH_SHORT).show();
            return;
        }
        if (mBookManager == null) return;

        Book book = new Book();
        book.setName("APP研发录In");
        book.setPrice(30);
        try {
            mBookManager.addBook(book);
            Log.e(getLocalClassName(), book.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * 尝试与服务端建立连接
     */
    private void attemptToBindService() {
        Intent intent = new Intent();
        intent.setAction("com.lypeer.aidl");
        intent.setPackage("com.lypeer.ipcserver");
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (!mBound) {
            attemptToBindService();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mBound) {
            unbindService(mServiceConnection);
            mBound = false;
        }
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e(getLocalClassName(), "service connected");
            mBookManager = BookManager.Stub.asInterface(service);
            mBound = true;

            if (mBookManager != null) {
                try {
                    mBooks = mBookManager.getBooks();
                    Log.e(getLocalClassName(), mBooks.toString());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(getLocalClassName(), "service disconnected");
            mBound = false;
        }
    };
}

之后就可以开始通信啦。 如果发现有问题,检查下面配置:

aidl6.png

AIDL 原理

其实在写完AIDL文件后,编译器会帮我们自动生成一个同名的 .java 文件。(图是截图参考文章的,写的时候忘记截了~~~)

aidl5.png

  1. 从客户端开始
public void onServiceConnected(ComponentName name, IBinder service) 
    mBookManager = BookManager.Stub.asInterface(service);
}

这里的传递给了我们一个service对象,经过Debug,发现它是AIDLService

aidl7.png

不深究它是怎么来的。我们接着看。 我们看到这里调用了asInterface方法,我们点击查看一下源码:

public static com.lypeer.ipcclient.BookManager asInterface(android.os.IBinder obj) {
    //验空
    if ((obj == null)) {
        return null;
    }
    //DESCRIPTOR = "com.lypeer.ipcclient.BookManager",搜索本地是否已經
    //有可用的对象了,如果有就将其返回
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.lypeer.ipcclient.BookManager))) {
        return ((com.lypeer.ipcclient.BookManager) iin);
    }
    //如果本地没有的话就新建一个返回
    return new com.lypeer.ipcclient.BookManager.Stub.Proxy(obj);
}

返回一个proxy对象。这里proxy对象就是客户端跟服务端进行沟通的桥梁了。 2. proxy中方法做了什么 我们接着看proxy方法中做了什么:

@Override
public java.util.List<com.lypeer.ipcclient.Book> getBooks() throws android.os.RemoteException {
    //很容易可以分析出来,_data用来存储流向服务端的数据流,
    //_reply用来存储服务端流回客户端的数据流
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    java.util.List<com.lypeer.ipcclient.Book> _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        //调用 transact() 方法将方法id和两个 Parcel 容器传过去
        mRemote.transact(Stub.TRANSACTION_getBooks, _data, _reply, 0);
        _reply.readException();
        //从_reply中取出服务端执行方法的结果
        _result = _reply.createTypedArrayList(com.lypeer.ipcclient.Book.CREATOR);
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    //将结果返回
    return _result;
}
  • transact方法:这是客户端和服务端通信的核心方法。调用这个方法之后,客户端将会挂起当前线程,等候服务端执行完相关任务后通知并接收返回的 _reply 数据流。关于这个方法的传参,这里有两点需要说明的地方:
    • 方法 ID :transact() 方法的第一个参数是一个方法 ID ,这个是客户端与服务端约定好的给方法的编码,彼此一一对应。在AIDL文件转化为 .java 文件的时候,系统将会自动给AIDL文件里面的每一个方法自动分配一个方法 ID。
    • 第四个参数:transact() 方法的第四个参数是一个 int 值,它的作用是设置进行 IPC 的模式,为 0 表示数据可以双向流通,即 _reply 流可以正常的携带数据回来,如果为 1 的话那么数据将只能单向流通,从服务端回来的 _reply 流将不携带任何数据。 注:AIDL生成的 .java 文件的这个参数均为 0。

通过一个方法可以总结一下proxy方法的工作流程:

  1. 生成_data和_reply数据流,将它传给服务端

  2. 通过transact方法传递给服务端,并请求服务端调用指定方法

  3. 接收_reply数据流,并从中取出服务端返回的数据(服务端是如何返回_reply数据流回来的,这点是底层封装好的,我们不必要知道)

  4. 我们把数据分成Pacel传给服务端了,服务端干了些啥? 前面说了客户端通过调用 transact() 方法将数据和请求发送过去,那么理所当然的,服务端应当有一个方法来接收这些传过来的东西:在 BookManager.java 里面我们可以很轻易的找到一个叫做 onTransact() 的方法。我们看看这个方法是怎么写的:

@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
    switch (code) {
        case INTERFACE_TRANSACTION: {
            reply.writeString(DESCRIPTOR);
            return true;
        }
        case TRANSACTION_getBooks: {
            //省略
            return true;
        }
        case TRANSACTION_addBook: {
            //省略
            return true;
        }
    }
    return super.onTransact(code, data, reply, flags);

可以看到,它在接收了客户端的 transact() 方法传过来的参数后,什么废话都没说就直接进入了一个 switch 选择:根据传进来的方法 ID 不同执行不同的操作。接下来看一下每个方法里面它具体做了些什么,以 getBooks() 方法为例:

case TRANSACTION_getBooks: {
    data.enforceInterface(DESCRIPTOR);
    //调用 this.getBooks() 方法,在这里开始执行具体的事务逻辑
    //result 列表为调用 getBooks() 方法的返回值
    java.util.List<com.lypeer.ipcclient.Book> _result = this.getBooks();
    reply.writeNoException();
    //将方法执行的结果写入 reply ,
    reply.writeTypedList(_result);
    return true;
}

很简单,调用服务端的具体实现,然后获取返回值写入到reply流。这里的this.getBooks()方法就是由子类去实现的,也就是我们的AIDLService中的bookManager

aidl8.png

好了,整个流程结束了,现在我们来总结一下:

  • 获取客户端传递过来的数据,根据方法id执行相应的操作
  • 将传递过来的数据取出来,如果需要返回数据给服务端,那就封装成_data传回。否则,执行相应操作。
  • 服务端接收到数据后,将需要回传的数据写入_reply流,传回客户端。

整个流程的序列图是:

aidl9.png

这张图,跟我们之前ActivityThread那张图很像,因为本质上两者都是采用了Binder进行通信。

总结:

如果要使用Binder进行通信,那么你首先需要定义一个协议BookManager,然后你需要有两个代理,一个是服务端代理,也是最终真正做事的类--AIDLService,还需要一个本地代理类--BookMangerProxy,它只负责封装数据传递给服务端,然后服务端在onTransact中拿到数据进行处理即可。

Binder学习

1 用户空间和内核空间

Android系统基于Linux内核,即Linux Kernel。Linux Kernel是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了给Kernel提供一定的保护机制,于是就把Kernel和上层的应用程序抽象的隔离开,分别称之为内核空间(Kernel Space)和用户空间(User Space)

2 Binder驱动定义

Android使用的Linux内核拥有着非常多的跨进程通信机制,比如管道,System V,Socket等,但是出于安全和性能的考虑,采用了一种全新的通信方式——Binder。Binder本身并不是Linux内核的一部分,但是通过Linux的动态可加载内核模块机制(LKM),Binder模块在运行时被链接到内核作为内核的一部分在内核空间运行。

模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行。

在Android系统中,这个运行在内核空间的,负责各个用户进程通过Binder通信的内核模块叫做Binder驱动;

总结:

  • Binder是运行在内核态的,单不属于内核态。
  • 用户进程之间进行通信只能通过内核空间。

3 为什么使用Binder?

  • 安全

传统的进程通信方式对于通信双方的身份并没有做出严格的验证,只有在上层协议上进行架设;比如Socket通信ip地址是客户端手动填入的,都可以进行伪造;而Binder机制从协议本身就支持对通信双方做身份校检,因而大大提升了安全性。这个也是Android权限模型的基础。

  • 性能

在移动设备上,广泛地使用跨进程通信肯定对通信机制本身提出了严格的要求;Binder相对出传统的Socket方式,更加高效

4 Binder通信模型

比喻:打电话

SM : 通信录,记录着你要找的人的电话号码 Binder:基站

整个通信流程如图:

binder.png

5 Binder机制跨进程原理

上文Binder通信图给出了四个角色:Client,Server,SM,driver。但是我们仍然不清楚他们是怎么干活的? 看下图:

binder2.png

画画太烂,看一下原来的图吧。

binder3.png

整个流程简述为:

  1. Server向SM注册,告诉自己是谁,有什么能力(IInterface接口),对应到场景中也就是告诉SM,自己叫"zhangsan",有一个对象object,可以add
  2. Client向SM查询zhangsanobject
  3. 驱动在数据流过时,返回一个一模一样的代理类
  4. Client调用代理类的add方法
  5. 代理类封装方法参数,发送给Binder驱动
  6. Binder驱动拿到这个参数,通知Server进程调用object对象的add方法,并返回结果
  7. Server返回结果,Binder返回结果给Client

一句话总结就是:

Client进程只不过是持有了Server端的代理;代理对象协助驱动完成了跨进程通信。

总结四个角色的作用:

  • Client进程(简称Client):通信的发起进程。
  • Server进程(简称Server):通信的响应进程。
  • ServiceManager进程(简称SM): 所有的Server将自己的信息注册到SM,并为Client提供查询的功能。
  • Binder驱动:将SM为Client查询到的Server的目标数据转换为proxy,再传递给Client

深入了解Java的Binder

IBinder/IInterface/Binder/BinderProxy/Stub是什么?

  • IBinder:是一个接口,它代表了一种跨进程传输的能力
  • IInterface:代表的就是远程server对象具有什么能力。具体来说,就是aidl里面的接口。
  • Java层的Binder类:代表的其实就是Binder本地对象。BinderProxy类是Binder类的一个内部类,它代表远程进程的Binder对象的本地代理;这两个类都继承自IBinder, 因而都具有跨进程传输的能力;实际上,在跨越进程的时候,Binder驱动会自动完成这两个对象的转换。
  • Stub:继承自Binder,说明他是Binder的本地对象;实现IInterface接口,说明具有远程Server承诺给Client的能力;Stub是抽象类,说明具体的实现需要我们自己完成,这里使用了策略模式。

知道了这一点后,我们可以分析AIDL中各个类的作用了。

先从AIDL文件夹,也就是服务端中文件看起。

  1. ICompute.aidl
package com.example.test.app;
interface ICompute {
     int add(int a, int b);
}

然后用编译工具编译之后,可以得到对应的ICompute.java类:

public interface ICompute extends android.os.IInterface 

继承自IInterface,说明ICompute是用来定义了Server端具有的能力。

  1. 接下来看的内部类Stub
 /**
         * Cast an IBinder object into an com.example.test.app.ICompute interface,
         * generating a proxy if needed.
         */
        public static com.example.test.app.ICompute asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.test.app.ICompute))) {
                return ((com.example.test.app.ICompute) iin);
            }
            return new com.example.test.app.ICompute.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

Stub类继承自Binder,意味着这个Stub其实自己是一个Binder本地对象,然后实现了ICompute接口,ICompute本身是一个IInterface,因此他携带某种客户端需要的能力(这里是方法add)。

  1. 然后看看asInterface方法
/**
 * Cast an IBinder object into an com.example.test.app.ICompute interface,
 * generating a proxy if needed.
 */
public static com.example.test.app.ICompute asInterface(android.os.IBinder obj) {
    if ((obj == null)) {
        return null;
    }
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.example.test.app.ICompute))) {
        return ((com.example.test.app.ICompute) iin);
    }
    return new com.example.test.app.ICompute.Stub.Proxy(obj);
}

我们在bind一个Service之后,在onServiceConnecttion的回调里面,就是通过这个方法拿到一个远程的service的,这个方法做了什么呢?

首先看函数的参数IBinder类型的obj,这个对象是驱动给我们的,如果是Binder本地对象,那么它就是Binder类型,如果是Binder代理对象,那就是BinderProxy类型;然后,正如上面自动生成的文档所说,它会试着查找Binder本地对象,如果找到,说明Client和Server都在同一个进程,这个参数直接就是本地对象,直接强制类型转换然后返回,如果找不到,说明是远程对象(处于另外一个进程)那么就需要创建一个Binde代理对象,让这个Binder代理实现对于远程对象的访问。一般来说,如果是与一个远程Service对象进行通信,那么这里返回的一定是一个Binder代理对象,这个IBinder参数的实际上是BinderProxy;

我们知道,对于远程方法的调用,是通过Binder代理完成的,在这个例子里面就是Proxy类;Proxy对于add方法的实现如下:

Override
public int add(int a, int b) throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    int _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeInt(a);
        _data.writeInt(b);
        mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
        _reply.readException();
        _result = _reply.readInt();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    return _result;
}

它首先用Parcel把数据序列化了,然后调用了transact方法.

transact方法的作用:将Client线程挂起等待返回;驱动完成操作后调用Serer的onTransact方法,这个方法被结果返回给驱动,驱动之后唤醒挂起的Client并将结果返回。

整个过程结束。

6 Binder本地对象和Binder代理对象区别

  • Binder本地对象(Stub):实现IInterface,继承Binder(is a Binder)
  • Binder代理对象(Proxy):实现IInterace,并持有IBinder引用(has a Binder)

比喻来说就是一个是真正的皇帝,一个是挟天子以令诸侯。

7.结合Activity的启动流程看Binder

Activity的启动流程中涉及以下类:

  • Launcher
  • Activity
  • Instrumentation
  • ActivityManagerNative
  • ActivityManagerProxy
  • ActivityManagerService
  • ActivityStack
  • ApplicationThreadProxy
  • ApplicationThread
  • ActivityThread

再进行讲解Activity的启动过程之前,我们首先要知道各个类之间的关系,以及各个类的作用,另外还需要知道这些类是属于Server端还是Client端的。我们知道,Android IPC 通信采用的Binder,用到Binder就需要区分Server和Client.

类关系图

Activity1.png

getService()方法举例,说明调用顺序,Activity采用的是远程代理ActivityManagerProxyAMS在Client端的代理,同时,它也是个Binder对象,传递时将它传递给AMS,以后,AMS想对Activity做啥,就可以通过它。

代理模式2.png

总的来说,就是:

  1. ActivityManager首先通过远端代理ActivityManagerNative得到本地代理ActivityManagerProxy
  2. ActivityManager要调用什么方法,就通过本地代理ActivityManagerProxy的成员变量ActivityManagerNative去调用,其实就是将Binder对象ActivityManagerNative传递过去。

如果没了解过Activity的启动流程,请参考文章Activity启动流程

分析Activity启动流程中各个类对应的角色

  • IAcivityManger:是一个IInterface,代表远程Service有什么能力;
  • ActivityManagerNative:Binder本地对象(类似Stub
  • AtivityMangerService:真正的服务端, ActivityManagerNative的实现类
  • ActivityManagerProxy:Bidner代理对象
  • ActivityManager:不过是一个管理类而已,可以看到真正的操作都是转发给ActivityManagerNative进而交给他的实现ActivityManagerService 完成的。

总结:

  • ----------------------------- 进程间通信步骤 -----------------------------

  • 1.Client 发起远程调用请求 也就是RPC 到Binder。同时将自己挂起,挂起的原因是要等待RPC调用结束以后返回的结果

  • 2.Binder 收到RPC请求以后 把参数收集一下,调用transact方法,把RPC请求转发给service端。

  • 3.service端 收到rpc请求以后 就去线程池里 找一个空闲的线程去走service端的 onTransact方法 ,实际上也就是真正在运行service端的 方法了,等方法运行结束 就把结果 写回到binder中。

  • 4.Binder 收到返回数据以后 就唤醒原来的Client 线程,返回结果。至此,一次进程间通信 的过程就结束了

  • ----------------------------- 进程间通信步骤 -----------------------------

猜你喜欢

转载自juejin.im/post/5af534f46fb9a07ac0224c07