java parcelable_Parcelable最强解析

这两天有个同事在使用泛型的过程中,T extends BaseBean,对BaseBean类实现了parceable接口,当一个Activity中跳转到另一个Activity的时候,intent.putExtra("key",childBean),用到ChildBean对象,该类直接继承了BaseBean,他觉得在另外一个Acitivty拿不到ChildBean中的数据信息,甚至当他在用ChildBean=getIntent().getParcelableExtra()的时候出现了类型转换错误,用BaseBean=getIntent().getParcelableExtra()确没有问题,一时间对父类实现parcelable接口,子类是否有必要实现parcelable接口,然后传值产生了争议,相信也有不少同学也有这样的困惑,所以有了这篇文章 (这里的Basebean和ChildBean是指父类和子类,正文的也是这个意思)

1.Java serialization algorithm

答:当我们对一个对象实现Serializable 接口的时候,它会告诉序列化机制这个类是可以序列化的,java会通过文件流的形式,将object写在一个文件file当中,

public static void main(String args[]) throws IOException {

FileOutputStream fos = new FileOutputStream("temp.out");

ObjectOutputStream oos = new ObjectOutputStream(fos);

TestSerial ts = new TestSerial();

oos.writeObject(ts);

oos.flush();

oos.close();

}

这里我们要注意ObjectOutputStream的构造对象,会写如流的header,在这里注意下code后面的注释,因为在例子上面都要给对上的。

public ObjectOutputStream(OutputStream out) throws IOException {

verifySubclass();

......

writeStreamHeader();

.....

}

protected void writeStreamHeader() throws IOException {

bout.writeShort(STREAM_MAGIC);//这里写入序列化协议

bout.writeShort(STREAM_VERSION);//这里写入序列化的版本

}

protected void writeStreamHeader() throws IOException {

bout.writeShort(STREAM_MAGIC);

bout.writeShort(STREAM_VERSION);

}

具体的实现是在:

private void writeObject0(Object obj, boolean unshared) throws IOException

{

······

else if (obj instanceof Serializable) {

writeOrdinaryObject(obj, desc, unshared);

}

······

·····

}

如上面代码所示,刚开始的时候,对象是一个Serializable,所以会走writeOrdinaryObject(obj, desc, unshared);方法:

private void writeOrdinaryObject(Object obj,ObjectStreamClass desc,boolean unshared) throws IOException

{

······

try {

desc.checkSerialize();

bout.writeByte(TC_OBJECT);//这里写入TC_OBJECT

writeClassDesc(desc, false);//接着写classDesc

······

} finally {

if (extendedDebugInfo) {

debugInfoStack.pop();

}

}

}

上面再写入TC_OBJECT之后,就调用writeClassDesc方法,在这里我就不继续分析了,因为文章的重点不应该在Serializable的分析上,接下来都是些java代码的调用,也有源码,如果你自己感兴趣,相信你们也可以随便看看源代码就能分析出来,在这里我就不浪费大家的时间了,不过要提一下,写的时候,是先写自身类的描述,然后如果有父类就写父类的描述,如果自身类包含的字段是一个对象,再写该对象的描述,都写完了,最后写字段的数据。在这里对一个类获取里面的字段,方法等是用到了反射机制

以下是一个对象写入的例子,假设一个类是:

class TestSerial implements Serializable {

public byte version = 100;

}

如上一个对象所示,在写入磁盘的时候,保存的数据如下:

AC ED (序列化协议)

00 05 (序列化版本)

73 (TC_OBJECT. 新的对象)

72 (TC_CLASSDESC. 这是一个新类描述)

00 0A (类名的长度)

53 65 72 69 61 6C 54 65 73 74 (类的名称)

05 52 81 5A AC 66 02 F6 (SerialVersionUID)

02 (Various flags,0x02代表这个对象支持序列化)

00 01 (类有几个字段)

49 (代表是int类型)

00 07 (字段名称的长度)

76 65 72 73 69 6F 6E (version, 字段的名称)

