声明
- 其实对于Android系统Binder通信的机制早就有分析的想法,记得2019年6、7月份Mr.Deng离职期间约定一起对其进行研究的,但因为我个人问题没能实施这个计划,留下些许遗憾…
- 文中参考了很多书籍及博客内容,可能涉及的比较多先不具体列出来了;
- 本文使用的代码是LineageOS的cm-14.1,对应Android 7.1.2,可以参考我的另一篇博客:cm-14.1 Android系统启动过程分析(1)-如何下载Nexus5的LineageOS14.1(cm-14.1)系统源码并编译、刷机
1 Parcel 概述
可以想象下,同一进程间的对象传递都是通过引用来做的,因而本质上就是传递了一个内存地址。这种方式在跨进程的情况下就无能为力了。由于采用了虚拟内存机制,两个进程都有自己独立的内存地址空间,所以跨进程传递的地址值是无效的。
进程间的数据传递是 Binder 机制中的重要一环,Android系统中担负这一重任的就是 Parcel。Parcel 是一种数据的载体,用于承载希望通过 IBinder 发送的相关信息(包括数据和对象引用)。正是基于 Parcel 这种跨进程传输数据的能力,进程间的 IPC 通信才能更加平滑可靠。Parcel 的英文直译是“打包”,是对“进程间数据传递”的形象描述。既然直接传送对象的内存地址的做法是行不通的,如果把对象在进程 A 中占据的内存相关数据打包起来,然后寄送到进程B中,由 B 在自己的进程空间中“复现”这个对象,是否可行呢?
Parcel 不仅仅是一包数据的体现,Parcel 是布局在从 Java 层到内核层系统对象识别传递机制。对象识别最关键的处理在内核,内核里决定 Parcel 里对象的属性,C++层将 Parcel里的对象实体化,Java 层将 Parcel 在 Java 层实体化,但是尽管在每一层都有实体对象,这些实体对象只是在各层体现不同,其意义都是相同的。系统中大量的 service 对象和非service 对象的本地远程机制的建立都是依靠 Parcel 机制来完成。
Parcel 就具备这种打包和重组的能力。它提供了非常丰富的接口以方便应用程序的使用,详细说明可参见官方文档https://developer.android.com/reference/android/os/Parcel.html。
2 Parcel 相关接口使用
2.1 Parcel 设置相关
存入的数据越多,Parcel 所占内存空间也就越大,可以通过以下方法来进行相关设置:
方法 | 用途 |
---|---|
public int dataSize () | 获取当前已经存储的数据大小 |
public void setDataCapacity (int size) | 设置 Parcel 的空间大小,显然存储的数据不能大于这个值 |
public void setDataPosition (int pos) | 改变 Parcel中的读写位置,必须介于 0 和 dataSize() 间 |
public int dataAvail () | 当前 Parcel 中的可读数据大小 |
public int dataCapacity () | 当前 Parcel 的存储能力 |
public int dataPosition () | 数据的当前位置值,有点类似于游标 |
2.2 Primitives
原始类型数据的读写操作。比如:
方法 | 用途 |
---|---|
public void writeByte (byte val) | 写入一个 byte |
public void readByte() | 读取一个 byte |
public void writeDouble (double val) | 写入一个 double |
public void readDouble() | 读取一个 double |
2.3 Primitive Arrays
原始数据类型数组的读写操作通常是先写入用 4 个字节表示的数据大小值,接着才写入数据本身。另外,用户既可以选择将数据读入现有的数组空间中,也可以让 Parcel 返回一个新的数组。此类方法如下:
方法 | 用途 |
---|---|
public void writeBooleanArray (boolean[] val) | 写入布尔数组 |
public void readBooleanArray (boolean[] val) | 读取布尔数组 |
public boolean[] createBooleanArray () | 读取并返回一个布尔数组 |
public void writeByteArray (byte[] b) | 写入字节数组 |
public void writeByteArray (byte[] b, int offset, int len) | 函数最后面的两个参数分别表示数组中需要被写入的数据起点以及需要写入多少 |
public void readByteArray (byte[] val) | 读取字节数组 |
public byte[] createByteArray () | 读取并返回一个数组 |
2.4 Parcelables
遵循 Parcelable 协议的对象可以通过 Parcel 来存取,经常用到的 Bundle 就是继承自Parcelable的。与这类对象相关的 Parcel 操作包括:
方法 | 用途 |
---|---|
public void writeParcelable (Parcelable p, int parcelableFlags) | 将这个Parcelable 类的名字和内容写入 Parcel 中,实际上它是通过回调此 Parcelable 的 writeToParcel0方法来写入数据的 |
public T readParcelable (ClassLoader loader) | 读取并返回一个新的 Parcelable 对象 |
public void writeParcelableArray (T[] value, int parcelableFlags) | 写入Parcelable 对象数组 |
public Creator<?> readParcelableCreator (ClassLoader loader) | 读取并返回一个Parcelable 对象数组 |
2.5 Bundles
Bundle 继承自 Parcelable,是一种特殊的 type-safe 的容器。Bundle 的最大特点就是采用键-值对的方式存储数据,并在一定程度上优化了读取效率。这个类型的 Parcel 操作包括:
方法 | 用途 |
---|---|
public Bundle readBundle () | 读取并返回一个新的 Bundle 对象 |
public Bundle readBundle (ClassLoader loader) | 读取并返回一个新的 Bundle 对象,ClassLoader 用于 Bundle 获取对应的 Parcelable对象 |
public void writeBundle (Bundle val) | 将 Bundle 写入parcel |
2.6 Active Objects
Parcel 的另一个强大武器就是可以读写 Active Objects。什么是 Active Objects 呢? 通常我们存入 Parcel 的是对象的内容,而 Active Objects 写入的则是它们的特殊标志引用。所以在从 Parcel 中读取这些对象时,大家看到的并不是重新创建的对象实例,而是原来那个被写入的实例。可以猜想到,能够以这种方式传输的对象不会很多,目前主要有两类:
- Binder : Binder 一方面是 Android 系统 IPC 通信的核心机制之一,另一方面也是一个对象。利用 Parcel 将 Binder 对象写入,读取时就能得到原始的 Binder 对象,或者是它的特殊代理实现(最终操作的还是原始 Binder 对象)。与此相关的操作包括:
public void writeStrongBinder (IBinder val) //Write an object into the parcel at the current dataPosition(), growing dataCapacity() if needed. public void writeStrongInterface (IInterface val) //Write an object into the parcel at the current dataPosition(), growing dataCapacity() if needed. public IBinder readStrongBinder () //Read an object from the parcel at the current dataPosition().
- FileDescriptor : FileDescriptor 是 Linux 中的文件描述符,可以通过 Parcel 的如下方法进行传递:
public void writeFileDescriptor (FileDescriptor val) //Write a FileDescriptor into the parcel at the current dataPosition(), growing dataCapacity() if needed. public ParcelFileDescriptor readFileDescriptor () //Read a FileDescriptor from the parcel at the current dataPosition().
因为传递后的对象仍然会基于和原对象相同的文件流进行操作,因而可以认为是 Active Object 的一种。
对于一个IInterface 的实现者,可以调用其中的 asBinder(),将其转化成 IBinder 类型就可以通过 Parcel 传递了。实际上 Parcel 中一个名称为 writeStrongInterface() 就是利用这种方法,并通过调用 writeStrongBinder() 来实现的。
2.7 Untyped Containers
它是用于读写标准的任意类型的java 容器。包括:
public Object[] readArray (ClassLoader loader)
public void readList (List<E> outVal, ClassLoader loader)
public void writeArray (Object[] val)
public void writeList (List<E> val)
......
Parcel 所支持的类型很多,足以满足开发者的数据传递请求。如果要给 Parcel 找个类比的话它更像集装箱。理由如下:
- 货物无关性
即它并不排斥所运输的货物种类,电子产品可以,汽车也行,或者零部件也同样接受。 - 不同货物需要不同的打包和卸货方案
比如运载易碎物品和坚硬物品的装箱卸货方式就肯定会有很大不同。 - 远程运输和组装集装箱的货物通常是要跨洋的,有点类似于 Parcel 的跨进程能力。不过集装箱运输公司本身并不负责所运送货物的后期组装。举个例子,汽车厂商需要把一辆整车先拆卸成零部件后才能进行装货运输。到达目的地后,货运公司只需要把货物完整无缺地交由接收方即可,并不负有组装成整车的义务。而 Parcel 则更加敬业,它会依据协议(打包和重组所用的协议必须是配套的)来为接收方提供完整还原出原始数据对象的业务。
接下来,我们看看 Parcel内部是如何实现的。应用程序可以通过 Parcel.obtain() 接口来获取一个 Parcel 对象。代码位置:frameworks/base/core/java/android/os/Parcel.java
/**
* Retrieve a new Parcel object from the pool.
*/
public static Parcel obtain() {
final Parcel[] pool = sOwnedPool; //系统预产生一个Parcel池,大小为6
synchronized (pool) {
Parcel p;
for (int i=0; i<POOL_SIZE; i++) {
p = pool[i];
if (p != null) {
pool[i] = null; //引用置为空,这样下次就知道这个Parcel已经被占用了
if (DEBUG_RECYCLE) {
p.mStack = new RuntimeException();
}
return p;
}
}
}
return new Parcel(0); //如果Parcel池中已空,则新建一个
}
Parcel的构造函数:
private Parcel(long nativePtr) {
if (DEBUG_RECYCLE) {
mStack = new RuntimeException();
}
//Log.i(TAG, "Initializing obj=0x" + Integer.toHexString(obj), mStack);
init(nativePtr); //传入的参数nativePtr为0
}
private void init(long nativePtr) {
if (nativePtr != 0) {
mNativePtr = nativePtr;
mOwnsNativeParcelObject = false;
} else {
mNativePtr = nativeCreate(); //为本地层代码准备的指针
mOwnsNativeParcelObject = true;
}
}
Parcel的JNI层实现在/frameworks/base/core/jni/android_os_Parcel.cpp 中,实际上 Parcel,java 只是一个简单的中介最终所有类型的读/写操作都是通过本地代码实现的。
static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
Parcel* parcel = new Parcel();
return reinterpret_cast<jlong>(parcel);
}
所以上面的 mNativePtr 变量实际上是本地层的一个 Parcel(C++) 对象。接下来的内容就围绕这个对象展开,分为以下两个关键部分:
- Parcel 中如何存储数据。
- 选两个代表性的数据类型 (String 和 Binder) 来分析 Parcel 的处理流程,对应的接口分别是:writeString() / readString() 和 writeStrongBinder() / readStrongBinder() 。先来看看 Parcel c++类的构造过程,代码位置:frameworks/native/libs/binder/Parcel.cpp。
Parcel::Parcel()
{
LOG_ALLOC("Parcel %p: constructing", this);
initState();
}
void Parcel::initState()
{
LOG_ALLOC("Parcel %p: initState", this);
mError = NO_ERROR; //错误码
mData = 0; //Parcel中存储的数据,注意它是一个uint8_t类型指针
mDataSize = 0; //Parcel中已经存储的数据大小
mDataCapacity = 0; //最大存储能力
mDataPos = 0; //数据指针
ALOGV("initState Setting data size of %p to %zu", this, mDataSize);
ALOGV("initState Setting data pos of %p to %zu", this, mDataPos);
mObjects = NULL;
mObjectsSize = 0;
mObjectsCapacity = 0;
mNextObjectHint = 0;
mObjectsSorted = false;
mHasFds = false;
mFdsKnown = true;
mAllowFds = true;
mOwner = NULL;
#ifndef DISABLE_ASHMEM_TRACKING
mOpenAshmemSize = 0;
#endif
// racing multiple init leads only to multiple identical write
if (gMaxFds == 0) {
struct rlimit result;
if (!getrlimit(RLIMIT_NOFILE, &result)) {
gMaxFds = (size_t)result.rlim_cur;
//ALOGI("parcel fd limit set to %zu", gMaxFds);
} else {
ALOGW("Unable to getrlimit: %s", strerror(errno));
gMaxFds = 1024;
}
}
}
Parcel 对象的初始化过程只是简单地给各个变量赋了初值,并没有我们设想中的内存分配动作。这是因为 Parcel 遵循的是“动态扩展”的内存申请原则,只有在需要时才会申请内存以避免资源浪费。我们先来介绍上面代码段中出现的几个重要变量,因为后面的读写操作实际上就是围绕它们而实施的。
2.8 以writeString()为例分析其实现原理
本小节剩余内容将结合一个范例来详细讲解 writeString 的实现原理,这个范例是 ServiceManagerProxy 的 getService() 方法中对 Parcel 的操作。源码如下:
//获得一个 Parcel 对象,它最终是创建了一个本地的 Parcel 实例,并做了全面的初始化操作。
Parcel data = Parcel.obtain();
......
//用于写入 IBinder 接口标志,所带参数是 String 类型的,如IServiceManager.descriptor = "android.os.IServiceManager"
data.writeInterfaceToken(IServiceManager.descriptor);
//在 Parcel中写入需要向 ServiceManager 查询的 Service 名称
data.writestring(name);
Parcel 在整个 IPC 中的内部传递过程比较烦琐,特别在承载 Binder 数据时更是需要多次转换,因而容易让人失去方向。但不管过程如何曲折,有一点是始终不变的。那就是:写入方和读取方所使用的协议必须是完全一致的。(正如上面所举集装箱的例子,装货和卸货的规则是成套的,不能装货时用的是对付易碎物品的方式,而卸货时又把它当成坚硬物品)
来看看写入方 (ServiceManagerProxy) 都“装”了些什么东西到“集装箱”中。
// Write RPC headers. (previously just the interface token)
status_t Parcel::writeInterfaceToken(const String16& interface)
{ //无需深入研究getStrictModePolicy()的实现,只要知道此函数获得了一个int数值即可
writeInt32(IPCThreadState::self()->getStrictModePolicy() |
STRICT_MODE_PENALTY_GATHER);
// currently the interface identification token is just its name as a string
return writeString16(interface);
}
其中,interface就是"android.os.IServiceManager"。再来分析writeInt32 和 wirteString16所做的工作。
status_t Parcel::writeInt32(int32_t val)
{
return writeAligned(val);
}
这个函数的实现很简单一一只包含了一句代码。从函数名来判断,它是将 val 值按照对齐方式写入 Parcel 的存储空间中。换句话说,就是将数据写入 mDataPos 起始的mData中(当然内部还需要判断当前的存储能力是否满足要求、是否要申请新的内存等)。
status_t Parcel::writeString16(const String16& str)
{
return writeString16(str.string(), str.size());
}
status_t Parcel::writeString16(const char16_t* str, size_t len)
{
if (str == NULL) return writeInt32(-1);
status_t err = writeInt32(len); //先写入数据长度
if (err == NO_ERROR) {
len *= sizeof(char16_t); //长度*单位大小=占用的空间
uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
if (data) {
memcpy(data, str, len); //将数据复制到data所指向的位置中
*reinterpret_cast<char16_t*>(data+len) = 0;
return NO_ERROR;
}
err = mError;
}
return err;
}
整个 writeString16 的处理过程并不难理解:首先要填写数据的长度,占据4 个字节;然后计算出数据所需占据的空间大小; 最后才将数据复制到相应位置中一writelnplace 就是用来计算复制数据的目标地址的(下面分段阅读)。
void* Parcel::writeInplace(size_t len)
{
if (len > INT32_MAX) {
// don't accept size_t values which may have come from an
// inadvertent conversion from a negative int.
return NULL;
}
//pad_size用于计算“当以4 对齐时,容纳 len 大小的数据需要多少空间”(注意 len 在传进来时进行了 len *= sizeof(char16 t)),
const size_t padded = pad_size(len);
// 如果溢出则返回 NULL
if (mDataPos+padded < mDataPos) {
return NULL;
}
if ((mDataPos+padded) <= mDataCapacity) { // 数据大小没超过容量
restart_write:
//printf("Writing %ld bytes, padded to %ld\n", len, padded);
uint8_t* const data = mData+mDataPos;
// Need to pad at end?
if (padded != len) { //需要填充尾部的情况
#if BYTE_ORDER == BIG_ENDIAN //如果是Big Endian的情况
static const uint32_t mask[4] = {
0x00000000, 0xffffff00, 0xffff0000, 0xff000000
};
#endif
#if BYTE_ORDER == LITTLE_ENDIAN //如果是Little Endian的情况
static const uint32_t mask[4] = {
0x00000000, 0x00ffffff, 0x0000ffff, 0x000000ff
};
#endif
//printf("Applying pad mask: %p to %p\n", (void*)mask[padded-len],
// *reinterpret_cast<void**>(data+padded-4));
*reinterpret_cast<uint32_t*>(data+padded-4) &= mask[padded-len]; //填充尾部
}
finishWrite(padded); //更新mDataPos
return data;
}
//如果执行到这,说明数据大小已经超过了mDataCapacity的存储能力,需要扩大容量
status_t err = growData(padded);
if (err == NO_ERROR) goto restart_write; //重新开启业务
return NULL;
}
上面代码段的逻辑是:如果空间足够,就直接进行尾部的填充操作。否则要通过growData()申请更多的存储空间,然后返回restart_write 继续执行。尾部需要 padding 多少数据是由 len 和 padded 综合决定的,如当len=5 时,padded=8,这时就有3 个字节是填充数据(最多不超过)。填充数据的写法采用的是:
*reinterpret_cast<uint32_t*>(data+padded-4) &= mask[padded-len];
这里先做了 uint32_t 的强制转换,所以后续操作就是以4 个字节为单位,这样可以一次性把3个字节的填充数据(即 mask[3] = 0xf000000 或 0x00000取决于是 LITTLE_ENDIAN 还是 BIG_ENDIAN )都写入。最后调用 finishWrite() 来调整 mDataPos 指针,并返回 data 的位置以供调用者写入真正的数据内容。
可以看出,writeInplace() 用于确认即将写入的数据的起始和结束位置,并做好 padding 工作。
再回到前面 writeString16() 继续分析。因为有了需要写入的地址(即 data 指针),所以可以直接 memcpy。如下:
memcpy(data, str, len); //将数据复制到data所指向的位置中
*reinterpret_cast<char16_t*>(data+len) = 0; //最后写入字符串结束符0
通过上面的分析,写入一个String(writeString16)的步骤:
- writeInt32(len)
- memcpy
- padding(有些情况下不需padding。而且源码实现中这一步是在memcpy 之前)
回到ServiceManagerProxy 中的getService 里:
data.writeInterfaceToken(IServiceManager.descriptor);
data.writestring(name);
我们把上面两个语句进行分解,就得到写入方的工作了。
WriteInterfaceToken = writeInt32(policy value) + writeString16(interface)
writeString16(interface) = writeInt32(len) + 写入数据本身 + 填充
根据上面强调的准则,读取方也必须遵循同样的数据操作顺序。来做下验证,读取方是:frameworks/native/cmds/servicemanager/service_manager.c
uint16_t svcmgr_id[] = {
'a','n','d','r','o','i','d','.','o','s','.',
'I','S','e','r','v','i','c','e','M','a','n','a','g','e','r'
};
......
int svcmgr_handler(struct binder_state *bs,
struct binder_transaction_data *txn,
struct binder_io *msg,
struct binder_io *reply)
{
struct svcinfo *si;
uint16_t *s;
size_t len;
uint32_t handle;
uint32_t strict_policy;
int allow_isolated;
//ALOGI("target=%p code=%d pid=%d uid=%d\n",
// (void*) txn->target.ptr, txn->code, txn->sender_pid, txn->sender_euid);
if (txn->target.ptr != BINDER_SERVICE_MANAGER)
return -1;
if (txn->code == PING_TRANSACTION)
return 0;
// Equivalent to Parcel::enforceInterface(), reading the RPC
// header with the strict mode policy mask and the interface name.
// Note that we ignore the strict_policy and don't propagate it
// further (since we do no outbound RPCs anyway).
strict_policy = bio_get_uint32(msg);
s = bio_get_string16(msg, &len);
if (s == NULL) {
return -1;
}
if ((len != (sizeof(svcmgr_id) / 2)) ||
memcmp(svcmgr_id, s, sizeof(svcmgr_id))) {
fprintf(stderr,"invalid id %s\n", str8(s, len));
return -1;
}
switch(txn->code) {
case SVC_MGR_GET_SERVICE:
case SVC_MGR_CHECK_SERVICE:
s = bio_get_string16(msg, &len); //获取查询的 service name
if (s == NULL) {
return -1;
}
handle = do_find_service(s, len, txn->sender_euid, txn->sender_pid);
if (!handle)
break;
bio_put_ref(reply, handle);
return 0;
......
}
......
}
上面代码段用于判断收到的 interface 是否正确。其中 svcmgr_id 和前面的 “android.os.IServiceManager” 是一样的。可看到,ServiceManager 对数据的读取过程和数据的写入过程确实完全一致。这样我们以String为例,完整地分析了 Parcel 对基本数据类型的读写流程。相对于基础数据类型,Binder(ActiveObject) 的跨进程传递要复杂很多,就不再分析。