关于Parcel的使用

在分析Parcel之前,首先按照分析流程,介绍下关于Parcel的相关常规使用。

首先是关于Parcel的获取:

Parcel parcle = Parcel.Obtain();

额,这感觉似曾相识啊,我们这里大胆猜测Parcel的初始化也是由其对象池进行初始化的。在得到了Parcel对象之后,下一步的工作。嗯,我想起来,应该介绍下Parcel的作用吧:

其实看到这篇文章的各位,应该也不需要这种科普吧,哈哈。我从源码注释中截取如下:

*Container for a message (data and object references) that can
* be sent through an IBinder. A Parcel can contain both flattened data
* that will be unflattened on the other side of the IPC (using the various
* methods here for writing specific types, or the general

从这段注释中可以看出,Parcel是一个容器,他可以包含数据或者是对象引用,并且能够用于Binder的传输。同时支持序列化以及跨进程之后进行反序列化,同时其提供了很多方法帮助开发者完成这些功能。ok,对这里差不多明朗了,Parcel主要就是用来进行IPC通信的。当然不仅仅是Binder这一种跨进程通信。

接下来回到这题,既然Parcel是一个容器,那么肯定需要向其中传入数据才行啊,没错,所以在初始化Parcel之后,需要进行如下操作:

parcel.writeInt(int val);

向Parcel中传入一个Int型的数据,接下来还有:

parcel.writeString(String val);

向Parcel中传入一个String型的数据。

这里只以这两种最为常见的数据类型的写入作为例子,实际上Parcel所支持的数据类型可多了去了,具体可以如下图所示:

在完成了数据的写入之后,就需要进行数据的序列化:

parcel.marshall();

在经过上一步的处理之后,返回了一个byte数组,主要的IPC相关的操作主要就是围绕此byte数组进行的。同时,由于parcel的读写都是一个指针操作的,这一步涉及到native的操作,所以,在将数据写入之后,需要将指针手动指向到最初的位置,即如下的操作:

parcel.setDataPosition(0);

到此处,Parcel的这一步操作还没有收尾,想想前面parcel的Obtain()方法,我们有理由相信,parcel的销毁应该是使用了对应的recycle()方法。

所以此处有:

parcel.recycle();

将此Parcel对象进行释放,完成了IPC操作的一半。至于是如何将数据传输过去的,暂不进行展开。此处在IPC的另一端的Parcel的获取处理。

再进行了IPC的操作之后,一般读取出来的就是之前序列化的byte数组,所以,首先要进行一个反序列化操作,即如下的操作:

parcel.unmarshall(byte[] data, int offest, int length);

其中的参数分别是这个byte数组,以及读取偏移量,以及数组的长度。

此时得到的parcel就是一个正常的parcel对象,这时就可以将之前我们所存入的数据按照顺序进行获取,即:

parcel.readInt();

以及

parcel.readString();

读取完毕之后,同样是一个parcel的回收操作:

parcel.recycle();

以上就是parcel的常规使用,获取有些朋友不太知道parcel的使用场景,其实最常见的,在我们编写完AIDL的接口之后,IDE会自动生成一个对应的.java文件,这个java文件就是实际用来进行aidl的通信的,在这个实现里面,数据的传递就是使用的parcel,当然还有其他的应用场景,这里只说了一个大家都比较常见的实践。

关于Parcel的实现

之前有提到过,parcel的使用对于java开发者来说,还是比较陌生的,像极了指针的操作,所以基本可以确定java层对于parcel的处理仅仅是一个封装代理,实际的实现在c/c++ native。既然这样的话,我们就应该想到,parcel的使用同样涉及到jni的使用。所以我们目前的思路就是在源码中找到parcel的三层代码(Java-Jni-C)。

我的具体做法是直接使用 everything 在源码目录下搜索 parcel,然后根据之前的思路进行包的导出,我的分析基础就是以下的几个包里的实现:

Java层:

\frameworks\base\core\java\android\os

JNI:

\frameworks\base\core\jni

native:

\frameworks\native\libs\binder

然后导入这几个包中的文件方便检索:

根据我们以上的使用顺序来进行分析,首先需要进行一个Parcel的获取,看看Java层的实现:

 /**
* Retrieve a new Parcel object from the pool.
*/
public static Parcel obtain() {
final Parcel[] pool = sOwnedPool;
synchronized (pool) {
Parcel p;
for (int i=0; i<POOL_SIZE; i++) {
p = pool[i];
if (p != null) {
pool[i] = null;
if (DEBUG_RECYCLE) {
p.mStack = new RuntimeException();
}
return p;
}
}
}
return new Parcel(0);
}

从注释也可以看出,Parcle的初始化,主要是使用一个对象池进行的,这样可以提高性能以及内存消耗。首先要明确的是,源码中定义的池子有两个:

    private static final int POOL_SIZE = 6;
private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE];