78 (TC_ENDBLOCKDATA, 描述的结束符)

70 (TC_NULL)

00 00 00 64 (version的值)

从上面可以看到serialiable的序列化和反序列化会创造大量的对象和写入数据的时候,会写入除去真实数据以外的其它数据,比如序列化协议,版本等等。

2.Parcable 机制的原理?

首先我们在一个实体对象在实现parcelable的时候,这个时候,我们会重写writeToParcel方法,其中执行dest.writeInt(this.offLineBtn);writeLong等等类型的数据,实际是执行native方法,在这里我们就不分析各种数据类型的存取了,我们现在拿一个代表int来分析下,看下jni方法:

static void android_os_Parcel_writeInt(JNIEnv* env, jclass clazz, jint nativePtr, jint val) {

Parcel* parcel = reinterpret_cast(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);

writeInt32是调用了parcel中的方法,parcel的实现类是在Framework/native/libsbinderParcel.cpp,我们看下writeInt32方法:

status_t Parcel::writeInt32(int32_t val)

{

return writeAligned(val);

}

status_t Parcel::writeAligned(T val) {

COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));

if ((mDataPos+sizeof(val)) <= mDataCapacity) {

restart_write:

*reinterpret_cast(mData+mDataPos) = val;

return finishWrite(sizeof(val));

}

status_t err = growData(sizeof(val));

if (err == NO_ERROR) goto restart_write;

return err;

}

分析上面的之前,首先要知道mData、mDataPos、mDataCapacity三个变量的意义,mData指向parcel缓存的首地址,mDataCapacity表示parcel缓存容量(大小),mDataPos指向parcel缓存中空闲区域的首地址,整个parcel缓存是一块连续的内存。

物理地址 = 有效地址+偏移地址,首先会判断先写入的int数据的字节数是否超过了data的容量,如果没有超过,会执行数据的写入,reinterpret_cast是c++的一种再解释,强制转换,上面首先会将mData+mDataPos得到物理地址,转成指向T类型的指针(T类型就是你传进来的变量的类型),然后将val赋值给指针指向的内容。然后修改偏移地址,finishWrite(sizeof(val)):

status_t Parcel::finishWrite(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 BAD_VALUE;

}

//printf("Finish write of %d\n", len);

mDataPos += len;

ALOGV("finishWrite Setting data pos of %p to %zu", this, mDataPos);

if (mDataPos > mDataSize) {

mDataSize = mDataPos;

ALOGV("finishWrite Setting data size of %p to %zu", this, mDataSize);

}

//printf("New pos=%d, size=%d\n", mDataPos, mDataSize);

return NO_ERROR;

}

上面主要是将修改偏移地址,将偏移地址加上新增加的数据的字节数。

如果增加的数据大于容量的话,那么首先扩展parcel的缓存空间,growData(sizeof(val)):

status_t Parcel::growData(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 BAD_VALUE;

}

size_t newSize = ((mDataSize+len)*3)/2;

return (newSize <= mDataSize)

? (status_t) NO_MEMORY

: continueWrite(newSize);

}

扩展成功,就继续goto restart_write,在writeAligned方法中有restart_write,执行restart_write后面code,写入数据。

通过上面的解释相信大家已经明白int类型的数据写入parcel缓存了,既然知道存数据,那我们也要明白取数据了,在取数据的时候,我们会通过this.age = in.readInt();来取得int类型数据

static jint android_os_Parcel_readInt(jlong nativePtr)

{

Parcel* parcel = reinterpret_cast(nativePtr);

if (parcel != NULL) {

return parcel->readInt32();

}

return 0;

}

调用的parcel的readInt32方法:

int32_t Parcel::readInt32() const

{

return readAligned();

}

T Parcel::readAligned() const {

T result;

if (readAligned(&result) != NO_ERROR) {

result = 0;

}

return result;

}

