Android(安卓)Binder机制浅析及AIDL的使用
参考
轻松理解 Android Binder,只需要读这一篇
图文详解 Android Binder跨进程通信的原理
Android中的Parcel是什么
Android Binder IPC通信机制对比
前言
Android中有很多种IPC通信机制,如共享内存、管道、socket等。
Linux中的内存管理中存在虚拟内存和物理内存两种概念;每个进程都拥有自己独立的虚拟内存空间,这样用户进程可以认为自己所有用的是一段独立、连续的内存空间,而不必管这些数据具体存在于物理内存的哪个段、哪个页上,虚拟内存的映射统一交给kernel去管理。
Android中每个进程可以访问到的虚拟内存空间分为用户空间和内核空间。其中用户空间为每个进程独占,一个进程无法访问到其他进程的用户空间;而内核空间是公用的。所以大多数传统IPC采用的都是以公用的内核空间为数据传递的中间站,通过
copy_from_user():将用户空间的数据拷贝到内核空间;
copy_to_user():将内核空间的数据拷贝到用户空间;
完成数据由进程A用户空间到进程B用户空间的传递。
也就是说,在传统的IPC中,数据需要两次的拷贝过程才可以完成进程间通信。
再看下共享内存,简单来说,共享内存是依赖与内存映射来完成的。A和B两个进程使用共享内存进行通信,A和B将自己虚拟空间与共享对象进行内存映射。共享内存无需数据拷贝就可以完成数据通信,但同时,由于共享内存方法在使用上比较繁琐,再加上在一些使用场景不是很适用(比如A进程想使用B进程提供的某个服务,并获得这个服务的执行结果),所以此时就需要引入Android原生的IPC通信方式Binder,但共享内存在某些使用场景如大量数据的传输中,契合度还是很高的。
先看下Android几种IPC方式,数据拷贝次数的对比:
IPC | 数据拷贝次数 |
共享内存 | 0 |
Binder | 1 |
Socket/管道/消息队列 | 2 |
白话浅析Binder
Binder由Client进程、Server进程、Binder驱动、ServerManager组成。
举个例子,甲和乙两个人使用QQ聊天,甲问乙一句明天天气怎么样,乙回了一句明天下午,甲接受到了这条回复。甲和乙就相当于两个进程,他们因为不在一个地方所以无法直接进行通信,所以要借助IM工具、硬件、网络来完成这一系列交互。首先甲和乙要通过QQ聊天,那么乙一定是要注册QQ并在线的,那么此时甲相当于client进程,乙相当于server进程,乙注册QQ上线后,相当于告诉了QQ服务端自己的用户名、用户id、ip和端口号,并建立连接。那么这时候甲将信息发到QQ服务端后,QQ后端服务会根据数据库中存放的表,通过甲发送过来的乙的用户id来找到和乙建立的链接,然后将消息发送给乙。在这个过程中,QQ后端服务分管注册、用户查询、路由的这部分功能就相当于ServerManager,而双方的PC、QQ软件、网路、QQ后端服务的其他功能就相当于Binder Driver。
当然,拿QQ聊天举例子只是为了对Binder到底是个什么东西有个初步的认识,其中的实现细节会有很大的不同。
结合之前所说的,传统IPC通信是通过数据由用户空间拷贝到内核空间,再拷贝到用户空间完成的。共享内存是通过用户空间对共享对象的映射完成的。那么Binder呢?
下面盗图一张,图片出处 图文详解 Android Binder跨进程通信的原理
可以看到,client进程首先将要发送的数据通过Binder驱动,拷贝到内核空间,然后这个内核空间其实是与Binder创建的接收缓存区以及Server进程的用户空间是映射关系的,这样Client向内核空间copy数据后,相当于直接写到了server进程的用户空间,而server进程在接受到数据后并进行一些列的逻辑处理后,将结果写入到自己的用户空间,由于内存映射,也同时相当于写入到了Client进程的内核空间,然后再将返回的结果从内核空间拷贝到用户空间就可以了。也就是说单向的数据传递,在建立好了内存映射后,只需要发生一次数据拷贝即可。
从这可以看出,Binder对比与传统的IPC方式,数据传输效率是有提升的,而由于管道和socket等方式需要将自己的管道id和端口号等信息暴漏出来,而Binder则将所有注册和路由的方式交给ServerManager和Binder驱动来做,所以在安全性上,binder对比传统IPC方式也有很大提升。
从代码角度来看如何使用Binder
Binder Driver和ServerManager是由Android 的FW和HAL来实现的,那么做为应用开发人员使用Binder的重点就是就是实现client和server进程。
还是以上面甲询问乙天气怎么样为例,首先乙作为server进程,要定义好自己可以提供的服务,也就是说现在乙现在只能回答甲天气情况,而没回答吃饭了没~然后要将自己注册到ServerManager上。
那Server进程如何将自己注册到ServerManager上,并实现接受和发送的能力呢?这些都已经被封装好了,Server进程只需要继承Binder这个类,并且重写其中的
onTransact(int code, Parcel data, Parcel reply, int flags)
这个方法,并在实现onBind这个抽象方法,并将刚才继承Binder的那个类的对象通过onBind方法返回就可以了。
上代码
package com.qyy.remotemonitor.ui.service;import android.app.Service;import android.content.Intent;import android.os.Binder;import android.os.IBinder;import android.os.Parcel;import android.os.RemoteException;import android.support.annotation.Nullable;import java.util.Date;/** * Created by qinyy on 1/23/2019. */public class MyServerService extends Service{ public static final int WEATHER_INFO = 999; private MyServer mMyServer = new MyServer(); private class MyServer extends Binder implements IMyServer { @Override protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { switch (code) { case WEATHER_INFO: data.enforceInterface("MyServer"); long timeStamp = data.readLong(); String result = getWeatherInfo(com.blankj.utilcode.util.TimeUtils.millis2Date(timeStamp)); reply.writeInterfaceToken("MyClient"); reply.writeString(result); return true; } return super.onTransact(code, data, reply, flags); } @Override public String getWeatherInfo(Date date) { String weather = ""; //根据date查询天气情况 // mPresenter.getWeather(date); return weather; } } @Nullable @Override public IBinder onBind(Intent intent) { return mMyServer; } public interface IMyServer { String getWeatherInfo(Date date); }}
总结一下编写Server进程的要点:
1. 编写一个继承Binder类的类。
2. 重写Binder类中的onTransact方法,这个方法是为了接受Client传来的参数并在这里实现业务处理,并将结果返回给Client。这个方法是运行在Server进程利用进程池创建的线程中的,这个线程池的最大容量是16.
3. 通信使用了Parcel进行数据的封装与序列化,如果只传递简单的参数直接read就可以,如果需要传递复杂的参数,如何封装Parcel和读取,请参考Android中的Parcel是什么。
4. enforceInterface和writeInterfaceToken是配套出现的,用来在通信双端进行一个校验,writeInterfaceToken()方法标注远程服务名称,理论上讲,这个名称不是必要的,因为客户端既然已将获取指定远程服务的Binder引用,那么就不会调用其他远程服务,该名称作为Binder驱动确保客户端的确像调用指定的服务端,所以要在服务端在接受的时候调用enforeInterface来校验,至于校验的值,只要通信双方约定好即可。
5. 在Server进程的服务中,实现Service的onBind抽象方法,并将上面集成Binder的类实例化的对象返回。那这个Binder对象就相当于注册在Binder驱动中的一个remote代理,可以通过这个对象实现对Server进程的通信。
再看一下client进程如何通过Binder调用Server进程提供的服务,先看代码:
public class MyClientActivity extends Activity{ IBinder mRemoteService; ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { mRemoteService = iBinder; getWeatherRemote(); } @Override public void onServiceDisconnected(ComponentName componentName) { mRemoteService = null; } }; private void getWeatherRemote() { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken("MyServer"); //查询今天的天气情况 data.writeLong(TimeUtils.getNowMills()); try { mRemoteService.transact(MyServerService.WEATHER_INFO, data, reply, 0); reply.enforceInterface("MyClient"); String result = reply.readString(); ToastUtils.showLong("今天天气:" + result); } catch (RemoteException e) { e.printStackTrace(); } finally { data.recycle(); reply.recycle(); } } @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = new Intent(this,MyServerService.class); bindService(intent,mServiceConnection,BIND_AUTO_CREATE); }}
1. 首先,声明一个IBinder对象,这个对象在client来看就是server的代理,可以通过它与server通信。
2. 定义一个ServiceConnection对象,onServiceConnected和onServiceDisconnected两个方法,并在这两个方法中对刚才声明的IBinder对象进行赋值和置空。
3.使用BindService方法绑定Server进程的Service,并在参数中传入上一步定义的ServiceConnection对象,这样就可以完成由客户端到服务端的链接了。
4. client向server发送消息调用Server提供的服务,其实就是对parcel对象进行封装并传入约定值的过程,和server端接受数据并返回很像,可以直接看下代码。
5. client向server发送信息后,其实在Binder驱动中,会将client进程中执行发送数据的这个线程挂起来,然后在线程池中启动一个线程处理server端的接受逻辑,当Server端返回数据后,会notify被阻塞的client线程。
这样一个基本的Binder通信就完成了,但是如果是一些复杂数据传递起来,我们在封装parcel和解析的时候会很麻烦,还好Android对Binder的使用进行了一次封装,既AIDL。
使用AIDL实现两个应用的通信
AIDL最常用的使用场景就是两个应用之间的通信。要使用AIDL,首先要在两个应用的工程中分别建立aidl文件,注意这两个文件的包名和内容要相同,使用as建立aidl的方法如下:
创建完成后aidl文件所在的目录,这个包名是可以更改的。
看下现在里面是啥都没有的,只有一个自动生成的方法:
// IMyAidlInterface.aidlpackage com.qyy.myaidl;// Declare any non-default types here with import statementsinterface IMyAidlInterface { /** * 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);}
先不用管这个方法,我们可以修改这个aidl的接口文件,实现一些自定义的方法,比如上面说过的查询天气:
interface IMyAidlInterface { /** * 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); String getWeatherInfo(long timestamp);}
这样AIDL文件就定义好了,如果是应用间通信,别忘了这两个应用每个都要有一份这个文件。
使用AIDL通信同样有server端和client端的区别,老规矩,还是先看server端的实现。
package com.qyy.remotemonitor.ui.service;import android.app.Service;import android.content.Intent;import android.os.IBinder;import android.os.RemoteException;import android.support.annotation.Nullable;import com.qyy.myaidl.IMyAidlInterface;/** * Created by qinyy on 1/23/2019. */public class MyAIDLService extends Service{ private IMyAidlInterface.Stub mIMyAidlInterface = new IMyAidlInterface.Stub() { @Override public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException { } @Override public String getWeatherInfo(long timestamp) throws RemoteException { String weather = ""; //根据date查询天气情况 // mPresenter.getWeather( com.blankj.utilcode.util.TimeUtils.millis2Date(timestamp)); return weather; } }; @Nullable @Override public IBinder onBind(Intent intent) { return mIMyAidlInterface; }}
对比一下,和上面使用Binder写的server端是不是很像,但是更加的简洁?
可以看到,我们只要创建一个AIDL自动生成的stub对象,并将我们在aidl中定义的接口给实现,再把这个stub对象在onBind中返回就好了。而onTransact的重写、parcel的解析什么的,aidl都帮我们做好了~ 通过查看源码可以发现,这个stub对象,就是AIDL根据我们写的AIDL文件自动生成的、继承了IBinder的一个类,它已经重写了onTransact并在里面实现了parcel的解析、服务请求码的分发、向client返回结果等等操作,使用起来非常的简单。
回头看下客户端如何实现,client是存在另外一个应用中的,那这个应用中有可能有很多地方都需要server提供的服务,上面写Binder客户端的时候,我们是在bindService的时候传入一个ServiceConnection对象,并重写其中的监听方法,为了可以在client应用全局使用Server提供的服务,可以把上面的步骤写在client应用的application类中。
private IMyAidlInterface mRemoteService; private ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mRemoteService = IMyAidlInterface.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { mRemoteService = null; } };
声明一个AIDL文件接口的对象,然后和binder的使用一样,定义一个ServiceConnection,并在serviceconnect和disconnect的时候对remoteService进行赋值和置空
然后在合适的时机bind service
Intent remoteIntent = new Intent( "服务端Service的包名加服务名"); bindService(createExplicitFromImplicitIntent(MyApplication.this, remoteIntent), mServiceConnection, BIND_AUTO_CREATE);
createExplicitFromImplicitIntent方法是为了防止Service Intent must be explicit这个异常,具体代码如下:
public static Intent createExplicitFromImplicitIntent(Context context, Intent implicitIntent) { // Retrieve all services that can match the given intent PackageManager pm = context.getPackageManager(); List resolveInfo = pm.queryIntentServices(implicitIntent, 0); // Make sure only one match was found if (resolveInfo == null || resolveInfo.size() != 1) { return null; } // Get component info and create ComponentName ResolveInfo serviceInfo = resolveInfo.get(0); String packageName = serviceInfo.serviceInfo.packageName; String className = serviceInfo.serviceInfo.name; ComponentName component = new ComponentName(packageName, className); // Create a new intent. Use the old one for extras and such reuse Intent explicitIntent = new Intent(implicitIntent); // Set the component to be explicit explicitIntent.setComponent(component); return explicitIntent; }
然后可以在使用的时候就可以直接通过IMyAidlInterface 的实例调用对应Server端提供的方法了
mRemoteService.getWeatherInfo(System.currentTimeMillis());
这篇文章只是从一种浅析的、便于使用的角度来分析了Binder和AIDL,如果有兴趣的话可以继续研究Binder驱动和ServerManager的源码。
更多相关文章
- mybatisplus的坑 insert标签insert into select无参数问题的解决
- python起点网月票榜字体反爬案例
- 浅析android通过jni控制service服务程序的简易流程
- 《Android开发从零开始》——25.数据存储(4)
- Android系统配置数据库注释(settings.db)
- Android中不同应用间实现SharedPreferences数据共享
- android图表ichartjs
- Android内容提供者源码
- android SharedPreferences