从名字也可以看出,不同池子的作用,sOwnedPool 这个池子主要就是用来存储parcel的,Obtain()方法首先会去检索池子中的parcel对象,若是能取出parcel,那么先将这个这个parcel返回,同时将这个位置置空。若是现在连池子都不存在的话,那么就直接新建一个parcel对象。这里的实现与Handler中的message采用同样的处理。

我们了解了获取之后,比较关心的就是如何去新建一个parcel对象,也就是new这个过程,那么看看此处中的parcel构造方法:

private Parcel(int nativePtr) {
if (DEBUG_RECYCLE) {
mStack = new RuntimeException();
}
//Log.i(TAG, "Initializing obj=0x" + Integer.toHexString(obj), mStack);
init(nativePtr);
}

可以看到,在此处参数名称被称为:nativePtr,这个大家都比较熟悉了,ptr嘛,指的就是一个指针,这里又是一个封装,需要继续深入看实现:

private void init(int nativePtr) {
if (nativePtr != 0) {
mNativePtr = nativePtr;
mOwnsNativeParcelObject = false;
} else {
mNativePtr = nativeCreate();
mOwnsNativeParcelObject = true;
}
}

这里首先对参数进行检查,这里因为初始化传入的参数是0,那么直接执行nativeCreate(),并且将标志位mOwnsNativeParcelObject 置为true,表示这个 parcel已经在native进行了创建。

此处的ativeCreate()是一个native方法,其具体实现已经切换到native环境了,那么我们此时的分析就要从jni进行了,经过检索,在jni的代码中,其实现为以下函数:

static jint android_os_Parcel_create(JNIEnv* env, jclass clazz)
{
Parcel* parcel = new Parcel();
return reinterpret_cast<jint>(parcel);
}

这是一个jni的实现,首先是调用了native的初始化,并且,返回操作这个对象的指针:

Parcel::Parcel()
{
initState();
}

是一个c++的构造方法,关于析构方法,暂时不管,其中的init实现为:

void Parcel::initState()
{
mError = NO_ERROR;
mData = 0;
mDataSize = 0;
mDataCapacity = 0;
mDataPos = 0;
ALOGV("initState Setting data size of %p to %d\n", this, mDataSize);
ALOGV("initState Setting data pos of %p to %d\n", this, mDataPos);
mObjects = NULL;
mObjectsSize = 0;
mObjectsCapacity = 0;
mNextObjectHint = 0;
mHasFds = false;
mFdsKnown = true;
mAllowFds = true;
mOwner = NULL;
}

可以看出,对parcel的初始化,只是在native层初始化了一些数据值。

在完成初始化之后,就将这个操作指针给返回。这样就完成了parcel的初始化。

初始化完毕之后,就可以进行数据的写入了,首先写入一个int型数据,其java层实现如下:

 /**
* Write an integer value into the parcel at the current dataPosition(),
* growing dataCapacity() if needed.
*/
public final void writeInt(int val) {
nativeWriteInt(mNativePtr, val);
}

可以看出,在这里java层就纯粹是一个对于native实现的封装了,这时候的分析来到jni:

static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jint nativePtr, jint val) {
Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
const status_t err = parcel->writeInt32(val);
if (err != NO_ERROR) {
signalExceptionForError(env, clazz, err);
}
}

在这里我们要特别注意两个参数,一个是之前传上去的指针以及需要保存的int数据,这两个值分别是:

jint nativePtr, jint val

首先是根据这个指针,这里说一下,指针实际上就是一个整型地址值,所以这里使用强转将int值转化为parcel类型的指针是可行的,然后使用这个指针来操作native的parcel对象,即:

const status_t err = parcel->writeInt32(val);

这里注意到我们是写入了一个32位的int值,这个点一定要注意,32位,4个字节。

深入进去看看实现:

status_t Parcel::writeInt32(int32_t val)
{
return writeAligned(val);
}

可以看出,这里实际上调用了:

writeAligned(val);

来进行数据的写入,这里理解下align的意思,实际上是一个对齐写入,怎么个对齐法,看看:

template<class T>
status_t Parcel::writeAligned(T val) {
COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE(sizeof(T)) == sizeof(T)); if ((mDataPos+sizeof(val)) <= mDataCapacity) {
restart_write:
*reinterpret_cast<T*>(mData+mDataPos) = val;
return finishWrite(sizeof(val));
} status_t err = growData(sizeof(val));
if (err == NO_ERROR) goto restart_write;
return err;
}

在这个方法中首先是一个断言检查,然后对输入的参数取size值,再加上之前已经移动的位置,判断是否超过了该Pacel所定义的能力值mDataCapacity。

若是超过了能力值的话,那么直接将能力值进行扩大,扩大的值是val值的大小,比如,int值是32bit,那么就增加4个字节,返回的结果是状态值,若是没有出错的话,就利用goto语句执行,这里的goto的语句只要是一个指针的操作,将指针移动到端点,然后写入val的size值。这里可以看出这个函数的意义,因为无论是否超过能力值它都会写入T类型值的size值。

到这里,Parcel就写入了一个Int型的值。

同样的思路,大家可以参考以上的分析,继续进行Parcel一个常规使用的分析,我之前是想将全部的实现都分析出来的,但是后来发现,大体的思路都差不多,这么写的话,会多出来很多废话,所以接下来的分析,大家如果有兴趣的话,就继续分析下去,欢迎一起进行讨论!

另外,在分析过程中,我对Android的JNI调用进行一番探索,总之一句话就是说Jvm环境切换到Native环境之中后,Java如何通过Java层声明的native方法来查找到对应的JNI方法的?因为我对JVM的实现这一部分没有太多了解,所以只能从Android源码中代码层面上来分析,至少在Android中:

在切换到native环境之后,实际上,这两种函数的映射是由一个多重数组来进行管理的,具体如下:

static const JNINativeMethod gParcelMethods[] = {
{"nativeDataSize", "(I)I", (void*)android_os_Parcel_dataSize},
{"nativeDataAvail", "(I)I", (void*)android_os_Parcel_dataAvail},
{"nativeDataPosition", "(I)I", (void*)android_os_Parcel_dataPosition},
{"nativeDataCapacity", "(I)I", (void*)android_os_Parcel_dataCapacity},
{"nativeSetDataSize", "(II)V", (void*)android_os_Parcel_setDataSize},
{"nativeSetDataPosition", "(II)V", (void*)android_os_Parcel_setDataPosition},
{"nativeSetDataCapacity", "(II)V", (void*)android_os_Parcel_setDataCapacity}, {"nativePushAllowFds", "(IZ)Z", (void*)android_os_Parcel_pushAllowFds},
{"nativeRestoreAllowFds", "(IZ)V", (void*)android_os_Parcel_restoreAllowFds}, {"nativeWriteByteArray", "(I[BII)V", (void*)android_os_Parcel_writeNative},
{"nativeWriteInt", "(II)V", (void*)android_os_Parcel_writeInt},
{"nativeWriteLong", "(IJ)V", (void*)android_os_Parcel_writeLong},
{"nativeWriteFloat", "(IF)V", (void*)android_os_Parcel_writeFloat},
{"nativeWriteDouble", "(ID)V", (void*)android_os_Parcel_writeDouble},
{"nativeWriteString", "(ILjava/lang/String;)V", (void*)android_os_Parcel_writeString},
{"nativeWriteStrongBinder", "(ILandroid/os/IBinder;)V", (void*)android_os_Parcel_writeStrongBinder},
{"nativeWriteFileDescriptor", "(ILjava/io/FileDescriptor;)V", (void*)android_os_Parcel_writeFileDescriptor}, {"nativeCreateByteArray", "(I)[B", (void*)android_os_Parcel_createByteArray},
{"nativeReadInt", "(I)I", (void*)android_os_Parcel_readInt},
{"nativeReadLong", "(I)J", (void*)android_os_Parcel_readLong},
{"nativeReadFloat", "(I)F", (void*)android_os_Parcel_readFloat},
{"nativeReadDouble", "(I)D", (void*)android_os_Parcel_readDouble},
{"nativeReadString", "(I)Ljava/lang/String;", (void*)android_os_Parcel_readString},
{"nativeReadStrongBinder", "(I)Landroid/os/IBinder;", (void*)android_os_Parcel_readStrongBinder},
{"nativeReadFileDescriptor", "(I)Ljava/io/FileDescriptor;", (void*)android_os_Parcel_readFileDescriptor}, ........

以下还有很多映射关系,这样通过映射就可以将函数给进行对应了,但是还有一点,这个东西是何时,以及何处进行调用的,这个展开说又是一个漫长的故事了,所以这一段我也不进行分析了,大家知道有这么一个东西就ok了,当然欢迎一起进行讨论哦!

Android Parcel对象详解的更多相关文章

  1. Android AIDL使用详解_Android IPC 机制详解

    一.概述 AIDL 意思即 Android Interface Definition Language,翻译过来就是Android接口定义语言,是用于定义服务器和客户端通信接口的一种描述语言,可以拿来 ...

  2. Android之canvas详解

    首先说一下canvas类: Class Overview The Canvas class holds the "draw" calls. To draw something, y ...

  3. 【转】Android Canvas绘图详解(图文)

    转自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1212/703.html Android Canvas绘图详解(图文) 泡 ...

  4. Android GLSurfaceView用法详解(二)

    输入如何处理       若是开发一个交互型的应用(如游戏),通常需要子类化 GLSurfaceView,由此可以获取输入事件.下面有个例子: java代码: package eoe.ClearTes ...

  5. android屏幕适配详解

    android屏幕适配详解 官方地址:http://developer.android.com/guide/practices/screens_support.html 一.关于布局适配建议 1.不要 ...

  6. Android Studio 插件开发详解三:翻译插件实战

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/78113868 本文出自[赵彦军的博客] 一:概述 如果不了解插件开发基础的同学可以 ...

  7. Android Studio 插件开发详解二:工具类

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/78112856 本文出自[赵彦军的博客] 在插件开发过程中,我们按照开发一个正式的项 ...

  8. Android Socket通信详解

    一.Socket通信简介  Android与服务器的通信方式主要有两种,一是Http通信,一是Socket通信.两者的最大差异在于,http连接使用的是“请求—响应方式”,即在请求时建立连接通道,当客 ...

  9. JMessage Android 端开发详解

    目前越来越多的应用会需要集成即时通讯功能,这里就为大家详细讲一下如何通过集成 JMessage 来为你的 App 增加即时通讯功能. 首先,一个最基础的 IM 应用会需要有哪些功能? 用户注册 / 登 ...

随机推荐

  1. c#中char、string转换为十六进制byte的浅析

    问题引出: string转换为byte(十六进制) static void Main(string[] args) { "; byte[] b = Encoding.Default.GetB ...

  2. mysql 1709: Index column size too large. The maximum column size is 767 bytes.

    1709: Index column size too large. The maximum column size is 767 bytes. 修改排序规则解决 utf8_general_ci

  3. Unknown character set: 'utf8mb4'

    出现Unknown character set: 'utf8mb4'该错误是因为你的mysql-connector-java版本太高了,现在的mysql编码方式utf8mb4  然而老版本的却是utf ...

  4. SSH认证原理和批量分发管理

    SSH密码认证原理 几点说明: 1.服务端/etc/ssh目录下有三对公钥私钥: [root@m01 ssh]# ls moduli ssh_config sshd_config ssh_host_d ...

  5. Microsoft .NET Native

    首页: https://msdn.microsoft.com/en-US/vstudio/dotnetnative

  6. JavaScript中常用的BOM属性

    window 窗口 window.open():打开窗口.返回一个指向新窗口的引用. window.close():关闭窗口. window.resizeTo():调整窗口尺寸到指定值 window. ...

  7. 10:django 模板语言

    Django的模板语言的目的是取得力量和易用性之间的平衡,与其他的模板语言相比,django模板语言显得更简单,更专一, django模板系统由模板,变量,过滤器,标签,注释等主要部分组成 模板 一个 ...

  8. 51Nod 1022 石子归并 V2(区间DP+四边形优化)

    题目链接:http://www.51nod.com/onlineJudge/questionCode.html#!problemId=1022 题目大意: N堆石子摆成一个环.现要将石子有次序地合并成 ...

  9. inline-block,vertical-align:middle

    现在inline-block貌似可以替代float来实现多个item的排列分布吧 div是块级元素,如果不设置他的明确的宽度,那他就等于父元素的宽度,如果想让他其它随着子元素的变化而变化,需要改变他的 ...

  10. (编译)使用 AppCenter 持续输出导出到 Application Insights

    原文地址:https://blog.xamarin.com/appcenter-continuous-export-application-insights/ 五星手机应用有一个特殊的特点:他们不会放 ...