status_t Parcel::readAligned(T *pArg) const {

COMPILE_TIME_ASSERT_FUNCTION_SCOPE(PAD_SIZE_UNSAFE(sizeof(T)) == sizeof(T));

if ((mDataPos+sizeof(T)) <= mDataSize) {

const void* data = mData+mDataPos;

mDataPos += sizeof(T);

*pArg = *reinterpret_cast(data);

return NO_ERROR;

} else {

return NOT_ENOUGH_DATA;

}

}

读取数据的时候,首先我们会从parcel的起始地址+parcel偏移地址,得到读取的数据的地址,然后取出数据,然后将parcel的偏移地址+取出的数据的字节数,这样指针就可以指向下一个数据,这样说太抽象了,举个例子:

比如我们现在有一个对象,里面是

stu{

int age = 32;

double score = 99;

}

我们在写数据的时候,会在一块parcel的内存地址中,写32,99,然后读取的时候,会从起始地址+读取的字节数,来一一读取,首先读取parcel起始地址指向的数据,取出32,然后将指针地址偏移int字节数,指针指向99的地址,然后读取99,然后取出数据,这也就是parcelable在实现的时候为什么需要存和读取的顺序需要一致的原因。

3.在我们了解了,parcelable的实现原理的时候,我们就可以解答引言上面的问题了。

3.1 对BaseBean类实现了parceable接口,当一个Activity中跳转到另一个Activity的时候,intent.putExtra("key",childBean),另一个Activity能否用拿到数据?

答:因为在父类的BaseBean里面都有实现BaseBean中字段的读写,所以BaseBean中字段的数据是可以拿到的。

3.2 在用ChildBean=getIntent().getParcelableExtra()的时候出现了类型转换错误,用BaseBean=getIntent().getParcelableExtra()确没有问题?

答:其实这里是要看BaseBean中读数据,返回的对象是什么了?

public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {

@Override

public BaseBean createFromParcel(Parcel source) {

return new BaseBean(source);

}

@Override

public BaseBean[] newArray(int size) {

return new BaseBean[size];

}

};

很明显,在这里返回的BaseBean的对象,当你用ChildBean去接收的时候肯定会出现类型转换错误啦,如果还觉得想用ChildBean来接收的话(前提是有强迫症),可以重写createFromParcel方法

@Override

public BaseBean createFromParcel(Parcel source) {

ChildBean childBean = new ChildBean();

childBean.setName(source.readString());

childBean.setPrice(source.readDouble());

return childBean;

}

这不返回ChildBean不就可以了,当然不管你是哪种方式,如果childBean没有实现parceable的话,对于childBean中的字段是无法传递的.

attention:这个和Serializable的实现是不同的,Serializable是父类实现了Serializable,子类不需要实现Serializable,子类的数据也能够传递了,因为在写入数据的判断(obj instanceof Serializable),如果父类实现Serializable,子类肯定也是instanceof Serializable。

3.3 如果我们需要用到一个公共的界面,这个公共的界面可能是通过泛型T t =getIntent().getParcelableExtra()来获取数据的解决方案?

答:这里我们的BaseBean不应该是一个类,最合适的话,应该是一个interface,比如我们公共界面是用到了t.getName()来得到显示的数据,这个时候

class ChildBean implements BaseBean,Parcelable{

...

@Override

public String getName(){

return "WelliJohn";

}

...

}

当用到了传值的时候,ChildBean再自身实现了Parcelable接口,这样代码就完美了。这样如果真的在公共界面有个特殊的类型的话,判断下T的类型(ChildBean.class.isInstance(t)),强转下也可以进行某个特殊数据处理了。

4.总结

serialization

parcable

文件操作,且用到了反射

单独的内存空间,速度快

会创造大量的读写对象

直接操作内存读写

实现简单

实现复杂,而且读和取的数据要一致

写入的时候,会有字段名,长度等

只是写入数据,节省资源

因为写在文件中,适合持久化数据

不适合持久化数据,可能会变化

如果你们有对3.3的解决方案感觉有更好的处理思路的话,欢迎提出来共同探讨

如果你们觉得文章对你有启示作用,希望你们帮忙点个赞或者关注下,谢谢

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值